Spring Boot

felix.shao2025-04-03

Spring Boot

 详细内容可参考官方文档。Spring Boot 官网open in new window
 我们分小节研究其相关特性。

1. 手写模拟 SpringBoot 核心流程

概要

 参考文献如下。

 示例代码见 https://gitee.com/shj.com/sample-projects/tree/master/back/sample-sim-src-code/sample-sim-spring-boot-demo
 目前示例代码实现了以下核心流程

  • 手写模拟SpringBoot启动过程。
  • 手写模拟SpringBoot条件注解功能。
  • 手写模拟SpringBoot自动配置功能。
1. maven 依赖导入

 导入 spring、servlet、tomcat 等相关依赖。直接见示例 pom.xml 文件。

2. 创建应用启动入口

 即创建 SimSpringApplication。

public class SimSpringApplication {

    /**
     * 在run方法中,我们要实现的逻辑如下:
     * 1.创建一个Spring容器
     * 2.创建容器对象,如 Tomcat
     * 3.生成DispatcherServlet对象,并且和前面创建出来的Spring容器进行绑定
     * 4.将DispatcherServlet添加到Tomcat中
     * 5.启动容器
     * @param clazz
     */
    public static void run(Class clazz) {
        // 1. 创建 Spring 容器
        AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
        applicationContext.register(clazz);
        applicationContext.refresh();

        // 2. 启动容器
        WebServer webServer = getWebServer(applicationContext);
        webServer.start(applicationContext);
    }

    public static WebServer getWebServer(AnnotationConfigWebApplicationContext applicationContext) {
        // key为beanName, value为Bean对象
        Map<String, WebServer> webServers = applicationContext.getBeansOfType(WebServer.class);

        if (webServers.isEmpty()) {
            throw new NullPointerException();
        }
        if (webServers.size() > 1) {
            throw new IllegalStateException();
        }

        // 返回唯一的一个
        return webServers.values().stream().findFirst().get();
    }

}
3. 创建核心注解和核心类
  1. 创建 SimSpringBootApplication 注解。该注解作用是 applicationContext.register(clazz); 注册时,扫描到了 @ComponentScan 注解,复用 Spring 的扫描逻辑加载 bean 到容器中。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
@Import(SimImportSelect.class)
public @interface SimSpringBootApplication {

}
4. 启动 Tomcat 逻辑实现

 首先抽象个 Server 类,方便动态替换容器。然后定义对应的 TomcatWebServer 和 JettyWebServer。

public interface WebServer {
    void start(WebApplicationContext applicationContext);
}

public class TomcatWebServer implements WebServer{
    @Override
    public void start(WebApplicationContext applicationContext) {
        System.out.println("启动Tomcat web server");
        //此处复用上方启动tomcat代码
        startTomcat(applicationContext);
    }

    // 启动 tomcat
    public static void startTomcat(WebApplicationContext applicationContext) {
        Tomcat tomcat = new Tomcat();
        Server server = tomcat.getServer();
        Service service = server.findService("Tomcat");
        Connector connector = new Connector();
        connector.setPort(8081);
        Engine engine = new StandardEngine();
        engine.setDefaultHost("localhost");
        Host host = new StandardHost();
        host.setName("localhost");
        String contextPath = "";
        Context context = new StandardContext();
        context.setPath(contextPath);
        context.addLifecycleListener(new Tomcat.FixContextListener());
        host.addChild(context);
        engine.addChild(host);
        service.setContainer(engine);
        service.addConnector(connector);
        tomcat.addServlet(contextPath, "dispatcher", new DispatcherServlet(applicationContext));
        context.addServletMappingDecoded("/*", "dispatcher");
        try {
            tomcat.start();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

public class JettyWebServer implements WebServer {
    @Override
    public void start(WebApplicationContext applicationContext) {
        System.out.println("启动Jetty Web Server");
    }
}

 然后模拟实现条件注入,即模拟实现条件注入主键及其自定义匹配逻辑。

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Conditional(SimOnClassCondition.class)
public @interface SimConditionOnClass {

    String value() default "";

}

public class SimOnClassCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Map<String, Object> attributes = metadata.getAnnotationAttributes(SimConditionOnClass.class.getName());
        String className = (String) attributes.get("value");
        try{
            context.getClassLoader().loadClass(className);
            return true;
        }catch (ClassNotFoundException ex){
            return false;
        }
    }
}

/**
 * 模拟实现条件注入 
 */
public class WebServiceAutoConfiguration implements AutoConfiguration {

