likes
comments
collection
share

Spring5源码16-@EnableWebMvc注解原理

作者站长头像
站长
· 阅读数 17

欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈

请提前阅读Spring5源码14-SpringMVC-DispatcherServlet初始化,SpringMVC相关源码内容。

1. 需求及用途

我们以前自定义的九大组件给容器中一放,DispatcherServlet初始化看容器中有我们的,就不用自己的默认组件。导致我们很多默认功能失去了。

我们的期望:

  • 自己的组件能生效
  • SpringMVC默认的还能生效 可以采用 @EnableWebMvc + WebMvcConfigure。@EnableWebMVC会给容器中导入九大组件,还都留了入口【我们用WebMvcConfigurer就可以定制】,而不是用配置文件默认。

参考spring官网,我们发现@EnableWebMvc注解和<mvc:annotation-driven>具有相同的功能,都是用来启用springmvc的默认配置。当扫描到这个注解之后,就会向容器中注入一些默认组件。

那么这个注解是如何实现springmvc默认配置的呢?往下看

2. @EnableWebMvc

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

我们发现,实际上就是通过@Import注解向容器中注册了一个组件DelegatingWebMvcConfiguration,所以现在核心是DelegatingWebMvcConfiguration类干了什么?

3. DelegatingWebMvcConfiguration

先来看一下类图: Spring5源码16-@EnableWebMvc注解原理

两个Aware接口向该组件中自动注入一个ApplicationContext对象和ServletContext对象。现在我们要带着两个问题去看一下该类的源码?

  • 为什么这个组件能够使用户自定义的配置类生效呢?
  • 这个组件定义了哪些默认配置?

3.1 DelegatingWebMvcConfiguration源码

DelegatingWebMvcConfiguration类非常简单,只有一个字段configurers,持有所有的配置类对象。另外还需要注意一个方法setConfigurers(List<WebMvcConfigurer> configurers)方法, 该方法标注了@Autowired(required = false)注解,会被自动执行。

@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {

    //组合模式,所有配置类组合
    private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();

    /**
     * 此处添加了@Autowired(required = false)注解
     * 那么该方法在属性填充阶段就会被执行,其中方法参数来自于容器
     * 会自动从容器中找到类型匹配的bean,然后反射执行方法
     */
    @Autowired(required = false)
    public void setConfigurers(List<WebMvcConfigurer> configurers) {
        if (!CollectionUtils.isEmpty(configurers)) {
            //将容器中WebMvcConfigurer对象全部放入WebMvcConfigurerComposite中
            this.configurers.addWebMvcConfigurers(configurers);
        }
    }


    /********************************************************************************/

    /**
     * 下面的这些方法都差不多
     * 向WebMvcConfigurerComposite的每一个配置类中添加配置具体的过程见3.1.1
     */
    @Override
    protected void configurePathMatch(PathMatchConfigurer configurer) {
        this.configurers.configurePathMatch(configurer);
    }

    ...
    
    /********************************************************************************/

    //这个方法会获取配置类定义的唯一的Validator
    @Override
    @Nullable
    protected Validator getValidator() {
        return this.configurers.getValidator();
    }

    //这个方法会获取配置类定义的唯一的MessageCodesResolver
    @Override
    @Nullable
    protected MessageCodesResolver getMessageCodesResolver() {
        return this.configurers.getMessageCodesResolver();
    }
}

可以看到,在DelegatingWebMvcConfiguration组件属性填充阶段,会自动的查找容器中所有类型为WebMvcConfigurer的bean对象,然后以它们为参数,反射调用setConfigurers(List<WebMvcConfigurer> configurers)方法,将所有的配置类对象放入WebMvcConfigurerComposite中。

除此之外,它还重写了父类的一些方法(模板方法模式),用来向配置类中注册配置和获取配置类中一些配置对象,比如ValidatorMessageCodesResolver

3.2 WebMvcConfigurerComposite

下面是该类的源码,方法都很简单,主要注意以下四点:

  • 该类持有所有的配置类对象
  • 添加一项配置的时候会将配置注册给所有的配置类对象
  • getValidator()方法只能被一个配置类重写,只允许一个配置类返回一个Validator对象
  • getMessageCodesResolver()方法只能被一个配置类重写,只允许一个配置类返回一个MessageCodesResolver对象
class WebMvcConfigurerComposite implements WebMvcConfigurer {

    //持有的配置类对象
    private final List<WebMvcConfigurer> delegates = new ArrayList<>();


    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);
        }
    }

    ...

    /**
     * 这个方法需要注意一下
     * Validator必须是唯一的,也就说,只允许一个配置重写该方法返回一个Validator
     * 有多个就会抛出异常
     */
    @Override
    public Validator getValidator() {
        Validator selected = null;
        for (WebMvcConfigurer configurer : this.delegates) {
            Validator validator = configurer.getValidator();
            if (validator != null) {
                if (selected != null) {
                    throw new IllegalStateException("No unique Validator found: {" +
                                                    selected + ", " + validator + "}");
                }
                selected = validator;
            }
        }
        return selected;
    }


    /**
     * 这个方法也需要注意一下
     * MessageCodesResolver必须是唯一的,也就说,
     * 只允许一个配置重写该方法返回一个MessageCodesResolver,有多个就会抛出异常
     */
    @Override
    @Nullable
    public MessageCodesResolver getMessageCodesResolver() {
        MessageCodesResolver selected = null;
        for (WebMvcConfigurer configurer : this.delegates) {
            MessageCodesResolver messageCodesResolver = configurer.getMessageCodesResolver();
            if (messageCodesResolver != null) {
                if (selected != null) {
                    throw new IllegalStateException("No unique MessageCodesResolver found: {" +
                                                    selected + ", " + messageCodesResolver + "}");
                }
                selected = messageCodesResolver;
            }
        }
        return selected;
    }

}

