avatar

02.SpringMVC自动配置解析

一、入口

SpringMVC自动配置的入口是WebMvcAutoConfiguration,声明如下:

1
2
3
4
5
6
7
8
9
10
@Configuration  //声明此类是个配置类
@ConditionalOnWebApplication(type = Type.SERVLET) //web应用才启用配置
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }) //classPath下有这些类才启动
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) //容器中不存在WebMvcConfigurationSupport这个类才启用
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
ValidationAutoConfiguration.class }) //在这两个类加入容器后才自动注入
public class WebMvcAutoConfiguration {
………………
}

简要分析下,WebMvcAutoConfiguration这个类只有在满足以下条件才会自动配置:

  • 必须是web应用。而且是Servlet类型的
  • 在classPath下,必须有Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class 这里个类
  • 在容器中,不得存在WebMvcConfigurationSupport这个类以及子类

二、中间体分析

在WebMvcAutoConfiguration类中,中间体我们只看以下部分,剩下的在下一节给出:

  1. addResourceHandlers:这个方法是用来绑定静态资源的。从代码上我们可以知道在SpringBoot中,静态资源放在哪,我们才可以访问到。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//静态资源路径
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
"classpath:/META-INF/resources/", "classpath:/resources/",
"classpath:/static/", "classpath:/public/" };
/**
* Locations of static resources. Defaults to classpath:[/META-INF/resources/,
* /resources/, /static/, /public/].
*/
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache()
.getCachecontrol().toHttpCacheControl();
//如果没有/webjars/**这个路径映射。就增加
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry
.addResourceHandler("/webjars/**") //增加/webjars/**路径映射
//设置映射本地的路径为classpath:/META-INF/resources/webjars/
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod))
.setCacheControl(cacheControl));
}
//获取静态的路径映射/**
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
//如果没有/**这个路径映射,就添加
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(
registry.addResourceHandler(staticPathPattern)//添加/**的路径映射
.addResourceLocations(getResourceLocations(
this.resourceProperties.getStaticLocations()))//获取本地位置。本地位置值在方法上面
.setCachePeriod(getSeconds(cachePeriod))
.setCacheControl(cacheControl));
}
}
  • addResourceHandlers总结:
    • addResourceHandlers就是配置静态资源映射。主要是配置了如下几个:
      • 所有/webjars/**的请求,都会去classPath下的/META-INF/resources/webjars下面去找。
      • 所有的/**的请求,都会去classpath:/META-INF/resources/, classpath:/resources/,classpath:/static/, classpath:/public/下面去找。
    • 根据以上结论,我们只需要把静态资源放到classpath:/META-INF/resources/, classpath:/resources/,classpath:/static/, classpath:/public/下面即可。而webjars本身就是把静态资源,如jquery.js打包成一个jar包形式,我们只需要在maven中引用下,就可以使用了。
  1. WelcomePageHandlerMapping:配置欢迎页
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Bean
//欢迎页配置
public WelcomePageHandlerMapping welcomePageHandlerMapping(
ApplicationContext applicationContext) {
return new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext),
applicationContext, getWelcomePage(),//getWelcomePage()获取欢迎页位置
this.mvcProperties.getStaticPathPattern());
}

private Optional<Resource> getWelcomePage() {
String[] locations = getResourceLocations(
this.resourceProperties.getStaticLocations());//获取静态资源本地路径
//双冒号的用法,就是把方法当做参数传到stream内部,使stream的每个元素都传入到该方法里面执行一下。
return Arrays.stream(locations).map(this::getIndexHtml)
.filter(this::isReadable).findFirst();//遍历每个静态资源路径,拿到第一个名为index.html的页面作为欢迎页
}

private Resource getIndexHtml(String location) {
return this.resourceLoader.getResource(location + "index.html");
}
  • WelcomePageHandlerMapping总结
    • WelcomePageHandlerMapping是配置欢迎页,会遍历静态资源路径,寻找第一个名为index.html的页面为欢迎页。\
  1. FaviconConfiguration:图标配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    @Configuration
    //需要开启spring.mvc.favicon.enabled这个配置,但是默认是开启的
    @ConditionalOnProperty(value = "spring.mvc.favicon.enabled", matchIfMissing = true)
    public static class FaviconConfiguration implements ResourceLoaderAware {

    private final ResourceProperties resourceProperties;

    private ResourceLoader resourceLoader;

    public FaviconConfiguration(ResourceProperties resourceProperties) {
    this.resourceProperties = resourceProperties;
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
    this.resourceLoader = resourceLoader;
    }

    //设置图标映射
    @Bean
    public SimpleUrlHandlerMapping faviconHandlerMapping() {
    SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
    mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
    //所有的**/favicon.ico都会映射到faviconRequestHandler()的路径上。也就是静态资源路径上
    mapping.setUrlMap(Collections.singletonMap("**/favicon.ico",
    faviconRequestHandler()));
    return mapping;
    }

    @Bean
    public ResourceHttpRequestHandler faviconRequestHandler() {
    ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler();
    requestHandler.setLocations(resolveFaviconLocations());
    return requestHandler;
    }

    private List<Resource> resolveFaviconLocations() {
    //静态资源映射
    String[] staticLocations = getResourceLocations(
    this.resourceProperties.getStaticLocations());
    List<Resource> locations = new ArrayList<>(staticLocations.length + 1);
    Arrays.stream(staticLocations).map(this.resourceLoader::getResource)
    .forEach(locations::add);
    locations.add(new ClassPathResource("/"));
    return Collections.unmodifiableList(locations);
    }

    }
    • FaviconConfiguration总结:配置页面图标,所有的**/favicon.ico路径,都会去静态资源路径下去找。
  2. beanNameResolver、viewResolver。也就是说SpringBoot帮我们自动配置了ContentNegotiatingViewResolver、BeanNameViewResolver视图解析器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    @Bean
    @ConditionalOnMissingBean
    public InternalResourceViewResolver defaultViewResolver() {// 默认的视图解析器是InternalResourceViewResolver
    InternalResourceViewResolver resolver = new InternalResourceViewResolver();
    resolver.setPrefix(this.mvcProperties.getView().getPrefix());
    resolver.setSuffix(this.mvcProperties.getView().getSuffix());
    return resolver;
    }

    @Bean
    @ConditionalOnBean(View.class)
    @ConditionalOnMissingBean
    public BeanNameViewResolver beanNameViewResolver() {
    BeanNameViewResolver resolver = new BeanNameViewResolver();
    resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
    return resolver;
    }

    @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));
    // ContentNegotiatingViewResolver uses all the other view resolvers to locate
    // a view so it should have a high precedence
    resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
    return resolver;
    }

