首先將結論寫文章的最前面,一個項目中只能有一個繼承WebMvcConfigurationSupport的@Configuration類(使用@EnableMvc效果相同),如果存在多個這樣的類,只有一個配置可以生效。推薦使用
implements WebMvcConfigurer 的方法自定義mvc配置。
背景
項目中的一個模塊需要實現上傳圖片后通過url訪問保存在本地上的圖片的功能,在SpringBoot 系列教程(十八):SpringBoot通過url訪問獲取內部或者外部磁盤圖片中詳細介紹了各種方法,最后我采用了方式三中介紹的直接繼承WebMvcConfigurationSupport來實現這一功能。
場景復現
首先按照文章介紹的方法實現配置類
@Configuration
public class ImageConfig extends WebMvcConfigurationSupport {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/images/**")
.addResourceLocations("file:./localdata/images/");
}
}
重新啟動項目以后嘗試訪問圖片url,但是返回了404錯誤

經過一番排查,我發現重載的這段方法在Spring Boot啟動過程中實際並沒有執行,但之前添加的一個跨域的mvc配置卻是正確加載了。
這是跨域配置類的實現
@Configuration
public class CorsConfig extends WebMvcConfigurationSupport {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600);
}
}
兩個類都繼承了 WebMvcConfigurationSupport 並重寫了需要自定義配置的方法,但一個生效了另一個卻沒有。於是我猜測只能有一個繼承 WebMvcConfigurationSupport 的配置類,為了驗證我的猜測,我將跨域配置的@Configuration注解刪去,只保留ImageConfig的配置,果然可以正常訪問圖片了!到這里基本可以確定,在Spring Boot的啟動過程中,被@Configuration注解的所有類中只有一個WebMvcConfigurationSupport子類的自定義配置可以被正確加載。很容易可以想到,將兩段方法寫在同一個類中就可以解決這樣的問題。那么為什么會出現這樣的情況呢?我用類似的關鍵字搜索,發現同樣有人遇到了類似的問題:WebMvcConfigurationSupport沒有生效的問題。但是卻沒有一篇文章講清楚了原因,於是我決定探索一下其中的奧秘。
原因探索
顯然,要找到@Configuration類不能正確加載,就要從Spring Boot如何加載mvc配置入手,但是這方面我不是很了解,於是我在代碼中拋出一個throw new NullPointerException();來看以下調用堆棧
org.springframework.beans.factory.BeanCreationException: Error creating bean with
name 'resourceHandlerMapping' defined in class path resource
[test/config/ImageConfig.class]: Bean instantiation via factory method failed; nested
exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate
[org.springframework.web.servlet.HandlerMapping]: Factory method
'resourceHandlerMapping' threw exception; nested exception is
java.lang.NullPointerException
可以看到,是在創建'resourceHandlerMapping'這個bean對象的時候拋出了異常,那么自定義配置的代碼也一定是在這個時候被調用的。這個Bean對象正是在被繼承的WebMvcConfigurationSupport類中定義的。於是我又打開了相關的源碼
@Bean
@Nullable
public HandlerMapping resourceHandlerMapping(...) {
...
this.addResourceHandlers(registry);
...
}
}
忽略掉無關的代碼,可以看到這個被Bean修飾的方法調用了addResourceHandlers(registry)方法,而這個方法正是繼承了這個類后重寫的方法,我們用自己自定義的配置重寫這個方法就可以改變配置的行為。這樣的設計其實就是設計模式中的模板方法模式,在父類中定義方法的框架然后通過鈎子函數改變一些特定的步驟。
再回到原本的問題上來,其實當看到這個方法被@Bean修飾之后其實我已經心中又有數了:在Spring Boot中,一個被@Bean修飾的方法在啟動過程中會被調用生成Bean對象存放在IoC容器中(前提是這個類本身已經被Spring Boot管理生命周期),也就是說通過繼承WebMvcConfigurationSupport自定義的配置方法是在生成父類中定義的@Bean方法時被調用的,而兩個配置類中的Bean對象的id是一樣的(來自同一個父類相同的方法),也就是說在生成第二個配置類的對象的時候不會再調用其中被@Bean修飾的方法。
整個流程如下:
- 掃描到CorsConfig類,生成Bean對象時調用父類中的被Bean修飾的方法
- 其中某些方法調用了被子類重寫的
addCorsMappings(CorsRegistry registry)方法,完成了自定義配置 - 負責管理映射資源的
resourceHandlerMapping方法在此時也被調用了,但是在CorsConfig類中沒有對其調用的addResourceHandlers重寫,實際上調用了一個空實現。 - 掃描到ImageConfig類,生成Bean對象時發現其中從父類繼承的Bean方法已經生成實例了,於是不再調用
resourceHandlerMapping,因此重寫的addResourceHandlers方法也就不在有機會運行。
總結
Spring Boot中只能有一個WebMvcConfigurationSupport配置類是真正起作用的,對於這個問題,其實可以通過implements WebMvcConfigurer來解決,多個不同的類實現這個接口后的配置都可以正常運行。
事實上,對於映射資源,Spring Boot的官方文檔給出的例子也是通過實現接口完成的。從這次的經歷可以看出在寫代碼的過程中多閱讀官方文檔可以少走很多彎路,比各類博客文章的教程質量也要更高。另外,在寫代碼時遇到了問題,除了解決問題本身,了解產生問題的原因也是非常重要的,在這個過程中可以對使用的框架的運行流程更加熟悉。
