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

常用计算机的朋友一定记得, 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 ServletContainerInitializer {

这里的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 ServletContainerInitializer {

那这个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职责。

以后有类似的需求,就可以按规范的方式,来增加这种热插拔的能力了。 :)

☆★☆更多精彩内容☆★☆

一台机器上安装多个Tomcat 的原理(回复001)

监控Tomcat中的各种数据 (回复002)

启动Tomcat的安全机制(回复003)

乱码问题的原理及解决方式(回复007)

Tomcat 日志工作原理及配置(回复011)

web.xml 解析实现(回复 012)

线程池的原理( 回复 014)

Tomcat 的集群搭建原理与实现 (回复 015)

类加载器的原理 (回复 016)

类找不到等问题 (回复 017)

代码的热替换实现(回复 018)

Tomcat 进程自动退出问题 (回复 019)

为什么总是返回404? (回复 020)

...

               iOS赞赏通道

PS: 对于一些 Tomcat常见问题,在公众号的【常见问题】菜单中,有需要的朋友欢迎关注查看。

觉得本文对你有帮助?请分享给更多人支持一下吧,谢谢

关注『 Tomcat那些事儿  』 ,发现更多精彩文章!了解各种常见问题背后的原理与答案。深入源码,分析细节,内容原创,欢迎关注。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值