我们通过向 IoC 容器注入 FilterRegistrationBean 的实例,SpringBoot 就自动使其作为一个 Filter 在 Web 容器中生效。

springboot如何动态注册bean springboot动态注册controller_java

那这是什么原理呢?

springboot如何动态注册bean springboot动态注册controller_spring boot_02

通过继承关系与属性,我们可以看出 FilterRegistrationBean 本质是对 javax.servlet.Filter 的包装,同时也是 Spring Bean 其继承了 org.springframework.boot.web.servlet.ServletContextInitializer 以便 Spring 监听 Web 容器启动事件,并进行回调。

1. SpringApplication 启动——Web 环境检测

springboot如何动态注册bean springboot动态注册controller_spring_03


SpringBoot 启动时通过 deduceFromClasspath 工具类方法来检测当前 jvm 实例的 Classpath 下是否有 javax.servlet.Servlet、ServletContainer、Servlet 等的实现,依此判断当前的 Web 环境。

  • 都没有返回 WebApplicationType.NONE,于是 SpringBoot 就是一个普通 Java 应用,而不是 Web 应用。
  • WebApplicationType.REACTIVE | WebApplicationType.SERVLET 都是 Web 应用。

将 Web 应用类型存入 SpringApplication.webApplicationType,用于后续的处理。

我们当前使用的项目依赖:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
	<exclusions>
		<exclusion>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
		</exclusion>
	</exclusions>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

于是,现在启动的 SpringBoot 便是 jetty-web,是一个 Servlet 应用。

2. SpringApplication 启动——Web 环境 create

在 org.springframework.boot.SpringApplication#run(java.lang.String…) 方法中,触发 Spring refresh 过程。不过在 refresh 之前还需要通过 createApplicationContext() 创建 ApplicationContext,即 Spring 上下文。

Spring 上下文也就与第一步中得到的 webApplicationType 有关,根据值 “SERVLET” 创建 AnnotationConfigServletWebServerApplicationContext 上下文实例。

springboot如何动态注册bean springboot动态注册controller_java_04


AnnotationConfigServletWebServerApplicationContext 是 SpringBoot web 环境上下文,其中也包含了 javax.servlet.ServletContext,也可以看做是一个 Web 容器上下文。

3. SpringApplication 启动——Web 环境 prepare

这里会触发 before refresh 事件,包含 context 的 各种监听器的注册,同时我们使用的 jetty 容器也会监听事件初始化 JettyWebServer 线程池。WebServer 的启动在后续的 refreshed 过程中。

与本文的主线无关,暂不过多介绍。

4. SpringApplication 启动——Web 环境 refresh

也就是 Spring 的 refresh 流程,同时本问的主线内容 FilterRegistrationBean 的动态注册也发生在此 refresh 流程中。

在当前的 jetty-web 环境中,触发的是 ServletWebServerApplicationContext#refresh 不过其直接调用 super.refresh() 未做过多修改,因此我们直接看其父类 AbstractApplicationContext#refresh。

springboot如何动态注册bean springboot动态注册controller_spring boot_05


refresh 流程中顺序定义了各种处理,其中只有 onRefresh() 与本文主线有关,我们接下来关注 onRefresh 方法——ServletWebServerApplicationContext#onRefresh 主要调用 createWebServer 创建 webServer 与配置 servletContext。

WebServer 的创建

委托给了 JettyServletWebServerFactory#getWebServer 来处理,并注册了 JettyServerCustomizer 回调。

ServletWebServerFactory 哪里来?

springboot如何动态注册bean springboot动态注册controller_spring boot_06


通过 SpringBoot 的自动配置注入 spring context。基于条件注解 @Configuration,同样也是根据项目启动的 Classpath 情况进行创建。

如上图,创建了 JettyServletWebServerFactory。最后通过该 Factory#getWebServer 创建的 Jetty WebServer 实例,也就是 JettyEmbeddedWebAppContext。

至此,WebServer 也就创建成功。

WebServer 的启动

等等,我们好像漏掉了一个很重要的东西。WebServer 都创建成功了也没见 Filter 的注册,那 FilterRegistrationBean 什么时候注册到 WebServer呢?