3.3 WebMvcConfigurationSupport

这是最重要的一个类,该类里面有很多@Bean方法,用来向容器中注册组件。

3.3.1 静态私有字段

private static final boolean romePresent;

private static final boolean jaxb2Present;

private static final boolean jackson2Present;

private static final boolean jackson2XmlPresent;

private static final boolean jackson2SmilePresent;

private static final boolean jackson2CborPresent;

private static final boolean gsonPresent;

private static final boolean jsonbPresent;

static {
    ClassLoader classLoader = WebMvcConfigurationSupport.class.getClassLoader();
    romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", classLoader);
    jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);
    jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
        ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
    jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
    jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
    jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
    gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
    jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
}

这些静态字段是一些标志量,通过ClassUtils.isPresent()方法判断是否导入了对应的包,如果导入了,就将对应的静态字段置为true,后面就可以根据这些标志量反射实例化对象了。

这也解释了为什么只要我们导入了jackson包,就可以自动向HandlerAdapter中注册MappingJackson2HttpMessageConverterMappingJackson2XmlHttpMessageConverter对象。

3.3.2 普通字段

//web应用上下文
@Nullable
private ApplicationContext applicationContext;

//servlet上下文
@Nullable
private ServletContext servletContext;

//拦截器
@Nullable
private List<Object> interceptors;

//路径匹配配置器
@Nullable
private PathMatchConfigurer pathMatchConfigurer;

//内容协商管理器
@Nullable
private ContentNegotiationManager contentNegotiationManager;

//参数解析器
@Nullable
private List<HandlerMethodArgumentResolver> argumentResolvers;

//返回值处理器
@Nullable
private List<HandlerMethodReturnValueHandler> returnValueHandlers;

//http消息转换器
@Nullable
private List<HttpMessageConverter<?>> messageConverters;

//跨域配置
@Nullable
private Map<String, CorsConfiguration> corsConfigurations;

后面的小章节都是该类定义的@Bean方法,我们看看它到底向容器中放入了哪些组件?

3.3.3 注册RequestMappingHandlerMapping

@Bean
@SuppressWarnings("deprecation")
public RequestMappingHandlerMapping requestMappingHandlerMapping(
      @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
      @Qualifier("mvcConversionService") FormattingConversionService conversionService,
      @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {

   //创建一个RequestMappingHandlerMapping对象,就是简单的new一个
   RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping();
   //设置顺序,第一位
   mapping.setOrder(0);
   /**
    * getInterceptors()方法可以获取到用户注册和系统默认的所有拦截器对象
    * 然后将这些拦截器全部放入处理器映射器中
    */
   mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
   //设置内容协商管理器
   mapping.setContentNegotiationManager(contentNegotiationManager);
   //获取用户注册的所有跨域配置
   mapping.setCorsConfigurations(getCorsConfigurations());

   //获取路径匹配配置器
   PathMatchConfigurer pathConfig = getPathMatchConfigurer();
   if (pathConfig.getPatternParser() != null) {
      mapping.setPatternParser(pathConfig.getPatternParser());
   }
   else {
      //获取路径匹配配置器中设置的UrlPathHelper,使用这个UrlPathHelper覆盖默认的UrlPathHelper
      mapping.setUrlPathHelper(pathConfig.getUrlPathHelperOrDefault());
      //获取在路径匹配配置器中配置的路径匹配器,覆盖默认的路径匹配器
      mapping.setPathMatcher(pathConfig.getPathMatcherOrDefault());

      //已经被废弃掉了不推荐使用,直接跳过
      Boolean useSuffixPatternMatch = pathConfig.isUseSuffixPatternMatch();
      if (useSuffixPatternMatch != null) {
         mapping.setUseSuffixPatternMatch(useSuffixPatternMatch);
      }
      //已经被废弃掉了不推荐使用,直接跳过
      Boolean useRegisteredSuffixPatternMatch = pathConfig.isUseRegisteredSuffixPatternMatch();
      if (useRegisteredSuffixPatternMatch != null) {
         mapping.setUseRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch);
      }
   }
   //是否使用尾斜杠匹配
   Boolean useTrailingSlashMatch = pathConfig.isUseTrailingSlashMatch();
   if (useTrailingSlashMatch != null) {
      mapping.setUseTrailingSlashMatch(useTrailingSlashMatch);
   }
   //获取配置所有路径前缀
   if (pathConfig.getPathPrefixes() != null) {
      mapping.setPathPrefixes(pathConfig.getPathPrefixes());
   }

   return mapping;
}

我们可以发现,这个方法为RequestMappingHandlerMapping做了一大堆配置,如下所示

  • 获取用户和系统配置的所有拦截器并注册进去
  • 获取容器中内容协商管理器并注册进去
  • 获取用户注册的所有跨域配置并注册进去
  • 获取路径匹配配置器,然后将路径匹配配置器的配置覆盖进去,主要覆盖一下4中配置
  • 尾斜杠匹配
  • UrlPathHelper
  • PathMatcher
  • pathPrefixes

3.3.3.1 创建一个RequestMappingHandlerMapping对象

/**
 * Protected method for plugging in a custom subclass of
 * {@link RequestMappingHandlerMapping}.
 * @since 4.0
 */
protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
    return new RequestMappingHandlerMapping();
}

就是简单的new一个RequestMappingHandlerMapping对象

3.3.3.2 获取用户注册和系统默认的所有拦截器对象

