前言

本来没有计划这一篇文章的,只是在看完SpringBoot核心原理后,突然想到之前开发中遇到的MVC自动失效的问题,虽然网上有很多文章以及官方文档都说明了原因,但还是想亲自看一看,本以为很简单的事情,没想到却引发出一个较复杂的问题,请教了很多人都没有得到结果,网上文章也没有写清楚的,最后还是自己搞了很久才弄明白的,此篇主要记录自己的一个分析过程,。

正文

引出问题



上面是SpringBoot MVC的自动配置,问题是这样的,当我们需要自己配置MVC时,有三种选择:

  • 实现WebMvcConfigurer接口
  • 继承WebMvcConfigurerAdapter
  • 继承WebMvcConfigurationSupport

在老版本中我们常用的做法就是继承WebMvcConfigurerAdapter类,这个类本身是实现了WebMvcConfigurer接口的,因为老版本JDK接口没有默认方法,直接实现WebMvcConfigurer比较繁琐,而后来接口可以有默认方法了,WebMvcConfigurerAdapter就被标记为过时了,所以我们现在配置MVC只需要实现WebMvcConfigurer接口或者继承WebMvcConfigurationSupport,但是后者会导致SpringBoot的配置失效,因为在自动配置类上有@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)这样一个注解,表示没有WebMvcConfigurationSupport类及其子类的实例时才会加载自动配置(另外使用@EnableWebMvc注解也会导致自动配置失效)。

MVC自动配置失效的原因就是这个了,基本上所有网上的文章分析到这一步也就完了,但是注意上图我画的红方框,在这个自动配置类中有两个静态内部类,我们知道静态内部类是优于外部类加载的(SpringBoot自动配置大量使用了此特性),而其中EnableWebMvcConfiguration这个类,我注意到它是继承自DelegatingWebMvcConfiguration,而DelegatingWebMvcConfiguration又继承自WebMvcConfigurationSupport类,相信看到这你也应该会有疑惑了,为什么这个配置类没有导致自动配置失效,而我们自己实现的就会?

分析过程

我知道配置类的解析注册是在ConfigurationClassPostProcessor类中,而这个类我前面的文章多次分析过,虽然这个类的实现流程不难,但细节非常绕,所以之前没有深挖。遇到这个问题时,我首先想的是对这个类的理解不够深刻,因此第一时间想到仔细研究这个类,在花费了大量时间断点分析后,却没有太大的收获。

接着我又想,是不是配置类的注册顺序在自动配置的后面。这里我就犯了一个显而易见的错误,因为我考虑的是注册的顺序,不是实例化。因为ConditionalOnMissingBean注解是没有指定bean的实例时才会去加载,而我脑海里当时想成了ConditionalOnMissingClass。所以我在DefaultListableBeanFactory中的registerBeanDefinitionpreInstantiateSingletons方法上打上了断点,力图确认注册顺序如我所想:

  1. public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
  2. throws BeanDefinitionStoreException {
  3. BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
  4. if (existingDefinition != null) {
  5. ....
  6. this.beanDefinitionMap.put(beanName, beanDefinition);
  7. }
  8. else {
  9. if (hasBeanCreationStarted()) {
  10. // Cannot modify startup-time collection elements anymore (for stable iteration)
  11. synchronized (this.beanDefinitionMap) {
  12. this.beanDefinitionMap.put(beanName, beanDefinition);
  13. List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);
  14. updatedDefinitions.addAll(this.beanDefinitionNames);
  15. updatedDefinitions.add(beanName);
  16. this.beanDefinitionNames = updatedDefinitions;
  17. removeManualSingletonName(beanName);
  18. }
  19. }
  20. else {
  21. // Still in startup registration phase
  22. this.beanDefinitionMap.put(beanName, beanDefinition);
  23. this.beanDefinitionNames.add(beanName);
  24. removeManualSingletonName(beanName);
  25. }
  26. this.frozenBeanDefinitionNames = null;
  27. }
  28. }
  29. public void preInstantiateSingletons() throws BeansException {
  30. List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
  31. ....
  32. }



但结果beanDefinitionNames中的顺序却是两个静态内部类在前,也就是说静态内部类肯定是在外部类之前就注册到IOC容器中了,这下我就傻了。但幸好也是因此,否则我就该认为这就是结果了。最终我想到了应该看类的实例化顺序,但是正常情况下类的实例化顺序就是上面的断点图中的顺序,我想会不会是有什么类依赖了WebMvcAutoConfiguration,导致它提前实例化。于是我将断点又设置到AbstractBeanFactory中的doGetBean方法并加上了条件(不得不说idea的功能非常强大,回到上一个调用点、给断点设置条件、调用堆栈信息大大节省了我的调试时间):



