Spring Boot中只能有一个WebMvcConfigurationSupport配置类

首先将结论写文章的最前面,一个项目中只能有一个继承 WebMvcConfigurationSupport 的 @Configuration 类(使用 @EnableMvc 效果相同),如果存在多个这样的类,只有一个配置可以生效。推荐使用
implements WebMvcConfigurer 的方法自定义 mvc 配置。

背景

项目中的一个模块需要实现上传图片后通过 url 访问保存在本地上的图片的功能,在SpringBoot 系列教程 (十八):SpringBoot 通过 url 访问获取内部或者外部磁盘图片中详细介绍了各种方法,最后我采用了方式三中介绍的直接继承 WebMvcConfigurationSupport 来实现这一功能。

场景复现

首先按照文章介绍的方法实现配置类

@Configuration
public class ImageConfig extends WebMvcConfigurationSupport {
<span class="hljs-meta">@Override</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">addResourceHandlers</span><span class="hljs-params">(ResourceHandlerRegistry registry)</span> {
    registry.addResourceHandler(<span class="hljs-string">"/images/**"</span>)
            .addResourceLocations(<span class="hljs-string">"file:./localdata/images/"</span>);
}

}

重新启动项目以后尝试访问图片 url,但是返回了 404 错误

经过一番排查,我发现重载的这段方法在 Spring Boot 启动过程中实际并没有执行,但之前添加的一个跨域的 mvc 配置却是正确加载了。
这是跨域配置类的实现

@Configuration
public class CorsConfig extends WebMvcConfigurationSupport {
<span class="hljs-meta">@Override</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">addCorsMappings</span><span class="hljs-params">(CorsRegistry registry)</span> {
    registry.addMapping(<span class="hljs-string">"/**"</span>)
            .allowedOrigins(<span class="hljs-string">"*"</span>)
            .allowCredentials(<span class="hljs-literal">true</span>)
            .allowedMethods(<span class="hljs-string">"GET"</span>, <span class="hljs-string">"POST"</span>, <span class="hljs-string">"DELETE"</span>, <span class="hljs-string">"PUT"</span>)
            .maxAge(<span class="hljs-number">3600</span>);
}

}

两个类都继承了 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 修饰的方法。
整个流程如下:

  1. 扫描到 CorsConfig 类,生成 Bean 对象时调用父类中的被 Bean 修饰的方法
  2. 其中某些方法调用了被子类重写的addCorsMappings(CorsRegistry registry)方法,完成了自定义配置
  3. 负责管理映射资源的resourceHandlerMapping方法在此时也被调用了,但是在 CorsConfig 类中没有对其调用的addResourceHandlers重写,实际上调用了一个空实现。
  4. 扫描到 ImageConfig 类,生成 Bean 对象时发现其中从父类继承的 Bean 方法已经生成实例了,于是不再调用resourceHandlerMapping,因此重写的addResourceHandlers方法也就不在有机会运行。

总结

Spring Boot 中只能有一个 WebMvcConfigurationSupport 配置类是真正起作用的,对于这个问题,其实可以通过implements WebMvcConfigurer来解决,多个不同的类实现这个接口后的配置都可以正常运行。
事实上,对于映射资源,Spring Boot 的官方文档给出的例子也是通过实现接口完成的。从这次的经历可以看出在写代码的过程中多阅读官方文档可以少走很多弯路,比各类博客文章的教程质量也要更高。另外,在写代码时遇到了问题,除了解决问题本身,了解产生问题的原因也是非常重要的,在这个过程中可以对使用的框架的运行流程更加熟悉。