前言
本來沒有計劃這一篇文章的,只是在看完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中的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.beanDefinitionMap.put(beanName, beanDefinition);
List<String> 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<String> 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原始碼的理解,也修正了之前的一些錯誤理解,另外對於原始碼更多的是要自己去研究,不能只看一兩篇文章或聽別人說,只有自己親手除錯過才能知道自己的理解是否正確。