    @Bean
    @SimConditionOnClass("org.apache.catalina.startup.Tomcat")
    public TomcatWebServer tomcatWebServer(){
        return new TomcatWebServer();
    }

    @Bean
    @SimConditionOnClass("org.eclipse.jetty.server.Server")
    public JettyWebServer jettyWebServer(){
        return new JettyWebServer();
    }

}
5. 发现自动配置类

 使用 Spring SPI 发现自动配置类。即 META-INF/services 下新增 com.felix.sim.spring.boot.config.AutoConfiguration 文件,文件内容如下。

com.felix.sim.spring.boot.config.WebServiceAutoConfiguration

 再使用 Import 导入这些配置类。

public class SimImportSelect implements DeferredImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        ServiceLoader<AutoConfiguration> loader = ServiceLoader.load(AutoConfiguration.class);
        List<String> list = new ArrayList<>();
        for (AutoConfiguration configuration : loader) {
            list.add(configuration.getClass().getName());
        }
        return list.toArray(new String[0]);
    }
}
验证

 启动如下 main 函数,controller 随便写个返回 String 类型的 Http 接口,访问测试即可,详见样例代码(想要支持返回其他类型,解析为 String 类型还得拓展功能)。

@SimSpringBootApplication
public class SimSpringBootApplicationDemo {

    public static void main(String[] args) {
        SimSpringApplication.run(SimSpringBootApplicationDemo.class);
    }

}

2. SpringBoot 的 Spring MVC

2.1 概念

 为了更好理解相应概念,也会例举部分源码进行分析。

官方说明

 官方文档说明 Servlet Web Applicationsopen in new window
 要点翻译如下。
 Spring Boot 为 Spring MVC 提供了自动配置,可以很好地与大多数应用程序配合使用。它取代了 @EnableWebMvc 的需要,两者不能一起使用。除了 Spring MVC 的默认设置外,自动配置还提供了以下功能。

  • 包含 ContentNegotiatingViewResolver 和 BeanNameViewResolver bean。
  • 支持提供静态资源,包括支持 WebJars(本文稍后介绍)。
  • Converter、GenericConverter 和格式化 bean 的自动注册。
  • 支持 HttpMessageConverters(本文稍后介绍)。
  • MessageCodesResolver 的自动注册(本文稍后介绍)。
  • 静态 index.html 支持。
  • 自动使用 ConfigurableWebBindingInitializer bean(本文稍后介绍)。

 (本文稍后介绍)是翻译原话,我们这里参考源码、以及简单示例理解下部分功能,详细可以阅读官方文档。

 Spring Boot 两种方式使用 SpringMVC

 默认的自动配置类 WebMvcAutoConfiguration 是启用的,如下。

@AutoConfiguration(
    after = {DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class}
)
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
public class WebMvcAutoConfiguration {

  @Bean
  @ConditionalOnMissingBean
  public InternalResourceViewResolver defaultViewResolver() {
      InternalResourceViewResolver resolver = new InternalResourceViewResolver();
      resolver.setPrefix(this.mvcProperties.getView().getPrefix());
      resolver.setSuffix(this.mvcProperties.getView().getSuffix());
      return resolver;
  }

  @Bean
  @ConditionalOnBean({View.class})
  @ConditionalOnMissingBean
  public BeanNameViewResolver beanNameViewResolver() {
      BeanNameViewResolver resolver = new BeanNameViewResolver();
      resolver.setOrder(2147483637);
      return resolver;
  }

