Spring MVC之注解@EnableWebMvc

2,474 阅读7分钟

前言

其实我接触Java web开发比较晚,这句话的意思就是,我做开发的时候就使用的是比较新的技术了,比如spring boot,从来没用过ssh那一套,虽然用了spring mvc,但也是基于spring boot封装好的。

当然了,这有好处,也有坏处,好处是跟上了时代的潮流,坏处是对于被封装的那一套了解不够深刻。

今天在翻某些框架封装的源码时候,看到一些WEB项目的配置类继承了WebMvcConfigurerAdapter类,然后自定义了一些配置。另外在这个类还有这个注解:@EnableWebMvc。

然后产生了两个疑问:

1.想要自定义spring mvc的配置为什么继承WebMvcConfigurerAdapter类

2. @EnableWebMvc注解起什么用

带着这两个疑问,准备一探究竟

为什么要继承WebMvcConfigurerAdapter

首先说明下,spring mvc有几个核心组件,这些组件的配置都是可扩展的。
这几个核心组件不是本文的重点,也不做强调,需要了解的可以查阅相关资料。

我以前看过《看透Spring MVC源代码分析与实践》作者:韩路彪,这本书中讲解了spring mvc的九大组件,感兴趣的可以看下。

那么问题来了,如果想自定义这些配置,为什么要继承WebMvcConfigurerAdapter类。查看了其中一些类的注释,并不是一定要继承这个类,但是如果想自定义一些高级配置,建议继承它,什么算高级?嘿嘿。

先介绍下这个类,这个类实现了WebMvcConfigurer接口的所有方法(都是空实现),这里提一下WebMvcConfigurer接口,类的注释上是这样说明的:

定义回调方法,以通过{@code @EnableWebMvc}自定义启用Spring MVC的基于Java的配置。
{@code @EnableWebMvc}已注释的配置类可以实现此接口的回调,并有机会自定义默认配置。 考虑扩展{@link WebMvcConfigurerAdapter},它提供所有接口方法的存根实现。

这几句注释在我看来有下面几个意思:

  1. 使用@EnableWebMvc注解启用spring mvc的基于java config的配置 

  2. 实现WebMvcConfigurer接口的方法可以自定义spring mvc的配置

  3. 对于第2个意思,建议采用继承WebMvcConfigurerAdapter类来实现

  4. 如果想要让继承WebMvcConfigurerAdapter的自定义配置的子类起作用,那这个类应该是配置类(比如加上注解@Configuration,毕竟这个类应该托管到spring 容器内,spring mvc才会知道这个子类,要不这些自定义配置怎么起作用)

也就是说,想要启用spring mvc的时候,应用使用注解@EnableWebMvc启用spring mvc的配置,另外,如果想自定义这些配置,就使用一个可以托管到spring容器的配置类,继承WebMvcConfigurerAdapter类并重写需要自定义配置的那些方法。

到这里, 我又产生了几个疑问:

Q1. 我继承了WebMvcConfigurerAdapter类并重写需要自定义配置的那些方法,但是spring mvc是怎么知道的

Q2. spring mvc怎么知道我要自定义哪些配置,我自定义的配置会不会导致默认配置不可用

Q3. 这个自定义配置的子类是怎么和spring mvc关联的

这些问题的答案,应该就在@EnableWebMvc注解这里。所以,看下文

@EnableWebMvc注解起什么用

@EnableWebMvc注解起什么用?先看下源码中第一行的注解说明:

将此注解添加到{@code @Configuration}类可从{@link WebMvcConfigurationSupport}导入Spring MVC配置

也就是说,这个注解应当加到有@Configuration注解的类上(意思是这个类应当是托管到spring容器的配置类),然后就可以从从{@link WebMvcConfigurationSupport}导入Spring MVC配置,问题在这个WebMvcConfigurationSupport类上。

再看下@EnableWebMvc注解类的源码:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}

重点在于:@Import(DelegatingWebMvcConfiguration.class)这里,这是基于java config格式的配置类的导入,然后看下DelegatingWebMvcConfiguration的源码:

@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport 

那就明白了:DelegatingWebMvcConfiguration是继承了WebMvcConfigurationSupport的配置类。

现在先来看下WebMvcConfigurationSupport是什么,注释是这样解释的:

这是提供MVC Java配置背后的配置的主类。 通常通过将{@link EnableWebMvc @EnableWebMvc}添加到应用程序{@link Configuration @Configuration}类来导入它。

另一种选择高级选项是直接从此类扩展并根据需要覆盖方法,记住将{@link Configuration @Configuration}添加到子类和{@link Bean @Bean}以覆盖{@link Bean @Bean}方法。 有关更多详细信息,请参阅{@link EnableWebMvc @EnableWebMvc}的Javadoc。

这个WebMvcConfigurationSupport类的作用呢,其实提供了上文提到的spring mvc的几个核心组件的能力。如果想要从此类扩展,只需要继承并重写它的一些方法(有兴趣的可以看下这个类的源码)。

现在就是,DelegatingWebMvcConfiguration类继承了WebMvcConfigurationSupport类并重写了它的一些方法,并且DelegatingWebMvcConfiguration类是一个配置类被托管了spring容器,重点来了:

这里说明了@EnableWebMvc注解的一个作用:

1. 启用spring mvc的这几个核心组件提供的能力(就是启用了spring mvc)

