MVC 自动配置原理
1、官网阅读
在进行项目编写前,我们还需要知道一个东西,就是SpringBoot对我们的SpringMVC还做了哪些配置,包括如何扩展,如何定制。
只有把这些都搞清楚了,我们在之后使用才会更加得心应手。途径一:源码分析,途径二:官方文档!
1.1 Spring MVC Auto-configuration
Spring Boot 为 Spring MVC 提供了自动配置,它可以很好地与大多数应用程序一起工作。
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.
自动配置在 Spring 默认设置的基础上添加了以下功能:
The auto-configuration adds the following features on top of Spring’s defaults:
包含视图解析器
Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
支持静态资源文件夹的路径,以及 webjars
Support for serving static resources, including support for WebJars
自动注册了 Converter:
转换器,这就是我们网页提交数据到后台自动封装成为对象的东西,比如把 “1” 字符串自动转换为 int 类型
Formatter:【格式化器,比如页面给我们了一个2019-8-10,它会给我们自动格式化为Date对象】
Automatic registration of Converter, GenericConverter, and Formatter beans.
1.2 HttpMessageConverters
SpringMVC 用来转换 Http 请求和响应的的,比如我们要把一个 User 对象转换为 JSON 字符串,可以去看官网文档解释
Support for HttpMessageConverters (covered later in this document).
定义错误代码生成规则的
Automatic registration of MessageCodesResolver (covered later in this document).
首页定制
Static index.html support.
图标定制
Custom Favicon support (covered later in this document).
初始化数据绑定器:帮我们把请求数据绑定到 JavaBean 中!
Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).
如果您希望保留 Spring Boot MVC 功能,并且希望添加其他 MVC 配置(拦截器、格式化程序、视图控制器和其他功能),则可以添加自己
的@configuration
类,类型为webmvcconfiguer
,但不添加@EnableWebMvc
。如果希望提供
RequestMappingHandlerMapping、RequestMappingHandlerAdapter 或 ExceptionHandlerExceptionResolver 的自定义
实例,则可以声明 WebMVCregistrationAdapter 实例来提供此类组件。
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 wish to provide
custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or
ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components.
如果您想完全控制 Spring MVC,可以添加自己的@Configuration
,并用@EnableWebMvc
进行注释。
If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc.
它告诉我们 SpringBoot 已经帮我们自动配置好了 SpringMVC,然后自动配置了哪些东西呢?
2、ContentNegotiatingViewResolver 内容协商视图解析器
自动配置了 ViewResolver,就是我们之前学习的 SpringMVC 的视图解析器;
即根据方法的返回值取得视图对象(View),然后由视图对象决定如何渲染(转发,重定向)。
我们去看看这里的源码:我们找到 WebMvcAutoConfiguration , 然后搜索ContentNegotiatingViewResolver。找到如下方法!
@Bean
@ConditionalOnBean({ViewResolver.class})
@ConditionalOnMissingBean(
name = {"viewResolver"},
value = {ContentNegotiatingViewResolver.class}
)
public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
resolver.setContentNegotiationManager((ContentNegotiationManager)beanFactory.getBean(ContentNegotiationManager.class));
// ContentNegotiatingViewResolver使用所有其他视图解析器来定位视图,因此它应该具有较高的优先级
resolver.setOrder(-2147483648);
return resolver;
}
我们可以点进这类看看!找到对应的解析视图的代码;
@Nullable // 注解说明:@Nullable 即参数可为null
public View resolveViewName(String viewName, Locale locale) throws Exception {
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
List<MediaType> requestedMediaTypes = this.getMediaTypes(((ServletRequestAttributes)attrs).getRequest());
if (requestedMediaTypes != null) {
// 获取候选的视图对象
List<View> candidateViews = this.getCandidateViews(viewName, locale, requestedMediaTypes);
// 选择一个最适合的视图对象,然后把这个对象返回
View bestView = this.getBestView(candidateViews, requestedMediaTypes, attrs);
if (bestView != null) {
return bestView;
}
}
String mediaTypeInfo = this.logger.isDebugEnabled() && requestedMediaTypes != null ? " given " + requestedMediaTypes.toString() : "";
if (this.useNotAcceptableStatusCode) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo);
}
return NOT_ACCEPTABLE_VIEW;
} else {
this.logger.debug("View remains unresolved" + mediaTypeInfo);
return null;
}
}
我们继续点进去看,他是怎么获得候选的视图的呢?
getCandidateViews 中看到他是把所有的视图解析器拿来,进行 while 循环,挨个解析!
private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes) throws Exception {
List<View> candidateViews = new ArrayList();
if (this.viewResolvers != null) {
Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
Iterator var5 = this.viewResolvers.iterator();
while(var5.hasNext()) {
ViewResolver viewResolver = (ViewResolver)var5.next();
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
candidateViews.add(view);
}
Iterator var8 = requestedMediaTypes.iterator();
while(var8.hasNext()) {
MediaType requestedMediaType = (MediaType)var8.next();
List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
Iterator var11 = extensions.iterator();
while(var11.hasNext()) {
String extension = (String)var11.next();
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;
}
所以得出结论:ContentNegotiatingViewResolver 这个视图解析器就是用来组合所有的视图解析器的
我们再去研究下他的组合逻辑,看到有个属性 viewResolvers,看看它是在哪里进行赋值的!
protected void initServletContext(ServletContext servletContext) {
// 这里它是从 beanFactory 工具中获取容器中的所有视图解析器
// ViewRescolver.class 把所有的视图解析器来组合的
Collection<ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.obtainApplicationContext(), ViewResolver.class).values();
ViewResolver viewResolver;
if (this.viewResolvers == null) {
this.viewResolvers = new ArrayList(matchingBeans.size());
Iterator var3 = matchingBeans.iterator();
while(var3.hasNext()) {
viewResolver = (ViewResolver)var3.next();
if (this != viewResolver) {
this.viewResolvers.add(viewResolver);
}
}
} else {
for(int i = 0; i < this.viewResolvers.size(); ++i) {
viewResolver = (ViewResolver)this.viewResolvers.get(i);
if (!matchingBeans.contains(viewResolver)) {
String name = viewResolver.getClass().getName() + i;
this.obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(viewResolver, name);
}
}
}
AnnotationAwareOrderComparator.sort(this.viewResolvers);
this.cnmFactoryBean.setServletContext(servletContext);
}
既然它是在容器中去找视图解析器,我们是否可以猜想,我们就可以去实现一个视图解析器了呢?
我们可以自己给容器中去添加一个视图解析器;这个类就会帮我们自动的将它组合进来;我们去实现一下
3、自定义视图解析器
-
创建 config 目录,编写 myViewResolver
-
我们在我们的主程序中去写一个视图解析器来试试
package com.aze.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.View; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.Locale; @Configuration public class MyMvcConfig implements WebMvcConfigurer { @Bean // 放到 bean 中 public ViewResolver myViewResolver(){ return new MyViewResolver(); } // 我们写一个静态内部类,自定义一个自己的视图解析器 MyViewResolver,视图解析器就需要实现 ViewResolver接口 private static class MyViewResolver implements ViewResolver{ @Override public View resolveViewName(String viewName, Locale locale) throws Exception { return null; } } }
-
怎么看我们自己写的视图解析器有没有起作用呢?
- 我们给 DispatcherServlet 中的 doDispatch 方法 加个断点进行调试一下,因为所有的请求都会走到这个方法中
-
我们启动我们的项目,然后随便访问一个页面,看一下 Debug 信息
-
找到 this
-
找到视图解析器,我们看到我们自己定义的就在这里了
-
总结:
所以说,我们如果想要使用自己定制化的东西,我们只需要给容器中添加这个组件就好了!剩下的事情 SpringBoot 就会帮我们做了!
4、转换器和格式化器
找到格式化转换器
@Bean
public FormattingConversionService mvcConversionService() {
Format format = this.mvcProperties.getFormat();
// 拿到配置文件中的格式化规则
WebConversionService conversionService = new WebConversionService((new DateTimeFormatters()).dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime()));
this.addFormatters(conversionService);
return conversionService;
}
点进去
public String getDateTime() {
// Date format to use. For instance, `dd/MM/yyyy`. 默认的
return this.dateTime;
}
可以看到在我们的 Properties 文件中,我们可以进行自动配置它!
如果配置了自己的格式化方式,就会注册到 Bean 中生效,我们可以在配置文件中配置日期格式化的规则:
spring.mvc.date-format=
5、修改 SpringBoot 的默认配置
这么多的自动配置,原理都是一样的,通过这个 WebMVC 的自动配置原理分析,我们要学会一种学习方式,通过源码探究,得出结论;这个结论一定是属于自己的,而且一通百通。
SpringBoot 的底层,大量用到了这些设计细节思想,所以,没事需要多阅读源码!得出结论;
SpringBoot 在自动配置很多组件的时候,先看容器中有没有用户自己配置的(如果用户自己配置@bean),如果有就用用户配置的,如果没有就用自动配置的;
如果有些组件可以存在多个,比如我们的视图解析器,就将用户配置的和自己默认的组合起来!
扩展使用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 wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components.
我们要做的就是编写一个@Configuration
注解类,并且类型要为WebMvcConfigurer
,还不能标注@EnableWebMvc
注解;我们去自己写一个;我们新建一个包叫 config,写一个类 MyMvcConfig;
package com.aze.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
// 应为类型要求为 WebMvcConfigurer,所以我们实现其接口
// 可以使用自定义类扩展MVC的功能
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 浏览器发送 /aze , 就会跳转到 test 页面;
registry.addViewController("/aze").setViewName("test");
}
}
启动,并在浏览器访问一下看看:
确实也跳转过来了!所以说,我们要扩展 SpringMVC,官方就推荐我们这么去使用,既保 SpringBoot 留所有的自动配置,也能用我们扩展的配置!
我们可以去分析一下原理:
-
WebMvcAutoConfiguration 是 SpringMVC 的自动配置类,里面有一个类 WebMvcAutoConfigurationAdapter
-
这个类上有一个注解,在做其他自动配置时会导入:@Import(EnableWebMvcConfiguration.class)
-
我们点进 EnableWebMvcConfiguration 这个类看一下,它继承了一个父类:DelegatingWebMvcConfiguration
-
这个父类中有这样一段代码
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport { private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite(); public DelegatingWebMvcConfiguration() { } // 从容器中获取所有的 webmvcConfigurer @Autowired( required = false ) public void setConfigurers(List<WebMvcConfigurer> configurers) { if (!CollectionUtils.isEmpty(configurers)) { this.configurers.addWebMvcConfigurers(configurers); } } // ... }
-
-
我们可以在这个类中去寻找一个我们刚才设置的 viewController 当做参考,发现它调用了一个
protected void addViewControllers(ViewControllerRegistry registry) { this.configurers.addViewControllers(registry); }
-
点进去看一下
public void addViewControllers(ViewControllerRegistry registry) { Iterator var2 = this.delegates.iterator(); while(var2.hasNext()) { // 将所有的 WebMvcConfigurer 相关配置来一起调用!包括我们自己配置的和 Spring 给我们配置的 WebMvcConfigurer delegate = (WebMvcConfigurer)var2.next(); delegate.addViewControllers(registry); } }
结论:所有的 WebMvcConfiguration 都会被作用,不止 Spring 自己的配置类,我们自己的配置类当然也会被调用;
6、全面接管 SpringMVC
官方文档:If you want to take complete control of Spring MVCyou can add your own @Configuration annotated with @EnableWebMvc.
全面接管即:SpringBoot 对 SpringMVC 的自动配置不需要了,所有都是我们自己去配置!
只需在我们的配置类中要加一个@EnableWebMv
c。
我们看下如果我们全面接管了 SpringMVC 了,我们之前 SpringBoot 给我们配置的静态资源映射一定会无效,我们可以去测试一下;
不加注解之前,访问首页:
给配置类加上注解:@EnableWebMvc
我们发现所有的 SpringMVC 自动配置都失效了!回归到了最初的样子;
当然,我们开发中,不推荐使用全面接管 SpringMVC
思考问题?为什么加了一个注解,自动配置就失效了!我们看下源码:
-
点进这个注解,发现它是导入了一个类,我们可以继续进去看
-
点进去,发现它继承了一个父类 WebMvcConfigurationSupport
-
我们来回顾一下 Webmvc 自动配置类
@Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }) // 这个注解的意思就是:容器中没有这个组件的时候,这个自动配置类才生效 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class }) public class WebMvcAutoConfiguration { }
总结一句话:@EnableWebMvc
将 WebMvcConfigurationSupport 组件导入进来了;
而导入的 WebMvcConfigurationSupport 只是 SpringMVC 最基本的功能!
在 SpringBoot 中会有非常多的扩展配置,只要看见了这个,我们就应该多留心注意~