  @Bean
  @ConditionalOnBean({ViewResolver.class})
  @ConditionalOnMissingBean(
      name = {"viewResolver"},
      value = {ContentNegotiatingViewResolver.class}
  )
  public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
      ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
      resolver.setContentNegotiationManager((ContentNegotiationManager)beanFactory.getBean(ContentNegotiationManager.class));
      resolver.setOrder(Integer.MIN_VALUE);
      return resolver;
  }
}

 注意 @EnableWebMvc 启用时,导入了一个 WebMvcConfigurationSupport 类,因此 WebMvcAutoConfiguration 会失效,如下代码。

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

@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
}
 1. 相关核心类介绍

 ViewResolver 都是 SpringMVC 内置的视图解析器。

 1. InternalResourceViewResolver

 内部资源视图解析器,通过类似 prefix=/WEB-INF/suffix=jsp 配置支持如 jsp、html 文件的解析。

 2. BeanNameViewResolver

 简单示例见 MvcViewController.helloJavaView 和 HelloJavaView 对象代码。
 以下是要点。

  • 注意 Controller 不要有 @ResponseBody 注解,不然不会进入视图解析器。
  • 找的 Bean 要实现了 View 接口。
  • 是根据 Bean 的名称找的视图解析器。
 3. ContentNegotiatingViewResolver

 ContentNegotiatingViewResolver 本身不解析视图,而是委托给其他 ViewResolver。
 具体解析视图对应逻辑为 ContentNegotiatingViewResolver.resolveViewName() 方法,关注以下要点。

  • 返回 Json 对象时,返回的不是视图。
  • 所有视图解析器,都会根据返回的视图名称进行 resolveViewName。

 如下源码,关注下一下逻辑。

  • 初始化视图解析器。
  • 获得所有匹配的视图。
    • 委托给其他 ViewResolver 处理。
  • 获得最终匹配的视图。
public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
		implements ViewResolver, Ordered, InitializingBean {

  @Override
	protected void initServletContext(ServletContext servletContext) {
		Collection<ViewResolver> matchingBeans =
				BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
		if (this.viewResolvers == null) {
      // 初始化视图解析器,可以打个断点看下有哪些数据
			this.viewResolvers = new ArrayList<>(matchingBeans.size());
			for (ViewResolver viewResolver : matchingBeans) {
				if (this != viewResolver) {
					this.viewResolvers.add(viewResolver);
				}
			}
		}
		else {
			for (int i = 0; i < this.viewResolvers.size(); i++) {
				ViewResolver vr = this.viewResolvers.get(i);
				if (matchingBeans.contains(vr)) {
					continue;
				}
				String name = vr.getClass().getName() + i;
				obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name);
			}

		}
		AnnotationAwareOrderComparator.sort(this.viewResolvers);
		this.cnmFactoryBean.setServletContext(servletContext);
	}

   public View resolveViewName(String viewName, Locale locale) throws Exception {
		RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
		Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
		List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
		if (requestedMediaTypes != null) {
      // 获得所有匹配的视图
			List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
      // 获取最终的视图
			View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
			if (bestView != null) {
				return bestView;
			}
		}

		String mediaTypeInfo = logger.isDebugEnabled() && requestedMediaTypes != null ?
				" given " + requestedMediaTypes.toString() : "";

		if (this.useNotAcceptableStatusCode) {
			if (logger.isDebugEnabled()) {
				logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo);
			}
			return NOT_ACCEPTABLE_VIEW;
		}
		else {
			logger.debug("View remains unresolved" + mediaTypeInfo);
			return null;
		}
	}

  private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
			throws Exception {

		List<View> candidateViews = new ArrayList<>();
		if (this.viewResolvers != null) {
			Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
      // ContentNegotiatingViewResolver 本身不解析视图,而是委托给其他 ViewResolver
			for (ViewResolver viewResolver : this.viewResolvers) {
				View view = viewResolver.resolveViewName(viewName, locale);
				if (view != null) {
					candidateViews.add(view);
				}
				for (MediaType requestedMediaType : requestedMediaTypes) {
					List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
					for (String extension : extensions) {
						String viewNameWithExtension = viewName + '.' + extension;
						view = viewResolver.resolveViewName(viewNameWithExtension, locale);
						if (view != null) {
							candidateViews.add(view);
						}
					}
				}
			}
		}
		if (!CollectionUtils.isEmpty(this.defaultViews)) {
			candidateViews.addAll(this.defaultViews);
		}
		return candidateViews;
	}
}
 2. 支持提供静态资源

 以前要访问 jps\css、js 等静态资源文件,需要在 web.xml 配置,在 SpringBoot 不需要配置,只需要放在约定文件夹中就可以(阅读大于配置)。
 源码如下,注意如下要点(各版本差别较大的话,源码和配置会略有不同)。

  • webjars 是用 jar 包装了一些 js、css 的 jar 包,https://www.webjars.org/ 可以看到列表。
  • 使用步骤是 pom.xml 添加对应 webjars 依赖,然后就可以使用了,见 Demo,使用 http://localhost:8081/webjars/jquery/3.5.1/jquery.js 访问验证。
  • 静态资源地址,看源码有这些地址 /META-INF/resources/classpath:/resources/classpath:/static/classpath:/public/,可以通过配置覆盖 spring.web.resources.static-locations
  • 配置欢迎页面,WebMvcAutoConfiguration 搜 index.html 可以找到默认的欢迎页面名字,注意是找 staticLocations 下找欢迎页面。