protected final Object[] getInterceptors(
    FormattingConversionService mvcConversionService,
    ResourceUrlProvider mvcResourceUrlProvider) {
    if (this.interceptors == null) {
        //拦截器注册中心
        InterceptorRegistry registry = new InterceptorRegistry();
        /**
         * 该方法被子类重写,会以这个拦截器注册中心为参数调用所有配置类对象中
         * addInterceptors(registry)方法,用户通过这个注册中心注册拦截器对象。
         * 很明显,此处调用addInterceptors(registry)方法,
         * 就会带着WebMvcConfigurerComposite中所有的配置类对象都调用一次
         * addInterceptors(registry)方法,将配置类中配置的拦截器注册到拦截器注册中心
         * 这个注册中心会将所有类型的拦截器统一适配为InterceptorRegistration类型,方便管理
         */
        addInterceptors(registry);
        //这个拦截器将FormattingConversionService保存到请求域中
        registry.addInterceptor(new ConversionServiceExposingInterceptor(mvcConversionService));
        //这个拦截器将ResourceUrlProvider保存到请求域中
        registry.addInterceptor(new ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider));
        //获取注册中心中所有的拦截器,会先排序,再返回
        this.interceptors = registry.getInterceptors();
    }
    return this.interceptors.toArray();
}

我们重点看一下这个拦截器注册中心是如何注册拦截器的

// org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration#addInterceptors
@Override
protected void addInterceptors(InterceptorRegistry registry) {
   this.configurers.addInterceptors(registry);
}

// org.springframework.web.servlet.config.annotation.WebMvcConfigurerComposite#addInterceptors
@Override
public void addInterceptors(InterceptorRegistry registry) {
   for (WebMvcConfigurer delegate : this.delegates) {
      delegate.addInterceptors(registry);
   }
}

这个方法是拦截器相关的核心方法了,它会执行每一个WebMvcConfigurer配置类的addInterceptors(registry)方法, 将用户自定义的拦截器注册到拦截器注册中心中,最后再放入两个系统默认的拦截器,然后统一排序,并返回

3.3.3.3 获取用户注册的所有跨域配置

protected final Map<String, CorsConfiguration> getCorsConfigurations() {
    if (this.corsConfigurations == null) {
        //跨域配置注册中心
        CorsRegistry registry = new CorsRegistry();
        /**
         * 和上面拦截器一样,会以这个跨域配置注册中心为参数调用所有配置类对象中
         * addCorsMappings(registry)方法,用户通过这个注册中心注册跨域配置。
         * 很明显,此处调用addCorsMappings(registry)方法,
         * 就会带着WebMvcConfigurerComposite中所有的配置类对象都调用一次
         * addCorsMappings(registry)方法,将配置类中配置的跨域配置注册到注册中心
         */
        addCorsMappings(registry);
        //得到注册中心的所有的跨域配置
        this.corsConfigurations = registry.getCorsConfigurations();
    }
    return this.corsConfigurations;
}

和上面拦截器一样,它会执行每一个WebMvcConfigurer配置类的addCorsMappings(registry)方法, 将用户自定义的跨域配置注册到跨域配置注册中心,最后返回所有的跨域配置

3.3.3.4 获取路径匹配配置器

protected PathMatchConfigurer getPathMatchConfigurer() {
    if (this.pathMatchConfigurer == null) {
        //新建一个路径匹配配置器
        this.pathMatchConfigurer = new PathMatchConfigurer();
        /**
         * 又是同样的方式,会以这个路径匹配配置器为参数调用所有配置类对象中
         * configurePathMatch(pathMatchConfigurer)方法,用户通过这个配置器注册路径匹配器。
         * 很明显,此处调用configurePathMatch(pathMatchConfigurer)方法,
         * 就会带着WebMvcConfigurerComposite中所有的配置类对象都调用一次
         * configurePathMatch(pathMatchConfigurer)方法,将配置类中配置的路径匹配器
         * 注册到配置器中
         */
        configurePathMatch(this.pathMatchConfigurer);
    }
    return this.pathMatchConfigurer;
}

同样的方式,它会执行每一个WebMvcConfigurer配置类的configurePathMatch(pathMatchConfigurer)方法, 将用户自定义的路径匹配器注册到路径匹配配置器中,最后返回这个路劲匹配配置器

3.3.4 注册PathMatcher

@Bean
public PathPatternParser mvcPatternParser() {
   /**
    * getPathMatchConfigurer()方法,获取路径匹配配置器,见3.3.3.4
    * 然后得到这个配置器中的路径匹配器,把它放入容器中
    * 如果用户未配置,则使用默认的AntPathMatcher
    */
   return getPathMatchConfigurer().getPatternParserOrDefault();
}

先尝试从PathMatchConfigurer路径匹配配置器中取出用户配置的PathMatcher路径匹配器,若存在,则直接将用户配置的放入容器中,否则返回defaultPatternParser

3.3.5 注册UrlPathHelper

@Bean
public UrlPathHelper mvcUrlPathHelper() {
   /**
    * getPathMatchConfigurer()方法,获取路径匹配配置器,见3.3.3.4
    * 然后得到这个配置器中的UrlPathHelper,把它放入容器中
    * 如果用户未配置,则使用默认的UrlPathHelper
    */
   return getPathMatchConfigurer().getUrlPathHelperOrDefault();
}

先尝试从PathMatchConfigurer路径匹配配置器中取出用户配置的UrlPathHelper,若存在,则直接将用户配置的放入容器中,否则创建一个新的UrlPathHelper对象放入容器中

3.3.6 注册ContentNegotiationManager

