本篇文章内容详细可参考官方文档第 29 节。
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的自动配置
SpringMVC 的自动配置类为 org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration 。
视图解析器
SpringBoot 为 SpringMVC 自动配置了 ContentNegotiatingViewResolver 和 BeanNameViewResolver 即视图解析器。
- 视图解析器可以根据方法的返回值得到对应的视图对象,视图对象来决定如何渲染到页面。
BeanNameViewResolver
查看 BeanNameViewResolver 配置:
@Bean @ConditionalOnBean(View.class) @ConditionalOnMissingBean public BeanNameViewResolver beanNameViewResolver() { BeanNameViewResolver resolver = new BeanNameViewResolver(); resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10); return resolver; }
org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#beanNameViewResolver
查看 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); }
org.springframework.web.servlet.view.BeanNameViewResolver#resolveViewName
可以看到,该方法就是在容器中查看是否包含对应名称的 View 实例,如果包含,则直接返回。
ContentNegotiatingViewResolver
查看 ContentNegotiatingViewResolver 配置:
@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; }
org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#viewResolver
查看 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) { List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes); View bestView = getBestView(candidateViews, requestedMediaTypes, attrs); if (bestView != null) { return bestView; } } 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; } }
org.springframework.web.servlet.view.ContentNegotiatingViewResolver#resolveViewName
看到第 7 行通过 getCandidateViews(viewName, locale, requestedMediaTypes) 方法获取候选 View 的列表,查看该方法:
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; }
org.springframework.web.servlet.view.ContentNegotiatingViewResolver#getCandidateViews
可以看到该方法其实是在遍历 this.viewResolvers 来解析获取所有候选的 View,最后返回。那这个 this.viewResolvers 是什么呢?查看该解析器的初始化方法:
@Override protected void initServletContext(ServletContext servletContext) { 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); }
org.springframework.web.servlet.view.ContentNegotiatingViewResolver#initServletContext
这个方法实际上是通过 3-4 行从容器中获取了所有 ViewResolver.class 类型 bean 的集合,即从容器中获取了所有的视图解析器。接着返回到 org.springframework.web.servlet.view.ContentNegotiatingViewResolver#resolveViewName 方法,在 8-10 行获取到最合适的 View 返回。
总的来说,这个视图解析器的作用就是从容器中拿到所有其它的视图解析器解析出 View ,最终从这些 View 中挑选一个最合适的返回。所以我们如果要自己定义视图解析器,只需要将自定义的视图解析器注册到容器即可。
静态资源映射
上一节中有提到,参考【静态资源映射】。
默认首页
上一节中有提到,参考【欢迎页】。
页面图标
上一节中有提到,参考【页面图标】。
类型转换器和格式化器
SpringBoot 为 SpringMVC 自动配置了转换器(Converter)和格式化器(Formater)。例如:客户端发来一个请求携带一些参数,这些参数要与请求方法上的入参进行绑定,进行绑定的过程中涉及到类型转换(如要将 "18" 转换成 Integer 类型),这时就会用到转换器。而如果这些参数中有些参数要转换成日期或其它特殊类型,我们要按约定将请求参数按指定方式格式化(如要将 "3.4.2018" 格式化成我们需要的日期格式),这时就需要用到格式化器。转换器与格式化器的注册在自动配置类中也可以看到:
格式化器
@Bean @ConditionalOnProperty(prefix = "spring.mvc", name = "date-format") public Formatter<Date> dateFormatter() { return new DateFormatter(this.mvcProperties.getDateFormat()); }
org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#dateFormatter
可以看到,SpringBoot 在配置类中注册了一个日期格式化器,而这个日期格式化的规则可以通过配置文件中的 spring.mvc.date-format 属性进行配置。
转换器
@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(); }
org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#addFormatters
在该方法中可以看到,从容器中获取了所有的转换器进行注册。所以我们如果要自己定义转换器,只需要将自定义的转换器注册到容器即可。
消息转换器
消息转换器(HttpMessageConvert)是 SpringMVC 用来转换 HTTP 请求和响应的,例如我们要将一个 JavaBean 实例以 Json 方式输出,此时就会用到消息转换器。消息转换器的注册定义在 SpringMVC 的自动配置类的一个内部类中,这里贴出部分源码:
@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()); }
org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter
可以看到 messageConverters 是通过构造方法以懒加载的形式注入,即 messageConverters 同样是注册在容器中。继续查看 messageConverters 对应类:
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); }
org.springframework.boot.autoconfigure.web.HttpMessageConverters
可以看到该类实现了迭代器接口,且构造函数使用了可变参数,即会将容器中所有的 HttpMessageConverter 注入。所以我们如果要使用自己定义的消息转换器,也只需要将消息转换器注册到容器即可。
消息代码处理器
消息代码处理器(MessageCodesResolver)是用于定义 SpringMVC 的消息代码的生成规则。
@Override public MessageCodesResolver getMessageCodesResolver() { if (this.mvcProperties.getMessageCodesResolverFormat() != null) { DefaultMessageCodesResolver resolver = new DefaultMessageCodesResolver(); resolver.setMessageCodeFormatter( this.mvcProperties.getMessageCodesResolverFormat()); return resolver; } return null; }
org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#getMessageCodesResolver
数据绑定初始化器
数据绑定初始化器(ConfigurableWebBindingInitializer)是初始化 Web 数据绑定器(WebDataBinder),而 Web 数据绑定器是用来绑定 web 数据的,比如将请求参数绑定到 JavaBean 中就会用到 Web 数据绑定器。
@Override protected ConfigurableWebBindingInitializer getConfigurableWebBindingInitializer() { try { return this.beanFactory.getBean(ConfigurableWebBindingInitializer.class); } catch (NoSuchBeanDefinitionException ex) { return super.getConfigurableWebBindingInitializer(); } }
org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.EnableWebMvcConfiguration#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 typeWebMvcConfigurer
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 扩展配置类如下:
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"); } }
com.springboot.webdev1.config.MyMvcConfig
再次查看 SpringMVC 的自动配置类会发现该类有一个内部类也是继承了 WebMvcConfigurerAdapter ,即 SpringBoot 也是通过该方式实现 SpringMVC 的自动配置:
@Configuration @Import(EnableWebMvcConfiguration.class) @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter {
org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter
在该类上有一个注解 @Import(EnableWebMvcConfiguration.class) ,查看 EnableWebMvcConfiguration 类:
@Configuration public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {
org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.EnableWebMvcConfiguration
接着查看 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(); @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); } }
org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration
可以看到通过第 22 行的 setConfigurers(List<WebMvcConfigurer> configurers) 方法注入了所有的 WebMvcConfigurer 实例,即包含了我们自己编写的 WebMvcConfigurer bean。这就是我们自定义 WebMvcConfigurer 能生效的原因。
全面接管SpringMVC配置
全面接管 SpringMVC 配置在上面描述中也有说明,我们只需要在配置类的基础上再使用 @EnableWebMvc 注解即可:
package com.springboot.webdev1.config; 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"); } }
com.springboot.webdev1.config.MyMvcConfig
为什么加上 @EnableWebMvc 注解 SpringBoot 对于 SpringMVC 的自动配置就失效了呢?查看 SpringMVC 的自动配置类:
@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 {
org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration
可以看到配置类上有一个 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) 注解,该注解的意思就是当容器中不存在 WebMvcConfigurationSupport 这个类型的 bean 时自动配置类才生效。再查看 @EnableWebMvc 注解:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(DelegatingWebMvcConfiguration.class) public @interface EnableWebMvc { }
org.springframework.web.servlet.config.annotation.EnableWebMvc
可以看到它使用 @Import(DelegatingWebMvcConfiguration.class) 给容器中导入了 DelegatingWebMvcConfiguration 类型的 bean,而 DelegatingWebMvcConfiguration 又继承自 WebMvcConfigurationSupport ,即使用了 @EnableWebMvc 注解后容器中会注入 WebMvcConfigurationSupport 类型的 bean,此时就与 SpringMVC 自动配置类上的 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) 注解条件相斥,所以自动配置类失效。