public class WebMvcAutoConfiguration {
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    if (!this.resourceProperties.isAddMappings()) {
      logger.debug("Default resource handling disabled");
    } else {
      // 去 webjars 里面去找资源
      this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
      this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (Consumer)((registration) -> {
        // this.resourceProperties 里面有静态资源地址
        registration.addResourceLocations(this.resourceProperties.getStaticLocations());
        if (this.servletContext != null) {
            ServletContextResource resource = new ServletContextResource(this.servletContext, "/");
            registration.addResourceLocations(new Resource[]{resource});
        }

      }));
    }
  }

  // 获取欢迎页
  private Resource getIndexHtmlResource() {
    for(String location : this.resourceProperties.getStaticLocations()) {
      Resource indexHtml = this.getIndexHtmlResource(location);
      if (indexHtml != null) {
        return indexHtml;
      }
    }

    ServletContext servletContext = this.getServletContext();
    if (servletContext != null) {
      return this.getIndexHtmlResource((Resource)(new ServletContextResource(servletContext, "/")));
    } else {
      return null;
    }
  } 

  private Resource getIndexHtmlResource(String location) {
    return this.getIndexHtmlResource(this.resourceLoader.getResource(location));
  }

  private Resource getIndexHtmlResource(Resource location) {
    try {
      Resource resource = location.createRelative("index.html");
      if (resource.exists() && resource.getURL() != null) {
        return resource;
      }
    } catch (Exception var3) {
    }
    return null;
  }
}

public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {
  private final WebProperties.Resources resourceProperties;
}

@ConfigurationProperties("spring.web")
public class WebProperties {
  private final Resources resources;

  public WebProperties() {
      this.localeResolver = WebProperties.LocaleResolver.ACCEPT_HEADER;
      this.resources = new Resources();
  }

  public static class Resources {
    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};

    public Resources() {
        this.staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
        this.addMappings = true;
        this.customized = false;
        this.chain = new Chain();
        this.cache = new Cache();
    }
  }

  public Resources getResources() {
        return this.resources;
    }
}
 3. 类型转换、数据格式化、数据验证

 略。也可以了解下 WebMvcAutoConfiguration 默认的对象。

 4. 支持 HttpMessageConverters

 HttpMessageConverters 负责 Http 请求和响应报文的处理。比如数据接收和响应是用 XML 还是 JSON 处理。
 默认的 HttpMessageConverters 源码如下。