@Bean
public ContentNegotiationManager mvcContentNegotiationManager() {
    if (this.contentNegotiationManager == null) {
        //创建一个内容协商配置器
        ContentNegotiationConfigurer configurer = new ContentNegotiationConfigurer(this.servletContext);
        //将服务器默认能生产的媒体类型保存到内容协商配置器
        configurer.mediaTypes(getDefaultMediaTypes());
        /**
         * 和上面拦截器一样,会以这个内容协商配置器为参数调用所有配置类对象中
         * configureContentNegotiation(configurer)方法,用户通过这个配置器注册和
         * 修改内容协商管理器。很明显,此处调用configureContentNegotiation(configurer)方法,
         * 就会带着WebMvcConfigurerComposite中所有的配置类对象都调用一次
         * configureContentNegotiation(configurer)方法,
         * 将配置类中配置的内容协商管理器注册到配置器中
         */
        configureContentNegotiation(configurer);
        //根据这个配置器的配置创建一个内容协商管理器
        this.contentNegotiationManager = configurer.buildContentNegotiationManager();
    }
    return this.contentNegotiationManager;
}

下面这个方法会获取到服务器默认能生产的媒体类型,实际上就判断是否导入了对应包

protected Map<String, MediaType> getDefaultMediaTypes() {
    Map<String, MediaType> map = new HashMap<>(4);
    //根据3.3.1的这些标志量,来得到服务器能够生产的媒体类型
    if (romePresent) {
        map.put("atom", MediaType.APPLICATION_ATOM_XML);
        map.put("rss", MediaType.APPLICATION_RSS_XML);
    }
    //导了jackson包就能生产xml,json
    if (jaxb2Present || jackson2XmlPresent) {
        map.put("xml", MediaType.APPLICATION_XML);
    }
    if (jackson2Present || gsonPresent || jsonbPresent) {
        map.put("json", MediaType.APPLICATION_JSON);
    }
    if (jackson2SmilePresent) {
        map.put("smile", MediaType.valueOf("application/x-jackson-smile"));
    }
    if (jackson2CborPresent) {
        map.put("cbor", MediaType.APPLICATION_CBOR);
    }
    return map;
}

并不是直接new一个ContentNegotiationManager对象,而是先创建一个ContentNegotiationConfigurer对象,然后将用户的自定义配置保存在ContentNegotiationConfigurer中,由ContentNegotiationConfigurer来生产ContentNegotiationManager对象,就是工厂模式,隐藏对象创建的细节。

3.3.7 注册viewControllerHandlerMapping

