Tomcat中的可插拔以及SCI的实现原理

开发 开发工具
我们本次主要来分析 Tomcat 通过 SCI 实现的这种可插拔性(pluggability)。

常用计算机的朋友一定记得, U盘,硬盘等设备流行的时候,当时对于这项技术的介绍是热插拔。

这个介绍最主要的是想说明这些外接设备的便利性,同时也说明他们的无侵入性。

[[207784]]

在 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的实现。

  1. @HandlesTypes({ServerEndpoint.class, ServerApplicationConfig.class, 
  2.         Endpoint.class}) 
  3. public class WsSci implements ServletContainerInitiali 

这里的HandlesTypes里指明了实现 WebSocket需要关注的几个类,将通过注解方式声明WebSocket和通过编程方式声明都包含了进来。

在应用启动时,触发onStartup方法执行,然后初始化WebSocket相关的内容,解析注解等

  1. public void onStartup(Set<Class<?>> clazzes, ServletContext ctx) 
  2.         throws ServletException { 
  3.  
  4.     WsServerContainer sc = init(ctx, true); 
  5.  
  6.     if (clazzes == null || clazzes.size() == 0) { 
  7.         return; 
  8.     } 
  9.  
  10.     // Group the discovered classes by type 
  11.     Set<ServerApplicationConfig> serverApplicationConfigs = new HashSet<>(); 
  12.     Set<Class<? extends Endpoint>> scannedEndpointClazzes = new HashSet<>(); 
  13.     Set<Class<?>> scannedPojoEndpoints = new HashSet<>(); 

这里注意,由于WebSocket并不是为特定应用提供的,而是做为容器的基础能力提供,并且其是在 Tomcat_home/lib 目录内,因此,每个应用在启动时,都会触发 WebSocket,来解析其是否包含了对于 WebSocket的引用,从而为其提供支持。

这一条流程是如何串连的呢?我们前面的文章曾分析过应用的部署,提到过HostConfig, ContextConfig这些类。 应用在启动时startup事件会触发 ContextConfig 这个Listener 的执行,此时会扫描应用包含的JAR文件,解析web-fragement.xml等, 这其中也包含对于SCI实现的解析。

  1. // Step 11. Apply the ServletContainerInitializer config to the 
  2. // context 
  3. if (ok) { 
  4.     for (Map.Entry<ServletContainerInitializer
  5.             Set<Class<?>>> entry : 
  6.                 initializerClassMap.entrySet()) { 
  7.         if (entry.getValue().isEmpty()) { 
  8.             context.addServletContainerInitializer( 
  9.                     entry.getKey(), null); 
  10.         } else { 
  11.             context.addServletContainerInitializer( 
  12.                     entry.getKey(), entry.getValue()); 
  13.         } 
  14.     } 

这里解析出来的类会添加到Context中,在应用启动阶段,会调用每个SCI实现的onStartup方法

  1. // Call ServletContainerInitializers 
  2. for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry : 
  3.     initializers.entrySet()) { 
  4.     try { 
  5.         entry.getKey().onStartup(entry.getValue(), 
  6.                 getServletContext()); 
  7.     } catch (ServletException e) { 
  8.         log.error(sm.getString("standardContext.sciFail"), e); 
  9.         ok = false
  10.         break; 
  11.     } 

SpringBoot 也是这样被点燃的

  1. public void onStartup(ServletContext servletContext) throws ServletException { 
  2.     this.logger = LogFactory.getLog(this.getClass()); 
  3.     WebApplicationContext rootAppContext = this.createRootApplicationContext(servletContext); 
  4.     if(rootAppContext != null) { 
  5.         servletContext.addListener(new ContextLoaderListener(rootAppContext) { 
  6.             public void contextInitialized(ServletContextEvent event) { 
  7.             } 
  8.         }); 
  9.     } else { 
  10.         this.logger.debug("No ContextLoaderListener registered, as createRootApplicationContext() did not return an application context"); 
  11.     } 
  12.  

而且 JSP 的容器也开始使用这种方式进行工厂的初始化,以便于后面继续使用。

  1. /** 
  2.  * Initializer for the Jasper JSP Engine. 
  3.  */ 
  4. public class JasperInitializer implements ServletContain 

那这个Jasper 的SCI,难道就为了初始化一个工厂吗?这和 Servlet 3.x之前也没啥区别是吧?

别急,我们继续看其onStartup方法

  1. public void onStartup(Set<Class<?>> types, ServletContext context) throws ServletException { 
  2.   
  3. ... 
  4.     // scan the application for TLDs 
  5.     TldScanner scanner = newTldScanner(context, true, validate, blockExternal); 
  6.     try { 
  7.         scanner.scan(); 
  8.     } catch (IOException | SAXException e) { 
  9.         throw new ServletException(e); 
  10.     } 

原来将 TLD文件的扫描移到了这里, WebContainer 只需要处理web.xml 和 web-fragement.xml的处理即可, JSP 的工作就交给他来做嘛,各司其职,挺好的。用 spec 的话来形容,是更好的分离了 Web Container 和 JSP Container职责。

【本文为51CTO专栏作者“侯树成”的原创稿件,转载请通过作者微信公众号『Tomcat那些事儿』获取授权】

戳这里,看该作者更多好文

责任编辑:赵宁宁 来源: 51CTO专栏
相关推荐

2023-03-01 10:37:51

2019-12-09 15:20:09

JavascriptPromise前端

2021-06-30 10:32:33

反射多态Java

2010-01-12 09:10:31

Java EE 6Servlet 3.0Web分片

2021-01-05 05:26:10

postMessage聊天机器人跨域技术

2022-12-31 09:42:14

超时功能

2021-02-26 14:26:02

内存轻薄本CPU

2023-05-18 22:51:08

2014-05-16 10:04:19

JavaScriptthis原理

2017-05-16 10:23:51

数据仓库拉链表

2022-12-26 09:27:48

Java底层monitor

2017-03-16 20:00:17

Kafka设计原理达观产品

2009-06-01 11:41:53

SilverlightSilverlight拖放

2010-05-06 12:18:34

IP负载均衡

2022-05-19 14:59:32

Tomcat服务器开放

2022-07-11 20:46:39

AQSJava

2023-12-15 16:07:40

物联网

2015-06-18 10:33:02

iOS粘性动画

2009-09-07 05:24:22

C#窗体继承

2024-01-19 12:48:00

Redis存储数据库
点赞
收藏

51CTO技术栈公众号