常用计算机的朋友一定记得, U盘,硬盘等设备流行的时候,当时对于这项技术的介绍是热插拔。
这个介绍最主要的是想说明这些外接设备的便利性,同时也说明他们的无侵入性。
在 Servlet 3.x 的时候,也增加了这种可插拔的能力,让我们在项目组织上,可以接近于设备的接入。
例如在 Servlet 3 之前只能在web.xml中声明 Servlet、Filter 等, 在 Servlet 3 之后,除了 @WebFilter 这种注解的方式外
还可以在单独的fragement 打包文件,在web-fragement.xml 声明的组件,容器启动时就会扫描到。
当然,也可以在运行时动态的添加Servlet/Filter,即Servlet 3.x中的 Dynamic Servlet。
除此之外,对于 SCI 的实现,提供的也是这种能力。通过对标准接口的实现,在特定阶段触发动作执行。
比如我们前面说到的 Spring Boot 的应用,其以 Jar 的方式启动,来启动容器,提供服务的实现,就是通过SCI的方式来触发的。Tomcat 是怎样处理 SpringBoot应用的?
甚至容器自行的一些组件,如JSP Container的实现,也使用 SCI 的能力来进行实现。
我们本次主要来分析 Tomcat 通过 SCI 实现的这种可插拔性(pluggability)。
首先,什么是 SCI?
全称 ServletContainerInitializer,是一个用于接收Web 应用在启动阶段通知的接口,再根据通知进行一些编程式的处理,比如动态注册Servlet、Filter等。
如何使用?
SCI 的使用也比较容易,将实现 ServletContainerInitializer 接口的类增加 HandlesTypes ,注解内指定的一系列类,接口,注解的 class 集合, 会在启动阶段 class 扫描的时候,将与这些 class 相关的 文件都扫描出来,做为 SCI 的onStartup方法参数传递。
这一类实现了 SCI 的接口,如果做为独立的包发布,在打包时,会在 JAR 文件的 META-INF/services/javax.servlet.ServletContainerInitializer 文件中进行注册。 容器在启动时,就会扫描所有带有这些注册信息的类进行解析,启动时会调用其 onStartup方法。
这就是可插拔性? 类加载***个表示不服。“我还可以热替换啊!” 这里是有区别的, 热替换,类加载,都是根据限定的名称去加载,并没有相关的标准去加载未知的内容,而这里SCI则根据约定的标准,扫描META-INF中包含注册信息的 class 并在启动阶段调用其onStartup,这就是区别啊。
百闻不如一见,光说不练假把式,我们来看除了前面说的 Spring Boot 外,谁还在用SCI。
我们先来看在 Tomcat 关于 WebSocket的实现。
- @HandlesTypes({ServerEndpoint.class, ServerApplicationConfig.class,
- Endpoint.class})
- public class WsSci implements ServletContainerInitiali
这里的HandlesTypes里指明了实现 WebSocket需要关注的几个类,将通过注解方式声明WebSocket和通过编程方式声明都包含了进来。
在应用启动时,触发onStartup方法执行,然后初始化WebSocket相关的内容,解析注解等
- public void onStartup(Set<Class<?>> clazzes, ServletContext ctx)
- throws ServletException {
- WsServerContainer sc = init(ctx, true);
- if (clazzes == null || clazzes.size() == 0) {
- return;
- }
- // Group the discovered classes by type
- Set<ServerApplicationConfig> serverApplicationConfigs = new HashSet<>();
- Set<Class<? extends Endpoint>> scannedEndpointClazzes = new HashSet<>();
- Set<Class<?>> scannedPojoEndpoints = new HashSet<>();
这里注意,由于WebSocket并不是为特定应用提供的,而是做为容器的基础能力提供,并且其是在 Tomcat_home/lib 目录内,因此,每个应用在启动时,都会触发 WebSocket,来解析其是否包含了对于 WebSocket的引用,从而为其提供支持。
这一条流程是如何串连的呢?我们前面的文章曾分析过应用的部署,提到过HostConfig, ContextConfig这些类。 应用在启动时startup事件会触发 ContextConfig 这个Listener 的执行,此时会扫描应用包含的JAR文件,解析web-fragement.xml等, 这其中也包含对于SCI实现的解析。
- // Step 11. Apply the ServletContainerInitializer config to the
- // context
- if (ok) {
- for (Map.Entry<ServletContainerInitializer,
- Set<Class<?>>> entry :
- initializerClassMap.entrySet()) {
- if (entry.getValue().isEmpty()) {
- context.addServletContainerInitializer(
- entry.getKey(), null);
- } else {
- context.addServletContainerInitializer(
- entry.getKey(), entry.getValue());
- }
- }
- }
这里解析出来的类会添加到Context中,在应用启动阶段,会调用每个SCI实现的onStartup方法
- // Call ServletContainerInitializers
- for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
- initializers.entrySet()) {
- try {
- entry.getKey().onStartup(entry.getValue(),
- getServletContext());
- } catch (ServletException e) {
- log.error(sm.getString("standardContext.sciFail"), e);
- ok = false;
- break;
- }
- }
SpringBoot 也是这样被点燃的
- public void onStartup(ServletContext servletContext) throws ServletException {
- this.logger = LogFactory.getLog(this.getClass());
- WebApplicationContext rootAppContext = this.createRootApplicationContext(servletContext);
- if(rootAppContext != null) {
- servletContext.addListener(new ContextLoaderListener(rootAppContext) {
- public void contextInitialized(ServletContextEvent event) {
- }
- });
- } else {
- this.logger.debug("No ContextLoaderListener registered, as createRootApplicationContext() did not return an application context");
- }
- }
而且 JSP 的容器也开始使用这种方式进行工厂的初始化,以便于后面继续使用。
- /**
- * Initializer for the Jasper JSP Engine.
- */
- public class JasperInitializer implements ServletContain
那这个Jasper 的SCI,难道就为了初始化一个工厂吗?这和 Servlet 3.x之前也没啥区别是吧?
别急,我们继续看其onStartup方法
- public void onStartup(Set<Class<?>> types, ServletContext context) throws ServletException {
- ...
- // scan the application for TLDs
- TldScanner scanner = newTldScanner(context, true, validate, blockExternal);
- try {
- scanner.scan();
- } catch (IOException | SAXException e) {
- throw new ServletException(e);
- }
原来将 TLD文件的扫描移到了这里, WebContainer 只需要处理web.xml 和 web-fragement.xml的处理即可, JSP 的工作就交给他来做嘛,各司其职,挺好的。用 spec 的话来形容,是更好的分离了 Web Container 和 JSP Container职责。
【本文为51CTO专栏作者“侯树成”的原创稿件,转载请通过作者微信公众号『Tomcat那些事儿』获取授权】