@Bean
@Nullable
public HandlerMapping viewControllerHandlerMapping(
      @Qualifier("mvcConversionService") FormattingConversionService conversionService,
      @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {

   //视图控制注册中心
   ViewControllerRegistry registry = new ViewControllerRegistry(this.applicationContext);
   /**
    * addViewControllers(registry)这个方法我们应该不陌生,我们经常在配置类中重写该方法,
    * 注册路径->视图的映射关系。
    * 此处调用addViewControllers(registry)方法,
    * 就会带着WebMvcConfigurerComposite中所有的配置类对象都调用一次
    * addViewControllers(registry)方法,注册路径->视图的映射关系
    */
   addViewControllers(registry);
   //真实类型为SimpleUrlHandlerMapping
   AbstractHandlerMapping handlerMapping = registry.buildHandlerMapping();
   if (handlerMapping == null) {
      return null;
   }
   PathMatchConfigurer pathConfig = getPathMatchConfigurer();
   if (pathConfig.getPatternParser() != null) {
      handlerMapping.setPatternParser(pathConfig.getPatternParser());
   }
   else {
      handlerMapping.setUrlPathHelper(pathConfig.getUrlPathHelperOrDefault());
      handlerMapping.setPathMatcher(pathConfig.getPathMatcherOrDefault());
   }
   //将所有的拦截器设置进去
   handlerMapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
   //跨域配置
   handlerMapping.setCorsConfigurations(getCorsConfigurations());
   return handlerMapping;
}

这个还是很重要的:我们经常在配置类中重写addViewControllers(registry)方法,注册路径->视图(ParameterizableViewController)的映射关系。最终由SimpleControllerHandlerAdapter调用ParameterizableViewController的handleRequest()方法完成视图处理,见3.3.19

3.3.8 注册BeanNameUrlHandlerMapping

@Bean
public BeanNameUrlHandlerMapping beanNameHandlerMapping(
      @Qualifier("mvcConversionService") FormattingConversionService conversionService,
      @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {

   BeanNameUrlHandlerMapping mapping = new BeanNameUrlHandlerMapping();
   mapping.setOrder(2);
   mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
   mapping.setCorsConfigurations(getCorsConfigurations());
   return mapping;
}

3.3.9 注册RouterFunctionMapping

@Bean
public RouterFunctionMapping routerFunctionMapping(
    @Qualifier("mvcConversionService") FormattingConversionService conversionService,
    @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {

    RouterFunctionMapping mapping = new RouterFunctionMapping();
    mapping.setOrder(3);
    mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
    mapping.setCorsConfigurations(getCorsConfigurations());
    mapping.setMessageConverters(getMessageConverters());
    return mapping;
}

功能路由,类似于webflux,直接忽略

3.3.10 注册resourceHandlerMapping

@Bean
@Nullable
public HandlerMapping resourceHandlerMapping(
    @Qualifier("mvcUrlPathHelper") UrlPathHelper urlPathHelper,
    @Qualifier("mvcPathMatcher") PathMatcher pathMatcher,
    @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
    @Qualifier("mvcConversionService") FormattingConversionService conversionService,
    @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {

    Assert.state(this.applicationContext != null, "No ApplicationContext set");
    Assert.state(this.servletContext != null, "No ServletContext set");

    //创建资源处理器注册中心
    ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext,
                                                                   this.servletContext, contentNegotiationManager, urlPathHelper);
    /**
     * 会以这个资源处理器注册中心为参数调用所有配置类对象中
     * addResourceHandlers(registry)方法,用户通过这个资源处理器注册中心设置静态资源的
     * 路径,静态资源位置,静态资源缓存时间等等。
     * 很明显,此处调用addResourceHandlers(registry)方法,
     * 就会带着WebMvcConfigurerComposite中所有的配置类对象都调用一次
     * addResourceHandlers(registry)方法,将配置类中静态资源的配置保存到资源处理器注册中心中 
     */
    addResourceHandlers(registry);

    //通过资源处理器注册中心创建处理静态资源的SimpleUrlHandlerMapping
    AbstractHandlerMapping handlerMapping = registry.getHandlerMapping();
    if (handlerMapping == null) {
        return null;
    }
    //设置其它的一些配置,这些我们都见过,见3.3.3
    handlerMapping.setPathMatcher(pathMatcher);
    handlerMapping.setUrlPathHelper(urlPathHelper);
    handlerMapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
    handlerMapping.setCorsConfigurations(getCorsConfigurations());
    return handlerMapping;
}

这个HandlerMapping很重要,它用来处理静态资源。通过重写WebMvcConfigurer配置类的addResourceHandlers(registry)方法即可设置静态资源的路径,静态资源位置,静态资源缓存时间。然后没有配置@RequestMapping注解映射的路径就会交由这个HandlerMapping处理,只有它也不能处理才会返回404

3.3.11 注册ResourceUrlProvider

@Bean
public ResourceUrlProvider mvcResourceUrlProvider() {
   ResourceUrlProvider urlProvider = new ResourceUrlProvider();
   //获取用户在路径匹配配置器配置的UrlPathHelper
   urlProvider.setUrlPathHelper(getPathMatchConfigurer().getUrlPathHelperOrDefault());
   //获取用户在路径匹配配置器配置的PathMatcher
   urlProvider.setPathMatcher(getPathMatchConfigurer().getPathMatcherOrDefault());
   return urlProvider;
}

ResourceUrlProvider是一个监听器,监听ContextRefreshedEvent事件,即refresh()方法执行完成之前触发。

3.3.12 注册defaultServletHandlerMapping

@Bean
@Nullable
public HandlerMapping defaultServletHandlerMapping() {
    Assert.state(this.servletContext != null, "No ServletContext set");
    //默认Servlet处理配置器
    DefaultServletHandlerConfigurer configurer = new DefaultServletHandlerConfigurer(this.servletContext);
    /**
     * 会以这个默认Servlet处理的配置器为参数调用所有配置类对象中
     * configureDefaultServletHandling(configurer)方法,用户通过这个配置器启用Tomcat容器
     * 默认的Servlet,将静态资源请求转发给这个默认的Servlet处理。
     * 很明显,此处调用configureDefaultServletHandling(configurer)方法,
     * 就会带着WebMvcConfigurerComposite中所有的配置类对象都调用一次
     * configureDefaultServletHandling(configurer)方法,启用Tomcat容器默认的Servlet
     */
    configureDefaultServletHandling(configurer);
    //应用用户配置构建一个SimpleUrlHandlerMapping
    return configurer.buildHandlerMapping();
}

不知道大家知不知道springmmvc的这项配置<mvc:default-servlet-handler/>是什么意思?

其实这个配置就是启用Tomcat容器默认的Servlet处理静态资源请求,和此处配置类的configureDefaultServletHandling(configurer)方法是对应的,用户在这个方法中调用configurer.enable();就启用了Tomcat容器默认的Servlet

3.3.13 注册RequestMappingHandlerAdapter

@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
      @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
      @Qualifier("mvcConversionService") FormattingConversionService conversionService,
      @Qualifier("mvcValidator") Validator validator) {

   //创建一个RequestMappingHandlerAdapter对象
   RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();

   adapter.setContentNegotiationManager(contentNegotiationManager);
   /**
    * getMessageConverters()方法,获取所有的消息转换器,见3.3.13.2
    * 并将这些消息转换器设置到处理器适配器中
    */
   adapter.setMessageConverters(getMessageConverters());
   /**
    * getConfigurableWebBindingInitializer()方法,获取数据绑定器的初始化器,见3.3.13.3
    * 并将这个初始化器设置到处理器适配器中
    */
   adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer(conversionService, validator));
   /**
    * getArgumentResolvers()方法,获取所有用户自定义的参数解析器,见3.3.13.4
    * 并设置到处理器适配器中
    */
   adapter.setCustomArgumentResolvers(getArgumentResolvers());
   /**
    * getReturnValueHandlers()方法,获取所有用户自定义的返回值处理器,见3.3.13.5
    * 并设置到处理器适配器中
    */
   adapter.setCustomReturnValueHandlers(getReturnValueHandlers());

   /**
    * 导入了jackson包,就放入两个通知
    * 这两个通知分别会在@RequestBody注解参数类型转换前后执行
    * @ResponseBody注解方法返回值写入响应前后执行
    */
   if (jackson2Present) {
      adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice()));
      adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice()));
   }

   /*****************************异步支持的相关配置***********************************/
   // todo 异步支持配置器
   AsyncSupportConfigurer configurer = getAsyncSupportConfigurer();
   if (configurer.getTaskExecutor() != null) {
      //异步执行器,实际上就是线程池,异步完成处理器调用
      adapter.setTaskExecutor(configurer.getTaskExecutor());
   }
   if (configurer.getTimeout() != null) {
      //异步执行的超时时间
      adapter.setAsyncRequestTimeout(configurer.getTimeout());
   }
   //异步支持的拦截器
   adapter.setCallableInterceptors(configurer.getCallableInterceptors());
   adapter.setDeferredResultInterceptors(configurer.getDeferredResultInterceptors());

   return adapter;
}

我们可以发现,这个方法为RequestMappingHandlerAdapter做了一大堆配置,如下所示

  • 获取容器中内容协商管理器并注册进去
  • 获取所有的消息转换器并注册进去
  • 获取数据绑定器的初始化器并注册进去
  • 获取所有用户自定义的参数解析器并注册进去
  • 获取所有用户自定义的返回值处理器并注册进去
  • 将用户自定义的异步配置覆盖进去