三、如何定制我们自己需要的SpringMVC组件

  1. SpringBoot帮我们配置了什么?
    • Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
      • 自动配置了两个视图解析器
      • ContentNegotiatingViewResolver:组合所有的视图解析器的;
      • 如何定制我们自己的?
        • 往容器中加入一个视图解析器即可。SpringBoot会自动帮我们组合起来
    • Support for serving static resources, including support for WebJars (see below).
      • 配置好了静态资源路径以及webjars映射的路径
    • Automatic registration of Converter, GenericConverter, Formatter beans.
      • 自动注册了转换器、格式器
      • 如何定制?
        • 在容器中添加一个自己的转换器或格式器即可。
    • Support for HttpMessageConverters (see below).
      • HttpMessageConverter:SpringMVC用来转换Http请求和响应的;User—Json;
      • HttpMessageConverters 是从容器中确定;获取所有的HttpMessageConverter;
      • 如何定制?
        • 只需要在容器中添加一个自己的HttpMessageConverter。
    • Automatic registration of MessageCodesResolver (see below).
      • 定义错误代码生成规则
    • Static index.html support.
      • 静态首页访问
    • Custom Favicon support (see below).
      • 图标访问
    • Automatic use of a ConfigurableWebBindingInitializer bean (see below).
  2. 综上所述,如果需要定制我们自己需要的东西,可以直接在容器中添加相应的Bean即可。