然后启动项目就可以看到首先实例化的果然是WebMvcAutoConfiguration类,这样就搞清楚了为什么EnableWebMvcConfiguration没有导致自动配置失效。

但是还没完,为什么自动配置类会在静态内部类之前实例化呢?是由谁触发的呢?继续深入,这时我想到了看调用栈:



粗略看一下调用栈信息,如果对Spring源码熟悉,可以发现自动配置类的实例化是在instantiateUsingFactoryMethod中触发的:

  1. String factoryBeanName = mbd.getFactoryBeanName();
  2. if (factoryBeanName != null) {
  3. if (factoryBeanName.equals(beanName)) {
  4. throw new BeanDefinitionStoreException(mbd.getResourceDescription(), beanName,
  5. "factory-bean reference points back to the same bean definition");
  6. }
  7. factoryBean = this.beanFactory.getBean(factoryBeanName);
  8. if (mbd.isSingleton() && this.beanFactory.containsSingleton(beanName)) {
  9. throw new ImplicitlyAppearedSingletonException();
  10. }
  11. factoryClass = factoryBean.getClass();
  12. isStatic = false;
  13. }
  14. else {
  15. // It's a static factory method on the bean class.
  16. if (!mbd.hasBeanClass()) {
  17. throw new BeanDefinitionStoreException(mbd.getResourceDescription(), beanName,
  18. "bean definition declares neither a bean class nor a factory-bean reference");
  19. }
  20. factoryBean = null;
  21. factoryClass = mbd.getBeanClass();
  22. isStatic = true;
  23. }

这段代码在bean实例化的那一篇分析过,这个方法的作用是通过factoryMethod实例化当前的BeanDefinition,而实例化该BD优先会实例化factoryBeanName属性指向的Bean,这里的factoryBeanName就是org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfigurationfactoryMethod则是formContentFilter,而这两个属性的设置则是在ConfigurationClassPostProcessor解析@Configuration和@Bean就设置好了(@Bean标注的方法名会设置到factoryMethod,而该方法所在配置类的名称就是factoryBeanName),这里就不展开分析了。

  1. @Configuration(proxyBeanMethods = false)
  2. @ConditionalOnWebApplication(type = Type.SERVLET)
  3. @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
  4. @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
  5. @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
  6. @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
  7. ValidationAutoConfiguration.class })
  8. public class WebMvcAutoConfiguration {
  9. public static final String DEFAULT_PREFIX = "";
  10. public static final String DEFAULT_SUFFIX = "";
  11. private static final String[] SERVLET_LOCATIONS = { "/" };
  12. @Bean
  13. @ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
  14. @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
  15. public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
  16. return new OrderedHiddenHttpMethodFilter();
  17. }
  18. @Bean
  19. @ConditionalOnMissingBean(FormContentFilter.class)
  20. @ConditionalOnProperty(prefix = "spring.mvc.formcontent.filter", name = "enabled", matchIfMissing = true)
  21. public OrderedFormContentFilter formContentFilter() {
  22. return new OrderedFormContentFilter();
  23. }
  24. ......
  25. }

formContentFilter就是在MVC自动配置类中配置的,默认是加载的,而filter就不用多说了,在Tomcat启动后就会触发初始化,追踪调用栈也可以看到。另外我们还看到自动配置类中还配置了一个HiddenHttpMethodFilter,不过这个默认是不加载的,所以我们只要在application.properties中配置了如下属性,自动配置类就不会实例化了,但是两个静态内部类的实例化还是不会受影响的。

  1. spring.mvc.formcontent.filter.enabled=false

总结

该问题只是出于兴趣研究,虽然耗费了大量的时间和精力,但收获不少,加深了对Spring源码的理解,也修正了之前的一些错误理解,另外对于源码更多的是要自己去研究,不能只看一两篇文章或听别人说,只有自己亲手调试过才能知道自己的理解是否正确。