3.3.13.1 创建一个RequestMappingHandlerAdapter对象

protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() {
    return new RequestMappingHandlerAdapter();
}

就是简单的new一个RequestMappingHandlerAdapter对象

3.3.13.2 获取所有的消息转换器

protected final List<HttpMessageConverter<?>> getMessageConverters() {
    if (this.messageConverters == null) {
        this.messageConverters = new ArrayList<>();
        /**
         * 此处调用configureMessageConverters(list)方法,
         * 就会带着WebMvcConfigurerComposite中所有的配置类对象都调用一次
         * configureMessageConverters(list)方法,向这个集合中添加消息转换器
         * 通过此方法添加消息转换器,springmvc容器就不会注册默认的消息转换器了
         */
        configureMessageConverters(this.messageConverters);
        if (this.messageConverters.isEmpty()) {
            //添加默认的消息转换器
            addDefaultHttpMessageConverters(this.messageConverters);
        }
        /**
         * 此处调用extendMessageConverters(list)方法,
         * 就会带着WebMvcConfigurerComposite中所有的配置类对象都调用一次
         * extendMessageConverters(list)方法,向这个集合中添加消息转换器
         * 这个方法是在系统默认的转换器基础额外添加用户自定义的消息转换器
         */
        extendMessageConverters(this.messageConverters);
    }
    return this.messageConverters;
}

如果用户未自定义消息转换器,那么springmvc就会添加一些默认的消息转换器

protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
   messageConverters.add(new ByteArrayHttpMessageConverter());
   messageConverters.add(new StringHttpMessageConverter());
   messageConverters.add(new ResourceHttpMessageConverter());
   messageConverters.add(new ResourceRegionHttpMessageConverter());
   try {
      messageConverters.add(new SourceHttpMessageConverter<>());
   }
   catch (Throwable ex) {
      // Ignore when no TransformerFactory implementation is available...
   }
   messageConverters.add(new AllEncompassingFormHttpMessageConverter());

   if (romePresent) {
      messageConverters.add(new AtomFeedHttpMessageConverter());
      messageConverters.add(new RssChannelHttpMessageConverter());
   }

   if (jackson2XmlPresent) {
      Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
      if (this.applicationContext != null) {
         builder.applicationContext(this.applicationContext);
      }
      messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
   }
   else if (jaxb2Present) {
      messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
   }

   if (jackson2Present) {
      Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();
      if (this.applicationContext != null) {
         builder.applicationContext(this.applicationContext);
      }
      messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build()));
   }
   else if (gsonPresent) {
      messageConverters.add(new GsonHttpMessageConverter());
   }
   else if (jsonbPresent) {
      messageConverters.add(new JsonbHttpMessageConverter());
   }

   if (jackson2SmilePresent) {
      Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile();
      if (this.applicationContext != null) {
         builder.applicationContext(this.applicationContext);
      }
      messageConverters.add(new MappingJackson2SmileHttpMessageConverter(builder.build()));
   }
   if (jackson2CborPresent) {
      Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.cbor();
      if (this.applicationContext != null) {
         builder.applicationContext(this.applicationContext);
      }
      messageConverters.add(new MappingJackson2CborHttpMessageConverter(builder.build()));
   }
}

可以看到,如果系统导入了对应的包,就会创建对应类型的消息转换器

3.3.13.3 获取数据绑定器的初始化器

protected ConfigurableWebBindingInitializer getConfigurableWebBindingInitializer(
    FormattingConversionService mvcConversionService, Validator mvcValidator) {

    //直接new一个数据绑定器的初始化器
    ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
    //统一转换服务
    initializer.setConversionService(mvcConversionService);
    //校验器
    initializer.setValidator(mvcValidator);
    /**
     * 此处调用getMessageCodesResolver()方法,
     * 就会带着WebMvcConfigurerComposite中所有的配置类对象都调用一次
     * getMessageCodesResolver()方法,获取用户配置的消息解码器
     * 只允许一个配置类重写getMessageCodesResolver()方法返回一个消息解码器
     */
    MessageCodesResolver messageCodesResolver = getMessageCodesResolver();
    if (messageCodesResolver != null) {
        initializer.setMessageCodesResolver(messageCodesResolver);
    }
    return initializer;
}

该方法应用用户自定义配置,初始化一个ConfigurableWebBindingInitializer数据绑定器的初始化器

3.3.13.4 获取所有用户自定义的参数解析器

protected final List<HandlerMethodArgumentResolver> getArgumentResolvers() {
    if (this.argumentResolvers == null) {
        this.argumentResolvers = new ArrayList<>();
        /**
         * 此处调用addArgumentResolvers(list)方法,
         * 就会带着WebMvcConfigurerComposite中所有的配置类对象都调用一次
         * addArgumentResolvers(list)方法,向这个集合中添加用户自定义的参数解析器
         */
        addArgumentResolvers(this.argumentResolvers);
    }
    return this.argumentResolvers;
}

该方法获取到所有配置类addArgumentResolvers(list)方法配置的参数解析器

3.3.13.5 获取所有用户自定义的返回值处理器

protected final List<HandlerMethodReturnValueHandler> getReturnValueHandlers() {
    if (this.returnValueHandlers == null) {
        this.returnValueHandlers = new ArrayList<>();
        /**
         * 此处调用addReturnValueHandlers(list)方法,
         * 就会带着WebMvcConfigurerComposite中所有的配置类对象都调用一次
         * addReturnValueHandlers(list)方法,向这个集合中添加用户自定义的返回值处理器
         */
        addReturnValueHandlers(this.returnValueHandlers);
    }
    return this.returnValueHandlers;
}

该方法获取到所有配置类addReturnValueHandlers(list)方法配置的返回值处理器