ServletWebServerFactory 创建 WebServer 的方法 getWebServer 有一个可变参数 ServletContextInitializer...

springboot如何动态注册bean springboot动态注册controller_Web_07

在 jetty webServer 的构建过程 JettyServletWebServerFactory#getWebServer 中,将 ServletContextInitializer 实例转换为 Configuration 实例存储到了 jetty WebAppContext 中。

springboot如何动态注册bean springboot动态注册controller_java_08

Spring ServletContextInitializer 转 Configuration 以中间形式 ServletContextInitializerConfiguration 类型存在。ServletContextInitializerConfiguration 实现了 jetty Configuration#configure(WebAppContext) 方法,组合了所有 Spring ServletContextInitializer。

当 jetty 启动时回调 ServletContextInitializerConfiguration#configure,同时也需要避免当前类加载器不可见 web 容器派生的类加载器加载的类,于是通过 TCC 机制切换 TCCL 回调了所有的 Spring ServletContextInitializer#onStartup 方法。

而 FilterRegistrationBean 的注册逻辑也就包含在 ServletContextInitializer#onStartup 方法中。

FilterRegistrationBean 的注册逻辑 —— ServletContextInitializer

springboot如何动态注册bean springboot动态注册controller_spring boot_09


ServletWebServerFactory 在创建 WebServe 时,可以传入一个 ServletContextInitializer 类型的参数,用于在 webServer 启动时回调。

从上图可以看出,FilterRegistrationBean 实现了 ServletContextInitializer ,也就是说我们在文章开头向 IoC 中注册的 Filter 其具备被 web 容器启动时回调的功能

ServletContextInitializer 的实现包含在了 ServletWebServerApplicationContext#selfInitialize 方法中。也就是说 jetty web 在启动时触发的 org.eclipse.jetty.webapp.AbstractConfiguration#configure 回调其实也就是在执行下图中的 selfInitialize 方法。非常重要的部分,代表 web 容器与 Spring 容器的关联。

springboot如何动态注册bean springboot动态注册controller_java_10


接下来主要关注 selfInitialize 方法实现。

springboot如何动态注册bean springboot动态注册controller_spring boot_11

getServletContextInitializerBeans 方法将容器中所有的 ServletContextInitializer 类型的 Bean 收集到集合,通过隐式返回迭代器进行遍历。

springboot如何动态注册bean springboot动态注册controller_前端_12


详细的 ServletContextInitializer bean 收集逻辑:

springboot如何动态注册bean springboot动态注册controller_java_13

debug 可以看到我们文章开头动态注册的 Bean :

springboot如何动态注册bean springboot动态注册controller_前端_14

遍历集合实现依次执行回调方法 ServletContextInitializer#onStartup,同时也就触发了将 Filter Bean 动态注册到 web 容器的回调:

springboot如何动态注册bean springboot动态注册controller_前端_15

至此,本文完整地详述了 Spring Filter Bean —— FilterRegistrationBean 到 Web Filter 的转换实现逻辑。

SpringWebMVC 应用部署到传统 servlet 容器,由 web 先启动是利用 SPI 扫描得到 ServletContainerInitializer 实例, servlet 容器启动时执行其回调从而启动 Spring。传统 servlet 容器不同于本文的嵌入式 servlet 容器,由 Springboot 先启动从容器中收集 ServletContextInitializer 注册给 servlet 容器。——传统的是容器主动扫描,嵌入式的是被动注入给容器。

SpringBoot 支持部署到传统 servlet 容器,利用 ServletContainerInitializer 的 SPI 实现。

也就是说,其实现原理是在打包时生成 META-INF/services/javax.servlet.ServletContainerInitializer 在其中指定 org.springframework.web.SpringServletContainerInitializer,配合 @HandlesTypes 利用 SPI 实现通过 servlet 容器启动引发 SpringBoot web 应用启动。

参考web 容器 SCI 机制 javax.servlet.ServletContainterInitializer & Spring org.springframework.web.SpringServletContainerInitializer,详细原理不再赘述。