如果还不明白,这里解释下:上文说了,@EnableWebMvc注解需要用在一个可以注册到spring容器的配置类上,然后@EnableWebMvc注解导入了DelegatingWebMvcConfiguration配置类,这个类继承了WebMvcConfigurationSupport类提供的spring mvc各个组件的能力并且这个类也被注册到了spring容器。

那么,@EnableWebMvc注解和自定义配置的关系在哪,我认为这算是@EnableWebMvc注解提供的第二个作用:

2. 支持自定义spring mvc配置的能力

而它的这个能力,关键在于DelegatingWebMvcConfiguration配置类,上文说了DelegatingWebMvcConfiguration类继承自WebMvcConfigurationSupport类并重写了它的一些方法,就是它重写的这些方法,允许了我们增加自定义配置。

看一下DelegatingWebMvcConfiguration类的部分源码,作个解释:

@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {

	private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();


    // 注意看这里,这里把spring容器的所有实现了WebMvcConfigurer接口的类的bean作为一个集合
    // 变量注入到了这里。
    // 这就意味着,我们需要自定义spring mvc配置的那些配置类,都会被注入到这里
    // 这样就可以把所有配置(包括我们自定义的配置添加进去)
	@Autowired(required = false)
	public void setConfigurers(List<WebMvcConfigurer> configurers) {
		if (!CollectionUtils.isEmpty(configurers)) {
			this.configurers.addWebMvcConfigurers(configurers);
		}
	}


	@Override
	protected void configurePathMatch(PathMatchConfigurer configurer) {
		this.configurers.configurePathMatch(configurer);
	}

	@Override
	protected void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
		this.configurers.configureContentNegotiation(configurer);
	}

	@Override
	protected void configureAsyncSupport(AsyncSupportConfigurer configurer) {
		this.configurers.configureAsyncSupport(configurer);
	}

	@Override
	protected void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
		this.configurers.configureDefaultServletHandling(configurer);
	}
 //...
}

不用太多,上面几行就明白了。

关键在于DelegatingWebMvcConfiguration类的这个属性上:

private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();

// 看下WebMvcConfigurerComposite类的部分源码:
//可以看到这个类有个属性delegates,是个WebMvcConfigurer接口的实现类的集合
class WebMvcConfigurerComposite implements WebMvcConfigurer {

	private final List<WebMvcConfigurer> delegates = new ArrayList<WebMvcConfigurer>();


	public void addWebMvcConfigurers(List<WebMvcConfigurer> configurers) {
		if (!CollectionUtils.isEmpty(configurers)) {
			this.delegates.addAll(configurers);
		}
	}


	@Override
	public void configurePathMatch(PathMatchConfigurer configurer) {
		for (WebMvcConfigurer delegate : this.delegates) {
			delegate.configurePathMatch(configurer);
		}
	}

	@Override
	public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
		for (WebMvcConfigurer delegate : this.delegates) {
			delegate.configureContentNegotiation(configurer);
		}
	}
//...
}

注意看下,我上面贴的两段代码中加的一些中文注释。结合起来一看,这样就明白了,DelegatingWebMvcConfiguration类会把所有实现了接口WebMvcConfigurer的类(子类也是,这是java语法,就不说了)包括我们那些托管到spring容器的自定义的配置类(因为也实现了它)都会把这些配置加上。

这也就是解释了第1节中提到的Q1、Q3的问题:我的配置类注册到了spring容器中,spring通过自动注入的方式把所有WebMvcConfigurer接口的实现类注入到了DelegatingWebMvcConfiguration的configurers属性中,在WebMvcConfigurerComposite类把这些配置都给配置上。然后回调那些实现了WebMvcConfigurer接口的实现类,最终将我们自定义的配置都给加上。

现在,就剩Q2这个问题了,spring mvc怎么知道我自定义哪些配置了,在WebMvcConfigurerComposite类回调我们重写方法的接口时,如果我们重写了需要自定义配置的方法,自然就加上了,现在的问题是第二个,如果自定义了配置,是否会加载默认配置?这个就看自定义谁的配置了,比如HttpMessageConverter,如果在重写了方法configMessageConverters自定义了配置,就不会加载默认配置,如果重写的方法是extendMessageContertes就会加载自定义的和默认的,看下源码就明白了:

	/**
	 * Provides access to the shared {@link HttpMessageConverter}s used by the
	 * {@link RequestMappingHandlerAdapter} and the
	 * {@link ExceptionHandlerExceptionResolver}.
	 * This method cannot be overridden.
	 * Use {@link #configureMessageConverters(List)} instead.
	 * Also see {@link #addDefaultHttpMessageConverters(List)} that can be
	 * used to add default message converters.
	 */
	protected final List<HttpMessageConverter<?>> getMessageConverters() {
		if (this.messageConverters == null) {
			this.messageConverters = new ArrayList<HttpMessageConverter<?>>();
			configureMessageConverters(this.messageConverters);
			if (this.messageConverters.isEmpty()) {
                // 这里如果非空的就不加载默认配置了,注释上也有解释
				addDefaultHttpMessageConverters(this.messageConverters);
			}
			extendMessageConverters(this.messageConverters);
		}
		return this.messageConverters;
	}

其它几个组件,有兴趣可以查阅相关资料,或者翻下源码了解下。