3.3.14 注册HandlerFunctionAdapter

@Bean
public HandlerFunctionAdapter handlerFunctionAdapter() {
   HandlerFunctionAdapter adapter = new HandlerFunctionAdapter();

   AsyncSupportConfigurer configurer = getAsyncSupportConfigurer();
   if (configurer.getTimeout() != null) {
      adapter.setAsyncRequestTimeout(configurer.getTimeout());
   }
   return adapter;
}

与3.3.9节中注册的RouterFunctionMapping对应

3.3.15 注册FormattingConversionService

@Bean
public FormattingConversionService mvcConversionService() {
    //DefaultFormattingConversionService的构造方法会自动注册一些默认的格式化器,类型转换器
    FormattingConversionService conversionService = new DefaultFormattingConversionService();
    /**
     * 此处调用addFormatters(conversionService)方法,
     * 就会带着WebMvcConfigurerComposite中所有的配置类对象都调用一次
     * addFormatters(conversionService)方法,向这个格式转换服务中添加用户自定义的配置
     * 比如注册格式化器,类型转换器等
     */
    addFormatters(conversionService);
    return conversionService;
}

FormattingConversionService注册到容器之前,允许用户向这个格式转换服务中添加自定义的配置

3.3.16 注册Validator

@Bean
public Validator mvcValidator() {
    /**
     * 此处调用getValidator()方法,
     * 就会带着WebMvcConfigurerComposite中所有的配置类对象都调用一次
     * getValidator()方法,获取用户配置的唯一的那一个Validator对象
     * 比如注册格式化器,类型转换器等
     */
    Validator validator = getValidator();
    /**
     * 用户未手动配置Validator,但是
     * 导入了Validator对应的包,于是自动使用反射实例化一个Validator对象
     */
    if (validator == null) {
        if (ClassUtils.isPresent("javax.validation.Validator", getClass().getClassLoader())) {
            Class<?> clazz;
            try {
                String className = "org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean";
                clazz = ClassUtils.forName(className, WebMvcConfigurationSupport.class.getClassLoader());
            }
            catch (ClassNotFoundException | LinkageError ex) {
                throw new BeanInitializationException("Failed to resolve default validator class", ex);
            }
            validator = (Validator) BeanUtils.instantiateClass(clazz);
        }
        //不校验的Validator
        else {
            validator = new NoOpValidator();
        }
    }
    return validator;
}

向容器中注册一个Validator,来源有三个地方

  • 用户在WebMvcConfigurer配置类中配置的惟一的Validator
  • 导入了Validator包,然后系统自动使用反射实例化一个Validator对象
  • 没有导入Validator包,那么就创建一个NoOpValidator对象,这个对象里面逻辑是空的,不进行校验

3.3.17 注册CompositeUriComponentsContributor

@Bean
public CompositeUriComponentsContributor mvcUriComponentsContributor(
    @Qualifier("mvcConversionService") FormattingConversionService conversionService,
    @Qualifier("requestMappingHandlerAdapter") RequestMappingHandlerAdapter requestMappingHandlerAdapter) {
    return new CompositeUriComponentsContributor(
        requestMappingHandlerAdapter.getArgumentResolvers(), conversionService);
}

这个暂时还不知道有什么用途,跳过

3.3.18 注册HttpRequestHandlerAdapter

@Bean
public HttpRequestHandlerAdapter httpRequestHandlerAdapter() {
    return new HttpRequestHandlerAdapter();
}

可用来处理器3.3.10和3.3.12节中注册的SimpleUrlHandlerMapping返回的处理器。它只能处理实现了HttpRequestHandler接口的处理器

3.3.19 注册SimpleControllerHandlerAdapter

@Bean
public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter() {
   return new SimpleControllerHandlerAdapter();
}

可用来处理器3.3.7节中注册的SimpleUrlHandlerMapping返回的处理器。它只能处理实现了Controller接口的处理器

3.3.20 注册HandlerExceptionResolver

@Bean
public HandlerExceptionResolver handlerExceptionResolver(
    @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager) {
    //处理器异常解析器
    List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<>();
    /**
     * 此处调用configureHandlerExceptionResolvers(list)方法,
     * 就会带着WebMvcConfigurerComposite中所有的配置类对象都调用一次
     * configureHandlerExceptionResolvers(list)方法,向这个集合中添加用户自定义的
     * 处理器异常解析器。注意:通过此方法添加处理器异常解析器,springmvc容器就不会注册默认的
     * 处理器异常解析器了
     */
    configureHandlerExceptionResolvers(exceptionResolvers);
    if (exceptionResolvers.isEmpty()) {
        //添加默认的处理器异常解析器
        addDefaultHandlerExceptionResolvers(exceptionResolvers, contentNegotiationManager);
    }
    /**
     * 此处调用extendHandlerExceptionResolvers(list)方法,
     * 就会带着WebMvcConfigurerComposite中所有的配置类对象都调用一次
     * extendHandlerExceptionResolvers(list)方法,向这个集合中添加处理器异常解析器
     * 这个方法是在系统默认的处理器异常解析器基础额外添加用户自定义的处理器异常解析器
     */
    extendHandlerExceptionResolvers(exceptionResolvers);
    //组合模式,多个处理器异常解析器完成解析功能
    HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
    composite.setOrder(0);
    composite.setExceptionResolvers(exceptionResolvers);
    return composite;
}

如果用户未自定义处理器异常解析器,那么springmvc就会添加一些默认的处理器异常解析器