全网最深分析SpringBoot MVC自动配置失效的原因的更多相关文章

  1. Springboot MVC 自动配置

    Springboot MVC 自动配置 官方文档阅读 https://docs.spring.io/spring-boot/docs/current/reference/html/web.html#w ...

  2. springboot mvc自动配置(三)初始化mvc的组件

    所有文章 https://www.cnblogs.com/lay2017/p/11775787.html 正文 在springboot mvc自动配置的时候,获得了DispatcherServlet和 ...

  3. springboot mvc自动配置(目录)

    对于长时间基于spring框架做web开发的我们,springmvc几乎成为了开发普通web项目的标配.本系列文章基于快速启动的springboot,将从源码角度一点点了解springboot中mvc ...

  4. springboot mvc自动配置(一)自动配置DispatcherServlet和DispatcherServletRegistry

    所有文章 https://www.cnblogs.com/lay2017/p/11775787.html 正文 springboot的自动配置基于SPI机制,实现自动配置的核心要点就是添加一个自动配置 ...

  5. springboot mvc自动配置(二)注册DispatcherServlet到ServletContext

    所有文章 https://www.cnblogs.com/lay2017/p/11775787.html 正文 上一篇文章中,我们看到了DispatcherServlet和DispatcherServ ...

  6. 小BUG大原理:重写WebMvcConfigurationSupport后SpringBoot自动配置失效

    一.背景 公司的项目前段时间发版上线后,测试反馈用户的批量删除功能报错.正常情况下看起来应该是个小 BUG,可怪就怪在上个版本正常,且此次发版未涉及用户功能的改动.因为这个看似小 BUG 我了解到不少 ...

  7. 面试题: SpringBoot 的自动配置原理

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 3.Spring Boot 的自动配置原理 package com.mmall; import org. ...

  8. 小BUG大原理 | 第一篇:重写WebMvcConfigurationSupport后SpringBoot自动配置失效

    一.背景 公司的项目前段时间发版上线后,测试反馈用户的批量删除功能报错.正常情况下看起来应该是个小BUG,可怪就怪在上个版本正常,且此次发版未涉及用户功能的改动.因为这个看似小BUG我了解到不少未知的 ...

  9. spring-boot spring-MVC自动配置

    Spring MVC auto-configuration Spring Boot 自动配置好了SpringMVC 以下是SpringBoot对SpringMVC的默认配置:==(WebMvcAuto ...

随机推荐

  1. 分布式事务专题笔记(一) 基础概念 与 CAP 理论

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 一.基础概念 1.什么是事务 什么是事务?举个生活中的例子:你去小卖铺买东西,“一手交钱,一手交货”就是 ...

  2. Ftrace的部分使用方法

    ftrace主要是用于调试linux kernel调度相关的一个工具,也可用于分析部分kernel性能问题. 相关ftrace的介绍可以参考:kernel/msm-4.9/Documentation/ ...

  3. TI CC1310 sub1G的SDK开发之入门

    CC1310是TI新出的一款sub1G射频模块,具体参数见数据手册吧,这款芯片的SDK跑的是rtos系统,是基于free-rtos定制的ti-rtos,多任务运行.芯片集成了两个核,一个M3做控制MU ...

  4. Java实现 蓝桥杯VIP 算法训练 乘法表

    问题描述 输出九九乘法表. 输出格式 输出格式见下面的样例.乘号用""表示. 样例输出 下面给出输出的前几行: 11=1 21=2 22=4 31=3 32=6 33=9 41=4 ...

  5. Java实现 蓝桥杯VIP 算法训练 装箱问题

    题目描述 有一个箱子容量为V(正整数0≤V≤20000),同时有n个物品(0<n≤30,每个物品有一个体积(正整数). 要求nn个物品中,任取若干个装入箱内,使箱子的剩余空间为最小. 输入输出格 ...

  6. Java实现 LeetCode 143 重排链表

    143. 重排链表 给定一个单链表 L:L0→L1→-→Ln-1→Ln , 将其重新排列后变为: L0→Ln→L1→Ln-1→L2→Ln-2→- 你不能只是单纯的改变节点内部的值,而是需要实际的进行节 ...

  7. java实现海盗比酒量

    ** 海盗比酒量** 有一群海盗(不多于20人),在船上比拼酒量.过程如下:打开一瓶酒,所有在场的人平分喝下,有几个人倒下了.再打开一瓶酒平分,又有倒下的,再次重复- 直到开了第4瓶酒,坐着的已经所剩 ...

  8. Burpsuite intruder模块 越过token进行爆破,包含靶场搭建

    安装靶场 链接:https://pan.baidu.com/s/19X0oC63oO2cQKK6UL5xgOw 提取码:yq7f 下载完成放入网站根目录 点击初始化安装 出现错误,进行跟踪 发现是数据 ...

  9. zabbix 监控 tcp 连接数

    一.zabbix-agent 服务器配置 1.编辑zabbix_agent配置文件,添加以下内容 vim /etc/zabbix/zabbix_agentd.conf ##添加此行 UserParam ...

  10. 01.Markdown学习

    Markdown学习 一.标题 在想要设置为标题的文字前面加#来表示(#后面有空格) 一个#是一级标题,二个#是二级标题,以此类推.支持六级标题. 示例: # 这是一级标题 ## 这是二级标题 ### ...