@AutoConfiguration(
  after = {GsonAutoConfiguration.class, JacksonAutoConfiguration.class, JsonbAutoConfiguration.class}
)
@ConditionalOnClass({HttpMessageConverter.class})
@Conditional({NotReactiveWebApplicationCondition.class})
@Import({JacksonHttpMessageConvertersConfiguration.class, GsonHttpMessageConvertersConfiguration.class, JsonbHttpMessageConvertersConfiguration.class})
public class HttpMessageConvertersAutoConfiguration {

}
@Configuration(
    proxyBeanMethods = false
)
class JacksonHttpMessageConvertersConfiguration {
  
}

@Configuration(
  proxyBeanMethods = false
)
@ConditionalOnClass({ObjectMapper.class})
@ConditionalOnBean({ObjectMapper.class})
@ConditionalOnProperty(
  name = {"spring.mvc.converters.preferred-json-mapper"},
  havingValue = "jackson",
  matchIfMissing = true
)
static class MappingJackson2HttpMessageConverterConfiguration {
  MappingJackson2HttpMessageConverterConfiguration() {
  }

  @Bean
  @ConditionalOnMissingBean(
    value = {MappingJackson2HttpMessageConverter.class},
    ignoredType = {"org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter", "org.springframework.data.rest.webmvc.alps.AlpsJsonHttpMessageConverter"}
  )
  MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
    return new MappingJackson2HttpMessageConverter(objectMapper);
  }
}
 5. MessageCodesResolver 的自动注册

 比如验证 Bean 报错时的提示信息格式(errorCode.objectName.field 或 objectName.field.errorCode),略。

 6. 静态 index.html 支持

 即使用 InternalResourceViewResolver 默认的视图解析器,将后缀配置为 html,即可支持 html。

 7. 自动使用 ConfigurableWebBindingInitializer bean

 和 HttpMessageConverters 不同,这个其实是处理 json 的入参转换,如 @RequestBody。

public class WebMvcAutoConfiguration {
  protected ConfigurableWebBindingInitializer getConfigurableWebBindingInitializer(FormattingConversionService mvcConversionService, Validator mvcValidator) {
    try {
      return (ConfigurableWebBindingInitializer)this.beanFactory.getBean(ConfigurableWebBindingInitializer.class);
    } catch (NoSuchBeanDefinitionException var4) {
      return super.getConfigurableWebBindingInitializer(mvcConversionService, mvcValidator);
    }
  }
}

// 跟踪代码,找到 getConfigurableWebBindingInitializer.requestMappingHandlerAdapter 时会使用
public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
  @Bean
  public RequestMappingHandlerAdapter requestMappingHandlerAdapter(@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager, @Qualifier("mvcConversionService") FormattingConversionService conversionService, @Qualifier("mvcValidator") Validator validator) {
    RequestMappingHandlerAdapter adapter = this.createRequestMappingHandlerAdapter();
    adapter.setContentNegotiationManager(contentNegotiationManager);
    adapter.setMessageConverters(this.getMessageConverters());
    adapter.setWebBindingInitializer(this.getConfigurableWebBindingInitializer(conversionService, validator));
  }
}

2.2 使用示例

视图解析器简单验证

 在 ContentNegotiatingViewResolver.getCandidateViews() 方法上打个断点,触发 Controller 请求,看下是否会进入视图解析器处理逻辑。

  1. 非视图解析器的验证。  代码见 CommonController,浏览器输入 http://localhost:8081/common/hello,会正常的返回数据,不会进入 getCandidateViews() 方法。
  2. 进入视图解析器的验证。  浏览器输入不存在的 URL,如 http://localhost:8081/common/404Test,页面报错: Whitelabel Error Page。
  3. 自定义视图解析器。  即使用 @Bean 注入自定义的 ViewResolver 对象即可。
  4. 自定义 Excel 视图解析器。  添加自定义 View,实现 AbstractXlsxView,编写自己的业务逻辑即可,可以见 MyXlsxView 代码,数据对象应该是 Controller 的返回数据。
定制 SpringMVC ViewController

 以前写 SpringMVC 的时候,如果需要访问一个页面,必须要写 Controller 类,然后再写一个方法跳转到页面,感觉好麻烦,其实重写 WebMvcConfigurer 中的 addViewControllers 方法即可达到效果了。