protected final void addDefaultHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers,
                                                         ContentNegotiationManager mvcContentNegotiationManager) {

    //创建一个使用@ExceptionHandler注解方法处理异常的异常解析器
    ExceptionHandlerExceptionResolver exceptionHandlerResolver = createExceptionHandlerExceptionResolver();
    exceptionHandlerResolver.setContentNegotiationManager(mvcContentNegotiationManager);
    exceptionHandlerResolver.setMessageConverters(getMessageConverters());
    exceptionHandlerResolver.setCustomArgumentResolvers(getArgumentResolvers());
    exceptionHandlerResolver.setCustomReturnValueHandlers(getReturnValueHandlers());
    if (jackson2Present) {
        exceptionHandlerResolver.setResponseBodyAdvice(
            Collections.singletonList(new JsonViewResponseBodyAdvice()));
    }
    if (this.applicationContext != null) {
        exceptionHandlerResolver.setApplicationContext(this.applicationContext);
    }
    exceptionHandlerResolver.afterPropertiesSet();
    //1
    exceptionResolvers.add(exceptionHandlerResolver);

    ResponseStatusExceptionResolver responseStatusResolver = new ResponseStatusExceptionResolver();
    responseStatusResolver.setMessageSource(this.applicationContext);
    //2
    exceptionResolvers.add(responseStatusResolver);

    //3
    exceptionResolvers.add(new DefaultHandlerExceptionResolver());
}


/**
 * Protected method for plugging in a custom subclass of
 * {@link ExceptionHandlerExceptionResolver}.
 * @since 4.3
 */
protected ExceptionHandlerExceptionResolver createExceptionHandlerExceptionResolver() {
    return new ExceptionHandlerExceptionResolver();
}

默认注册了三个异常解析器:

  • ExceptionHandlerExceptionResolver: @ExceptionHandler注解方法处理异常
  • ResponseStatusExceptionResolver: @ResponseStatus注解处理异常,响应指定状态
  • DefaultHandlerExceptionResolver : 默认的异常解析器,根据异常类型响应指定的状态码

3.3.21 注册ViewResolver

@Bean
public ViewResolver mvcViewResolver(
    @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager) {
    //视图解析器注册中心
    ViewResolverRegistry registry =
        new ViewResolverRegistry(contentNegotiationManager, this.applicationContext);
    /**
     * 此处调用configureViewResolvers(registry)方法,
     * 就会带着WebMvcConfigurerComposite中所有的配置类对象都调用一次
     * configureViewResolvers(registry)方法,向这个注册中心中添加用户自定义的
     * 视图解析器
     */
    configureViewResolvers(registry);

    //用户未在配置类中注册视图解析器
    if (registry.getViewResolvers().isEmpty() && this.applicationContext != null) {
        //获取容器中所有视图解析器的beanName
        String[] names = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
            this.applicationContext, ViewResolver.class, true, false);
        //只有一个,说明就是当前这个视图解析器mvcViewResolver
        if (names.length == 1) {
            //添加一个默认的视图解析器InternalResourceViewResolver
            registry.getViewResolvers().add(new InternalResourceViewResolver());
        }
    }

    //组合模式,多个视图解析器先后解析一个视图,使用第一个能解析的视图解析器解析
    ViewResolverComposite composite = new ViewResolverComposite();
    composite.setOrder(registry.getOrder());
    composite.setViewResolvers(registry.getViewResolvers());
    if (this.applicationContext != null) {
        composite.setApplicationContext(this.applicationContext);
    }
    if (this.servletContext != null) {
        composite.setServletContext(this.servletContext);
    }
    return composite;
}

有两种配置视图解析器的方式:

  • 直接放入容器中,可以自动被识别为视图解析器
  • 通过WebMvcConfigurer配置类的configureViewResolvers(registry)方法注册用户自定义的视图解析器

需要注意的是:两种不能同时使用,否则第一种的就不会生效了

3.3.22 注册HandlerMappingIntrospector

@Bean
@Lazy
public HandlerMappingIntrospector mvcHandlerMappingIntrospector() {
    return new HandlerMappingIntrospector();
}

这个暂时还不知道有什么用途,跳过

3.4 总结一下注册的组件

使用@EnableWebMvc<mvc:annotation-driven>一共会向容器中注册20个组件,分别如下所示

requestMappingHandlerMapping->RequestMappingHandlerMapping
mvcPathMatcher->PathMatcher
mvcUrlPathHelper->UrlPathHelper
mvcContentNegotiationManager->ContentNegotiationManager
viewControllerHandlerMapping->SimpleUrlHandlerMapping
beanNameHandlerMapping->BeanNameUrlHandlerMapping
routerFunctionMapping->RouterFunctionMapping
resourceHandlerMapping->SimpleUrlHandlerMapping
mvcResourceUrlProvider->ResourceUrlProvider
defaultServletHandlerMapping->SimpleUrlHandlerMapping
requestMappingHandlerAdapter->RequestMappingHandlerAdapter
handlerFunctionAdapter->HandlerFunctionAdapter
mvcConversionService->FormattingConversionService
mvcValidator->Validator
mvcUriComponentsContributor->CompositeUriComponentsContributor
httpRequestHandlerAdapter->HttpRequestHandlerAdapter
simpleControllerHandlerAdapter->SimpleControllerHandlerAdapter
handlerExceptionResolver->HandlerExceptionResolverComposite
mvcViewResolver->ViewResolverComposite
mvcHandlerMappingIntrospector->HandlerMappingIntrospector

4. NoOpValidator

该类是WebMvcConfigurationSupport的嵌套类,表示不使用校验功能,可以看到,它的实现为空

private static final class NoOpValidator implements Validator {

   @Override
   public boolean supports(Class<?> clazz) {
      return false;
   }

   @Override
   public void validate(@Nullable Object target, Errors errors) {
   }
}

5. @EnableMVC流程图

Spring5源码16-@EnableWebMvc注解原理

参考文章

Spring5源码注释github地址 Spring源码深度解析(第2版) spring源码解析 Spring源码深度解析笔记 Spring注解与源码分析 Spring注解驱动开发B站教程