Spring Boot
Spring Boot
详细内容可参考官方文档。Spring Boot 官网。
我们分小节研究其相关特性。
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. 创建核心注解和核心类
- 创建 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 Applications。
要点翻译如下。
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 请求,看下是否会进入视图解析器处理逻辑。
- 非视图解析器的验证。 代码见 CommonController,浏览器输入 http://localhost:8081/common/hello,会正常的返回数据,不会进入 getCandidateViews() 方法。
- 进入视图解析器的验证。 浏览器输入不存在的 URL,如 http://localhost:8081/common/404Test,页面报错: Whitelabel Error Page。
- 自定义视图解析器。 即使用 @Bean 注入自定义的 ViewResolver 对象即可。
- 自定义 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 的自动配置。
- 在 WebMvcAutoConfiguration 有一个实现了 WebMvcConfigure 的配置类 WebMvcAutoConfigurationAdapter。
- 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 {
}
- 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);
}
}
}
附录一、参考文献
- 比较基础,更多的是教程 SpringBoot 夺命连环 70 问