@Override
public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/toLogin").setViewName("login");
}
定制 SpringMVC configurePathMatch

 重写 WebMvcConfigurer 中的 configurePathMatch 方法并通过调用 setUseSuffixPatternMatch 和 setUseRegisteredSuffixPatternMatch 方法来设置 PathMatch 的属性。

public void configurePathMatch(PathMatchConfigurer configurer) {
  configurer.setUseSuffixPatternMatch(false);
  configurer.setUseTrailingSlashMatch(true);
}
  • setUseSuffixPatternMatch(false):禁用后缀模式匹配,例如,/user 和 /user.json 将被视为不同的 URL。
  • setUseTrailingSlashMatch(true): 启用尾部斜杠匹配,例如, /user 和 /user/ 将被视为相同的 URL。
定制 SpringMVC 拦截器 Interceptor

 自定义实现了 HandlerInterceptor 的拦截器类,并重写 WebMvcConfigurer.addInterceptors 到 Spring 容器中即可。

定制 SpringMVC CORS 配置

 略。

定制 SpringMVC JSON

 略。

定制 SpringMVC 国际化

 略。

定制 SpringMVC 异常处理

 略。

2.3 原理

WebMvcConfigure 原理

 实现 WebMvcConfigure 接口可以拓展 MVC 实现,又既保留 SpringBoot 的自动配置。

  1. 在 WebMvcAutoConfiguration 有一个实现了 WebMvcConfigure 的配置类 WebMvcAutoConfigurationAdapter。
  2. WebMvcAutoConfigurationAdapter 它也是利用这种方式去进行扩展,所以我们通过查看这个类它帮我们实现了其他不常用的方法,帮助我们进行自动配置,我们只需定制(拦截器、视图控制器、CORS 在开发中需要额外定制的功能),这块主要逻辑为 @Import({EnableWebMvcConfiguration.class}) 配合处理的,代码如下。
@Configuration(
  proxyBeanMethods = false
)
@Import({EnableWebMvcConfiguration.class})
@EnableConfigurationProperties({WebMvcProperties.class, WebProperties.class})
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {
}
  1. EnableWebMvcConfiguration 它的父类上 setConfigurers 使用 @Autowired 注解。
    (1) 将容器中所有实现了 WebMvcConfigurer 接口的 Bean 都自动注入进来,添加到 configurers 中。
    (2) 添加到 delegates 委派器中。
    (3) 底层调用 WebMvcConfigure 对应的方法时,就是循环调用 delegates 处理。
    (4) 注意:当使用了 @EnableWebMvc 注解时,就不会使用 SpringMVC 自动配置类的默认配置了。
@Configuration(
  proxyBeanMethods = false
)
@EnableConfigurationProperties({WebProperties.class})
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {
}

@Configuration(
    proxyBeanMethods = false
)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
  @Autowired(required = false)
  public void setConfigurers(List<WebMvcConfigurer> configurers) {
    // (1) 将容器中所有实现了 WebMvcConfigurer 接口的 Bean 都自动注入进来,添加到 configurers 中。  
    if (!CollectionUtils.isEmpty(configurers)) {
      this.configurers.addWebMvcConfigurers(configurers);
    }
  }
}

class WebMvcConfigurerComposite implements WebMvcConfigurer {
  private final List<WebMvcConfigurer> delegates = new ArrayList();

  public void addWebMvcConfigurers(List<WebMvcConfigurer> configurers) {
    // (2) 添加到 delegates 委派器中。  
    if (!CollectionUtils.isEmpty(configurers)) {
      this.delegates.addAll(configurers);
    }
  }

  public void addInterceptors(InterceptorRegistry registry) {
    // (3) 底层调用 WebMvcConfigure 对应的方法时,就是循环调用 delegates 处理。
    for(WebMvcConfigurer delegate : this.delegates) {
      delegate.addInterceptors(registry);
    }
  }
}

附录一、参考文献

Last Updated 4/14/2025, 4:03:27 PM