SpringMVC 概述
SpringBoot 非常适合 Web 应用程序开发。可以使用嵌入式 Tomcat,Jetty,Undertow 或 Netty 创建自包含的 HTTP 服务器。大多数 Web 应用程序可以通过使用 spring-boot-starter-web
模块快速启动和运行。你还可以选择使用该 spring-boot-starter-webflux
模块构建响应式 Web 应用程序 。
SpringMVC 框架是一个丰富的“模型视图控制器” Web框架。SpringMVC 可以通过使用 @Controller
或 @RestController
注解来标注控制器处理传入的 HTTP 请求。控制器中的方法通过使用 @RequestMapping
注解完成请求映射 。
SpringMVC 是 Spring Framework 的一部分,详细信息可参考官方文档,也可参考博客。
SpringMVC 在 SpringBoot 中的自动配置
SpringMVC 的自动配置类为 org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration
。
视图解析器
SpringBoot 为 SpringMVC 自动配置了 ContentNegotiatingViewResolver
和 BeanNameViewResolver
即视图解析器。
视图解析器可以根据方法的返回值得到对应的视图对象,视图对象来决定如何渲染到页面。
BeanNameViewResolver
查看 BeanNameViewResolver
配置:
// org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#beanNameViewResolver
@Bean
@ConditionalOnBean(View.class)
@ConditionalOnMissingBean
public BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
return resolver;
}
查看 BeanNameViewResolver
的 resolveViewName
方法:
// org.springframework.web.servlet.view.BeanNameViewResolver#resolveViewName
@Override
public View resolveViewName(String viewName, Locale locale) throws BeansException {
ApplicationContext context = getApplicationContext();
if (!context.containsBean(viewName)) {
if (logger.isDebugEnabled()) {
logger.debug("No matching bean found for view name '" + viewName + "'");
}
// Allow for ViewResolver chaining...
return null;
}
if (!context.isTypeMatch(viewName, View.class)) {
if (logger.isDebugEnabled()) {
logger.debug("Found matching bean for view name '" + viewName +
"' - to be ignored since it does not implement View");
}
// Since we're looking into the general ApplicationContext here,
// let's accept this as a non-match and allow for chaining as well...
return null;
}
return context.getBean(viewName, View.class);
}
可以看到,该方法就是在容器中查看是否包含对应名称的 View
实例,如果包含,则直接返回。
ContentNegotiatingViewResolver
查看 ContentNegotiatingViewResolver
配置:
// org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#viewResolver
@Bean
@ConditionalOnBean(ViewResolver.class)
@ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class)
public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
resolver.setContentNegotiationManager(
beanFactory.getBean(ContentNegotiationManager.class));
resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
return resolver;
}
查看 ContentNegotiatingViewResolver
的 resolveViewName
方法:
// org.springframework.web.servlet.view.ContentNegotiatingViewResolver#resolveViewName
@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
Assert.isInstanceOf(ServletRequestAttributes.class, attrs);
List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
if (requestedMediaTypes != null) {
// <1>
List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
// <3 - begin>
View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
if (bestView != null) {
return bestView;
}
// <3 - end>
}
if (this.useNotAcceptableStatusCode) {
if (logger.isDebugEnabled()) {
logger.debug("No acceptable view found; returning 406 (Not Acceptable) status code");
}
return NOT_ACCEPTABLE_VIEW;
}
else {
logger.debug("No acceptable view found; returning null");
return null;
}
}
看到 <1>
处通过 getCandidateViews(viewName, locale, requestedMediaTypes)
方法获取候选 View
的列表,查看该方法:
// org.springframework.web.servlet.view.ContentNegotiatingViewResolver#getCandidateViews
private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
throws Exception {
List<View> candidateViews = new ArrayList<View>();
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
candidateViews.add(view);
}
for (MediaType requestedMediaType : requestedMediaTypes) {
List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
for (String extension : extensions) {
String viewNameWithExtension = viewName + "." + extension;
view = viewResolver.resolveViewName(viewNameWithExtension, locale);
if (view != null) {
candidateViews.add(view);
}
}
}
}
if (!CollectionUtils.isEmpty(this.defaultViews)) {
candidateViews.addAll(this.defaultViews);
}
return candidateViews;
}
可以看到该方法其实是在遍历 this.viewResolvers
来解析获取所有候选的 View
,最后返回。那这个 this.viewResolvers
是什么呢?查看该解析器的初始化方法:
// org.springframework.web.servlet.view.ContentNegotiatingViewResolver#initServletContext
@Override
protected void initServletContext(ServletContext servletContext) {
<2>
Collection<ViewResolver> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(getApplicationContext(), ViewResolver.class).values();
if (this.viewResolvers == null) {
this.viewResolvers = new ArrayList<ViewResolver>(matchingBeans.size());
for (ViewResolver viewResolver : matchingBeans) {
if (this != viewResolver) {
this.viewResolvers.add(viewResolver);
}
}
}
else {
for (int i = 0; i < viewResolvers.size(); i++) {
if (matchingBeans.contains(viewResolvers.get(i))) {
continue;
}
String name = viewResolvers.get(i).getClass().getName() + i;
getApplicationContext().getAutowireCapableBeanFactory().initializeBean(viewResolvers.get(i), name);
}
}
if (this.viewResolvers.isEmpty()) {
logger.warn("Did not find any ViewResolvers to delegate to; please configure them using the " +
"'viewResolvers' property on the ContentNegotiatingViewResolver");
}
AnnotationAwareOrderComparator.sort(this.viewResolvers);
this.cnmFactoryBean.setServletContext(servletContext);
}
这个方法实际上是通过 <2>
处从容器中获取了所有 ViewResolver.class
类型 bean 的集合,即从容器中获取了所有的视图解析器。接着返回到 org.springframework.web.servlet.view.ContentNegotiatingViewResolver#resolveViewName
方法,在 <3>
处获取到最合适的 View
返回。
总的来说,这个视图解析器的作用就是从容器中拿到所有其它的视图解析器解析出 View
,最终从这些 View
中挑选一个最合适的返回。所以我们如果要自己定义视图解析器,只需要将自定义的视图解析器注册到容器即可。
类型转换器和格式化器
SpringBoot 为 SpringMVC 自动配置了转换器(Converter
)和格式化器(Formater
)。例如:客户端发来一个请求携带一些参数,这些参数要与请求方法上的入参进行绑定,进行绑定的过程中涉及到类型转换(如要将 "18"
转换成 Integer
类型),这时就会用到转换器。而如果这些参数中有些参数要转换成日期或其它特殊类型,我们要按约定将请求参数按指定方式格式化(如要将 "3.4.2018"
格式化成我们需要的日期格式),这时就需要用到格式化器。转换器与格式化器的注册在自动配置类中也可以看到。
格式化器
// org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#dateFormatter
@Bean
@ConditionalOnProperty(prefix = "spring.mvc", name = "date-format")
public Formatter<Date> dateFormatter() {
return new DateFormatter(this.mvcProperties.getDateFormat());
}
可以看到,SpringBoot 在配置类中注册了一个日期格式化器,而这个日期格式化的规则可以通过配置文件中的 spring.mvc.date-format
属性进行配置。
类型转换器
// org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#addFormatters
@Override
public void addFormatters(FormatterRegistry registry) {
for (Converter<?, ?> converter : getBeansOfType(Converter.class)) {
registry.addConverter(converter);
}
for (GenericConverter converter : getBeansOfType(GenericConverter.class)) {
registry.addConverter(converter);
}
for (Formatter<?> formatter : getBeansOfType(Formatter.class)) {
registry.addFormatter(formatter);
}
}
private <T> Collection<T> getBeansOfType(Class<T> type) {
return this.beanFactory.getBeansOfType(type).values();
}
在该方法中可以看到,从容器中获取了所有的转换器进行注册。所以我们如果要自己定义转换器,只需要将自定义的转换器注册到容器即可。
消息转换器
消息转换器(HttpMessageConvert
)是 SpringMVC 用来转换 HTTP 请求和响应的,例如我们要将一个 JavaBean 实例以 Json 方式输出,此时就会用到消息转换器。消息转换器的注册定义在 SpringMVC 的自动配置类的一个内部类中,这里贴出部分源码:
// org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter
@Configuration
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter {
private static final Log logger = LogFactory
.getLog(WebMvcConfigurerAdapter.class);
private final ResourceProperties resourceProperties;
private final WebMvcProperties mvcProperties;
private final ListableBeanFactory beanFactory;
private final HttpMessageConverters messageConverters;
final ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer;
public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties,
WebMvcProperties mvcProperties, ListableBeanFactory beanFactory,
@Lazy HttpMessageConverters messageConverters,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider) {
this.resourceProperties = resourceProperties;
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConverters = messageConverters;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider
.getIfAvailable();
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.addAll(this.messageConverters.getConverters());
}
可以看到 messageConverters
是通过构造方法以懒加载的形式注入,即 messageConverters
同样是注册在容器中。继续查看 messageConverters
对应类:
// org.springframework.boot.autoconfigure.web.HttpMessageConverters
public class HttpMessageConverters implements Iterable<HttpMessageConverter<?>> {
private static final List<Class<?>> NON_REPLACING_CONVERTERS;
static {
List<Class<?>> nonReplacingConverters = new ArrayList<Class<?>>();
addClassIfExists(nonReplacingConverters, "org.springframework.hateoas.mvc."
+ "TypeConstrainedMappingJackson2HttpMessageConverter");
NON_REPLACING_CONVERTERS = Collections.unmodifiableList(nonReplacingConverters);
}
private final List<HttpMessageConverter<?>> converters;
/**
* Create a new {@link HttpMessageConverters} instance with the specified additional
* converters.
* @param additionalConverters additional converters to be added. New converters will
* be added to the front of the list, overrides will replace existing items without
* changing the order. The {@link #getConverters()} methods can be used for further
* converter manipulation.
*/
public HttpMessageConverters(HttpMessageConverter<?>... additionalConverters) {
this(Arrays.asList(additionalConverters));
}
/**
* Create a new {@link HttpMessageConverters} instance with the specified additional
* converters.
* @param additionalConverters additional converters to be added. Items are added just
* before any default converter of the same type (or at the front of the list if no
* default converter is found) The {@link #postProcessConverters(List)} method can be
* used for further converter manipulation.
*/
public HttpMessageConverters(
Collection<HttpMessageConverter<?>> additionalConverters) {
this(true, additionalConverters);
}
/**
* Create a new {@link HttpMessageConverters} instance with the specified converters.
* @param addDefaultConverters if default converters should be added
* @param converters converters to be added. Items are added just before any default
* converter of the same type (or at the front of the list if no default converter is
* found) The {@link #postProcessConverters(List)} method can be used for further
* converter manipulation.
*/
public HttpMessageConverters(boolean addDefaultConverters,
Collection<HttpMessageConverter<?>> converters) {
List<HttpMessageConverter<?>> combined = getCombinedConverters(converters,
addDefaultConverters ? getDefaultConverters()
: Collections.<HttpMessageConverter<?>>emptyList());
combined = postProcessConverters(combined);
this.converters = Collections.unmodifiableList(combined);
}
可以看到该类实现了迭代器接口,且构造函数使用了可变参数,即会将容器中所有的 HttpMessageConverter
注入。所以我们如果要使用自己定义的消息转换器,也只需要将消息转换器注册到容器即可。
消息代码处理器
消息代码处理器(MessageCodesResolver
)是用于定义 SpringMVC 的消息代码的生成规则。
// org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#getMessageCodesResolver
@Override
public MessageCodesResolver getMessageCodesResolver() {
if (this.mvcProperties.getMessageCodesResolverFormat() != null) {
DefaultMessageCodesResolver resolver = new DefaultMessageCodesResolver();
resolver.setMessageCodeFormatter(
this.mvcProperties.getMessageCodesResolverFormat());
return resolver;
}
return null;
}
数据绑定初始化器
数据绑定初始化器(ConfigurableWebBindingInitializer
)是初始化 Web 数据绑定器(WebDataBinder),而 Web 数据绑定器是用来绑定 Web 数据的,比如将请求参数绑定到 JavaBean 中就会用到 Web 数据绑定器。
// org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.EnableWebMvcConfiguration#getConfigurableWebBindingInitializer
@Override
protected ConfigurableWebBindingInitializer getConfigurableWebBindingInitializer() {
try {
return this.beanFactory.getBean(ConfigurableWebBindingInitializer.class);
}
catch (NoSuchBeanDefinitionException ex) {
return super.getConfigurableWebBindingInitializer();
}
}
小结
SpringBoot 在自动配置很多组件时,会先判断容器中有没有用户已注册的对应组件,如果没有,才自动配置它提供的默认组件。如果有些组件可以有多个(例如视图解析器),那么 SpringBoot 会将用户自己的配置和默认的配置组合起来。
SpringMVC 的扩展配置
下面是官方文档关于扩展配置的一段描述:
If you want to keep Spring Boot MVC features and you want to add additional MVC configuration (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc.
If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc.
如果你想在保留 Spring Boot MVC 一些默认配置的基础上添加一些额外的 MVC 配置,例如:拦截器、格式化器、视图控制器等,你可以使用
@Configuration
注解自定义一个WebMvcConfigurer
类但不能使用@EnableWebMvc
。
如果你想完全控制 Spring MVC,你可以在你的配置类上同时使用@Configuration
和@EnableWebMvc
。
定义 SpringMVC 配置类
WebMvcConfigurerAdapter
是 WebMvcConfigurer
的适配器类,按照上面描述我们可以定义 SpringMVC 扩展配置类如下:
// com.springboot.webdev1.config.MyMvcConfig
package com.springboot.webdev1.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* 要扩展什么功能直接重写对应方法即可
* 既保留了所有自动配置,也使用了我们扩展的配置
*/
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {
/**
* 添加视图映射
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 请求路径 /test 将会转发到名为 test 的视图
registry.addViewController("/test").setViewName("test");
}
}
源码分析
回头再查看 SpringMVC 的自动配置类会发现该类有一个内部类也是继承了 WebMvcConfigurerAdapter
,即 SpringBoot 也是通过该方式实现 SpringMVC 的自动配置:
// org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter
@Configuration
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter {
在该类上有一个注解 @Import(EnableWebMvcConfiguration.class)
,查看 EnableWebMvcConfiguration
类:
// org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.EnableWebMvcConfiguration
@Configuration
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {
接着查看 DelegatingWebMvcConfiguration
类:
// org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration
package org.springframework.web.servlet.config.annotation;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.HandlerExceptionResolver;
@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
// <1>
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (configurers == null || configurers.isEmpty()) {
return;
}
this.configurers.addWebMvcConfigurers(configurers);
}
@Override
protected void addInterceptors(InterceptorRegistry registry) {
this.configurers.addInterceptors(registry);
}
@Override
protected void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
this.configurers.configureContentNegotiation(configurer);
}
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
this.configurers.configureAsyncSupport(configurer);
}
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
this.configurers.configurePathMatch(configurer);
}
@Override
protected void addViewControllers(ViewControllerRegistry registry) {
this.configurers.addViewControllers(registry);
}
@Override
protected void configureViewResolvers(ViewResolverRegistry registry) {
this.configurers.configureViewResolvers(registry);
}
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
this.configurers.addResourceHandlers(registry);
}
@Override
protected void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
this.configurers.configureDefaultServletHandling(configurer);
}
@Override
protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
this.configurers.addArgumentResolvers(argumentResolvers);
}
@Override
protected void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {
this.configurers.addReturnValueHandlers(returnValueHandlers);
}
@Override
protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
this.configurers.configureMessageConverters(converters);
}
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
this.configurers.extendMessageConverters(converters);
}
@Override
protected void addFormatters(FormatterRegistry registry) {
this.configurers.addFormatters(registry);
}
@Override
protected Validator getValidator() {
return this.configurers.getValidator();
}
@Override
protected MessageCodesResolver getMessageCodesResolver() {
return this.configurers.getMessageCodesResolver();
}
@Override
protected void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
this.configurers.configureHandlerExceptionResolvers(exceptionResolvers);
}
@Override
protected void addCorsMappings(CorsRegistry registry) {
this.configurers.addCorsMappings(registry);
}
}
可以看到通过 <1>
处的 setConfigurers(List<WebMvcConfigurer> configurers)
方法注入了所有的 WebMvcConfigurer
实例,即包含了我们自己编写的 WebMvcConfigurer bean
。这就是我们自定义 WebMvcConfigurer
能生效的原因。
全面接管 SpringMVC 配置
全面接管 SpringMVC 配置在上面描述中也有说明,我们只需要在配置类的基础上再使用 @EnableWebMvc 注解即可:
// com.springboot.webdev1.config.MyMvcConfig
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* 全面接管 SpringMVC 配置,Spring 的自动配置已失效
*/
@EnableWebMvc
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {
/**
* 添加视图映射
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 请求路径 /test 将会转发到名为 test 的视图
registry.addViewController("/test").setViewName("test");
}
}
源码分析
为什么加上 @EnableWebMvc
注解 SpringBoot 对于 SpringMVC 的自动配置就失效了呢?查看 SpringMVC 的自动配置类:
// org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class,
WebMvcConfigurerAdapter.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
可以看到配置类上有一个 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
注解,该注解的意思就是当容器中不存在 WebMvcConfigurationSupport
这个类型的 bean 时自动配置类才生效。再查看 @EnableWebMvc
注解:
// org.springframework.web.servlet.config.annotation.EnableWebMvc
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}
可以看到它使用 @Import(DelegatingWebMvcConfiguration.class)
给容器中导入了 DelegatingWebMvcConfiguration
类型的 bean,而 DelegatingWebMvcConfiguration
又继承自 WebMvcConfigurationSupport
,即使用了 @EnableWebMvc
注解后容器中会注入 WebMvcConfigurationSupport
类型的 bean,此时就与 SpringMVC 自动配置类上的 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
注解条件相斥,所以自动配置类失效。
评论区