全网最深分析SpringBoot MVC自动配置失效的原因

网友投稿 655 2023-05-20

全网最深分析SpringBoot MVC自动配置失效的原因

全网最深分析SpringBoot MVC自动配置失效的原因

前言

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

正文

引出问题

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

实现WebMvEvNSAacConfigurer接口

继承WebMvcConfigurerAdapter类

继承WebMvcConfigurationSupport类

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

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

分析过程

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

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

public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)

throws BeanDefinitionStoreException {

BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);

if (existingDefinition != null) {

....

this.beanDefinitionMap.put(beanName, beanDefinition);

}

else {

if (hasBeanCreationStarted()) {

// Cannot modify startup-time collection elements anymore (for stable iteration)

synchronized (this.beanDefinitionMap) {

this.beanDefinitiEvNSAaonMap.put(beanName, beanDefinition);

List updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);

updatedDefinitions.addAll(this.beanDefinitionNames);

updatedDefinitions.add(beanName);

this.beanDefinitionNames = updatedDefinitions;

removeManualSingletonName(beanName);

}

}

else {

// Still in startup registration phase

this.beanDefinitionMap.put(beanName, beanDefinition);

this.beanDefinitionNames.add(beanName);

removeManualSingletonName(beanName);

}

this.frozenBeanDefinitionNames = null;

}

}

public void preInstantiateSingletons() throws BeansException {

List beanNames = new ArrayList<>(this.beanDefinitionNames);

....

}

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

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

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

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

String factoryBeanName = mbd.getFactoryBeanName();

if (factoryBeanName != null) {

if (factoryBeanName.equals(beanName)) {

throw new BeanDefinitionStoreException(mbd.getResourceDescription(), beanName,

"factory-bean reference points back to the same bean definition");

}

factoryBean = this.beanFactory.getBean(factoryBeanName);

if (mbd.isSingleton() && this.beanFactory.containsSingleton(beanName)) {

throw new ImplicitlyAppearedSingletonException();

}

factoryClass = factoryBean.getClass();

isStatic = false;

}

else {

// It's a static factory method on the bean class.

if (!mbd.hasBeanClass()) {

throw new BeanDefinitionStoreException(mbd.getResourceDescription(), beanName,

"bean definition declares neither a bean class nor a factory-bean reference");

}

factoryBean = null;

factoryClass = mbd.getBeanClass();

isStatic = true;

}

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

@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 {

public static final String DEFAULT_PREFIX = "";

public static final String DEFAULT_SUFFIX = "";

private static final String[] SERVLET_LOCATIONS = { "/" };

@Bean

@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)

@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)

public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {

return new OrderedHiddenHttpMethodFilter();

}

@Bean

@ConditionalOnMissingBean(FormContentFilter.class)

@ConditionalOnProperty(prefix = "spring.mvc.formcontent.filter", name = "enabled", matchIfMissing = true)

public OrderedFormContentFilter formContentFilter() {

return new OrderedFormContentFilter();

}

......

}

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

spring.mvc.formcontent.filter.enabled=false

总结

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

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:SSM框架把日志信息保存到数据库过程详解
下一篇:如何基于ssm框架实现springmvc拦截器
相关文章

 发表评论

暂时没有评论,来抢沙发吧~