四、如何扩展SpringMVC

  1. 官方文档上的原文:

    If you want to keep Spring Boot MVC features, and you just want to add additional MVC configuration (interceptors, formatters, view controllers etc.) you can add your own @Configuration class of type WebMvcConfigurerAdapter, but without @EnableWebMvc. If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter or ExceptionHandlerExceptionResolver you can declare a WebMvcRegistrationsAdapter instance providing such components.

  2. 翻译出来的意识大概是这样的,如果我们需要扩展SpringMVC,需要做以下几点,这样就扩展了SpringMVC,并且自动配置的内容也还生效。即既保留了自动配置的,也有我们自己扩展的。相互合作。:

    1. 定义一个类,在类名上加上Configuration 注解
    2. 该类继承WebMvcConfigurerAdapter 在最新版本的SpringBoot中,已经不需要继承这个适配类,而是直接实现WebMvcConfigurer
    3. 如果你需要定制RequestMappingHandlerMappingRequestMappingHandlerAdapterExceptionHandlerExceptionResolver ,需要声明一个WebMvcRegistrationsAdapter 实例。
    4. ==不能再类名上加上@EnableWebMvc 注解。==
  3. 原理:

    1. 在SpringMVC的自动配置类中,有如下代码:

      1
      2
      3
      4
      5
      6
      @Configuration
      @Import(EnableWebMvcConfiguration.class)
      @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
      @Order(0)
      public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ResourceLoaderAware {
      }

      这个内部类在声明处做了如下几件事:

      • @Configuration:声明该类是一个配置类
      • @Import(EnableWebMvcConfiguration.class) 加载了EnableWebMvcConfiguration到容器
      • @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) 为WebMvcProperties、ResourceProperties绑定了相应的配置文件。
    2. 既然加载了EnableWebMvcConfiguration这个类,那我们看看这个类的声明。如下:

      1
      2
      3
      @Configuration
      public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {
      }

      该类也是个配置类。我们看看他的父类做了什么。代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      @Configuration
      public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {

      private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
      //自动注入所有的WebMvcConfigurer
      @Autowired(required = false)
      public void setConfigurers(List<WebMvcConfigurer> configurers) {
      if (!CollectionUtils.isEmpty(configurers)) {
      this.configurers.addWebMvcConfigurers(configurers);
      }
      }
      }

      可以看到他是会自动注入所有WebMvcConfigurer实例。而WebMvcConfigurerAdapter的声明是这样的。

      1
      2
      3
      public abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer {

      }

      而我们在扩展的时候,继承了WebMvcConfigurerAdapter ,所以我们这个扩展类也是WebMvcConfigurer的子类,所以就会自动注入进去。实现和自动配置相互合作的效果。

      那么,他是怎么调用我们的呢?在DelegatingWebMvcConfiguration这个类中有这样的方法:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      @Override
      protected void addViewControllers(ViewControllerRegistry registry) {
      this.configurers.addViewControllers(registry);
      }
      //this.configurers.addViewControllers(registry)调的就是下面这个方法。
      @Override
      public void addViewControllers(ViewControllerRegistry registry) {
      for (WebMvcConfigurer delegate : this.delegates) {
      delegate.addViewControllers(registry);
      }
      }

      可以看见,他是遍历所有的WebMvcConfigurer。

五、如何覆盖SpringMVC的自动配置

  1. 官方文档原文:

    If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc.

    1. 翻译出来大概意思为:如果你需要完全控制SpringMVC,你需要做如下几点:

    2. 写一个类,上面加上@Configuration ,并在上面加上 @EnableWebMvc

    3. 原理:

    4. 在SpringMVC自动配置类的上的声明如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Configuration
    @ConditionalOnWebApplication(type = Type.SERVLET)
    @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
    @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) //不存在WebMvcConfigurationSupport这个类以下配置才生效
    @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
    @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
    ValidationAutoConfiguration.class })
    public class WebMvcAutoConfiguration {

    }
    • 可以在上面看到,只有在容器里没有WebMvcConfigurationSupport这个类的实例,自动配置才会生效。

    • 那么,现在我们看看@EnableWebMvc

      • @EnableWebMvc的声明
      1
      2
      3
      4
      5
      6
      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.TYPE)
      @Documented
      @Import(DelegatingWebMvcConfiguration.class) //往容器中添加DelegatingWebMvcConfiguration的实例
      public @interface EnableWebMvc {
      }
    • DelegatingWebMvcConfiguration 声明如下

      1
      2
      @Configuration
      public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {

    }

1
2
3
4

- 从代码上来看,他是继承WebMvcConfigurationSupport。是WebMvcConfigurationSupport的一个实例.
- 结论
- 如果加上了`@EnableWebMvc` 则会导入`DelegatingWebMvcConfiguration` ,而`DelegatingWebMvcConfiguration` 继承了`WebMvcConfigurationSupport` ,但是SpringMVC自动配置生效的条件是容器中没有`WebMvcConfigurationSupport` 这个实例。所以加上了`@EnableWebMvc` 就会忽略SpringMVC的自动配置。

六、结论

  1. 如何定制我们需要的SpringMVC组件?
    • 只需要把我们需要的组件加入到容器中即可
  2. 如何扩展SpringMVC的功能?
    • 编写一个配置类,即加上@Configuration
    • 该类继承WebMvcConfigurerAdapter
    • 不能再类名上加上@EnableWebMvc 注解。
  3. 如何覆盖SpringMVC的自动配置
    • 编写一个配置类,即加上@Configuration
    • 加上@EnableWebMvc 注解

评论