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

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

转载: 侯树成 Tomcat那些事儿 2017-09-19

引子

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

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职责

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Tomcat是一个Java Servlet容器,用于处理HTTP请求。它通过一系列的组件和功能来实现对请求的处理。其,连接器和映射器是实现Tomcat请求路径与虚拟站点及Servlet之间的映射的关键组件。 连接器是Tomcat的一个组件,用于接收来自客户端的HTTP请求,并将请求传递给Tomcat服务器。当接收到一个HTTP请求后,连接器会解析请求的路径信息,并将请求路径传递给映射器。 映射器是另一个重要的组件,用于将请求路径映射到对应的虚拟站点或Servlet。它通过解析请求路径,并与配置文件的映射规则进行匹配,来确定请求应该由哪个虚拟站点或Servlet处理。映射规则通常是通过配置文件(如web.xml和Tomcat的server.xml)进行定义的。 在Tomcat启动时,它会加载并解析这些配置文件,并根据配置的规则建立请求路径与虚拟站点或Servlet的映射关系。当一个请求到达时,映射器就会根据这些映射关系将请求转发到相应的处理组件(如Servlet)。 总结起来,Tomcat实现请求路径的查找和处理是通过连接器接收请求,映射器根据配置文件的映射规则确定请求应该由哪个虚拟站点或Servlet处理。这样,Tomcat能够根据请求路径找到对应的虚拟站点,并将请求传递给相应的Servlet进行处理。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [tomcat的工作原理](https://blog.csdn.net/dongcheng_2015/article/details/117091790)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值