SpringBoot中的SCI接口
更新日期:
servlet3.0使用Java SPI机制
Servlet3.0环境下ServletContainerInitializer(简称SCI)接口的使用.
SCI定义
1 | // 完整命名: javax.servlet.ServletContainerInitializer |
SCI概述
ServletContainerInitializer 是 Servlet 3.0 新增的一个接口,主要用于在容器启动阶段通过编程风格注册Filter, Servlet以及Listener,以取代通过web.xml配置注册。这样就利于开发内聚的web应用框架.
我们以SpringMVC举例, servlet3.0之前我们需要在web.xml中依据Spring的规范新建一堆配置。这样就相当于将框架和容器紧耦合了。而在3.x后注册的功能内聚到Spring里,Spring-web就变成一个纯粹的即插即用的组件,不用依据应用环境定义一套新的配置。
SCI原理
1)ServletContainerInitializer接口的实现类通过java SPI声明自己是ServletContainerInitializer 的provider.
2)容器启动阶段依据java spi获取到所有ServletContainerInitializer的实现类,然后执行其onStartup方法.
3)另外在实现ServletContainerInitializer时还可以通过@HandlesTypes注解定义本实现类希望处理的类型,容器会 将当前应用中所有这一类型(继承或者实现)的类放在ServletContainerInitializer接口的集合参数c中传递进来。 如果不定义处理类型,或者应用中不存在相应的实现类,则集合参数c为空.
4)这一类实现了 SCI 的接口,如果做为独立的包发布,在打包时,会在JAR 文件的 META-INF/services/javax.servlet.ServletContainerInitializer 文件中进行注册。 容器在启动时,就会扫描所有带有这些注册信息的类(@HandlesTypes(WebApplicationInitializer.class)这里就是加载WebApplicationInitializer.class类)进行解析,启动时会调用其 onStartup方法——也就是说servlet容器负责加载这些指定类, 而ServletContainerInitializer的实现者(例如Spring-web中的SpringServletContainerInitializer对接口ServletContainerInitializer的实现中,是可以直接获取到这些类的)
与类加载的区别
类加载是根据限定的名称去加载,并没有相关的标准去加载未知的内容.
而SCI(全称 ServletContainerInitializer)则是根据约定的标准,扫描META-INF中包含注册信息的 class 并在启动阶段调用其onStartup.
SpringMVC中的应用
通过查看ServletContainerInitializer继承层级, 可以发现spring-web中的SpringServletContainerInitializer正是实现了ServletContainerInitializer接口;
1 | @HandlesTypes(WebApplicationInitializer.class) |
1)SpringServletContainerInitializer 由支持Servlet3.0+的Servlet容器实例化并调用.
2)Servlet容器还会查询classpath下SpringServletContainerInitializer类上修饰的@HandlesTypes注解所标注的 WebApplicationInitializer接口的实现类. 这一步也是容器帮我们完成的.
3)SpringServletContainerInitializer通过实现ServletContainerInitializer将自身并入到Servlet容器的生命周期中, 并通过自身定义的WebApplicationInitializer将依赖于Spring框架的系统初始化需求与Servlet容器解耦. 即依赖于spring的系统可以通过实现WebApplicationInitializer来实现自定义的初始化逻辑. 而不需要去实现ServletContainerInitializer
Tomcat调用SCI的时机
现在还有一个疑问, 就是 ServletContainerInitializer 的调用时机?, 因为servlet容器除了会回调SCI之外, 还有回调诸如servlet, listener等. 搞清楚这些先后顺序可以帮助我们快速定位和理解某些奇怪的问题.
这里我们就以Tomcat举例, 以下逻辑总结于Tomcat7.x, 有兴趣的读者可以去StandardContext类中对startInternal的实现中(第5608行 —— 第5618行, 这也是Tomcat中唯一的调用ServletContainerInitializers接口的onStartup方法的位置)求证下:
1)解析web.xml
2)往ServletContext实例中注入 context-param 参数
3)回调Servlet3.0的ServletContainerInitializers接口实现类
4)触发 Listener 事件(beforeContextInitialized, afterContextInitialized); 这里只会触发 ServletContextListener 类型的
5)初始化 Filter, 调用其init方法
6)加载 启动时即加载的servlet
原文链接:https://blog.csdn.net/lqzkcx3/article/details/78507169
springBoot打成war在外部容器的启动
Springboot在使用外部容器运行时
需要写一个初始化类继承SpringBootServletInitializer
1 | public class OasysServletInitializer extends SpringBootServletInitializer { |
SpringBootServletInitializer实现了WebApplicationInitializer接口,而实现了WebApplicationInitializer的类会被servlet容器扫描,并且调用onStartup()方法。
1 | public abstract class SpringBootServletInitializer implements WebApplicationInitializer { |
Springboot提供的SCI接口
ServletContextInitializer接口
1 | package org.springframework.boot.web.servlet; |
注释:
1)接口用于以编程方式配置Servlet 3.0+ {@link ServletContext context}。
2)实现这个接口(并且没有实现{@link WebApplicationInitializer})的类不会被{@link SpringServletContainerInitializer}检测到,因此不会被Servlet容器自动引导。
3)这个接口的作用类似于{@link ServletContainerInitializer},但是它的生命周期是由Spring管理的,而不是由Servlet容器管理的。
接口使用
在springboot中注册一个servlet可以使用这种方式
1 | @Bean |
ServletRegistrationBean extends DynamicRegistrationBean
DynamicRegistrationBean extends RegistrationBean
RegistrationBean implements ServletContextInitializer, Ordered
通过继承结构可以看出,是通过实现Springboot提供的SCI接口(调用该接口的onStartup())来完成的。
关于该接口的一些想法及猜测
1)实现了该接口的类必须通过@Bean等方式注册进spring容器中,才能被springboot发现从而调用其onStartup()
2)servlet 3动态加载servlet的机制只能在上下文ServletContext加载时对Servlet,Filter,,Listener进行注册。
当springboot打包成war包放入外部容器中时,外部容器启动springboot项目入口是实现WebApplicationInitializer接口的SpringBootServletInitializer类,SpringBootServletInitializer实例执行onStartup方法的时候会通过createRootApplicationContext方法来执行run方法,接下来的过程就同以jar包形式启动的应用的run过程一样了,在内部会创建IOC容器并返回,只是以war包形式的应用在创建IOC容器过程中,不再创建Servlet容器了。
因此整个启动过程都是在ServletContext加载时进行的,使用Springboot提供的SCI接口注册Servlet的Bean也会被springboot发现从而被调用onStartup()方法完成servlet注册(且该Bean是由spring容器管理的,并不会被servlet容器发现)。
3) springboot的内置容器启动和外置容器启动
内置容器:
jar包:
执行SpringBootApplication的run方法,启动IOC容器,然后创建嵌入式Servlet容器
外置容器:
war包:
先是启动Servlet服务器,服务器启动Springboot应用(springBootServletInitizer),然后启动IOC容器