当前位置: 首页 > 科技观察

Tomcat中的可插拔和SCI的实现原理

时间:2023-03-19 13:18:34 科技观察

经常使用电脑的朋友一定记得,在U盘、硬盘等设备还流行的时候,引入这项技术的就是热插拔。这篇介绍主要是为了说明这些外接设备的方便性,同时说明它们的非侵入性。在Servlet3.x中,也加入了这种可插拔的能力,让我们可以接近项目组织中设备的访问。比如在Servlet3之前,只能在web.xml中声明Servlet、Filter等。Servlet3之后,除了@WebFilter之外,还可以将文件打包在一个单独的fragment中,在web-fragement.xml中声明的组件,在容器启动时会进行扫描。当然也可以在运行时动态添加Servlet/Filter,即Servlet3.x中的DynamicServlet。另外,对于SCI的实现,也提供了这个能力。通过标准接口的实现,在特定阶段触发动作执行。比如我们前面提到的SpringBoot应用,就是以Jar的形式启动容器,提供服务,由SCI触发。Tomcat是如何处理SpringBoot应用的?甚至容器本身的一些组件,比如JSPContainer的实现,也是利用SCI的能力实现的。本次主要分析Tomcat通过SCI实现的可插拔性(pluggability)。首先,什么是SCI?全称是ServletContainerInitializer,是一个接口,用于在启动阶段接收来自Web应用程序的通知,然后根据通知进行一些程序化的处理,比如动态注册Servlets、Filters等。如何使用?SCI的使用也比较容易。将HandlesTypes添加到实现ServletContainerInitializer接口的类。在启动阶段将扫描注解中指定的一系列类、接口和类集合。这些类相关的文件都被扫描出来,作为SCI的onStartup方法的参数传递。此类实现SCI接口。如果作为独立包发布,打包时会注册到JAR文件的META-INF/services/javax.servlet.ServletContainerInitializer文件中。当容器启动时,它会扫描所有带有这些注册信息的类进行分析,并在启动时调用它的onStartup方法。这是可插拔性?类加载是第一个反对意见。“我还能做热替换!”这里有区别。热替换和类加载都是按照限定名称加载,加载未知内容没有相关标准。在这里,SCI是基于约定的标准。扫描META-INF中包含注册信息的类,在启动阶段调用其onStartup。这就是区别。百闻不如一见。看看除了上面说的SpringBoot还有谁还在用SCI。我们先来看看WebSocket在Tomcat中的实现。@HandlesTypes({ServerEndpoint.class,ServerApplicationConfig.class,Endpoint.class})publicclassWsSciimplementsServletContainerInitiali这里HandlesTypes指定了实现WebSocket时需要注意的几个类,包括WebSocket通过注解的声明和通过编程声明。当应用程序启动时,触发onStartup方法的执行,然后初始化WebSocket相关内容,解析注解等。;if(clazzes==null||clazzes.size()==0){return;}//GroupthediscoveredclassesbytypeSetserverApplicationConfigs=newHashSet<>();Set>scannedEndpointClazzes=newHashSet<>();设置<类>scannedPojoEndpoints=newHashSet<>();这里要注意,由于WebSocket并不是针对具体的应用提供的,而是作为容器的基础能力,并且在Tomcat_home/lib目录下,所以每次应用启动时,都会触发WebSocket来解析是否包含对WebSocket的引用,所以为它提供支持。这个过程是如何串联起来的?我们在上一篇文章中分析了应用的部署,提到了HostConfig、ContextConfig等类。当应用程序启动时,启动事件会触发ContextConfigListener的执行。这时候会扫描应用中包含的JAR文件,解析web-fragement.xml,其中也包含了SCI实现的解析。//Step11.ApplytheServletContainerInitializerconfigtothe//contextif(ok){for(Map.Entry>>entry:initializerClassMap.entrySet()){if(entry.getValue().isEmpty()){context.addServletContainerInitializer(entry.getKey(),null);}else{context.addServletContainerInitializer(entry.getKey(),entry.getValue());}}}这里解析出来的class会被添加到Context中,application会启动阶段,会调用每个SCI实现的onStartup方法(entry.getValue(),getServletContext());}catch(ServletExceptione){log.error(sm.getString("standardContext.sciFail"),e);ok=false;break;}}SpringBoot也是这样触发的publicvoidonStartup(ServletContextservletContext)throwsServletException{this.logger=LogFactory.getLog(this.getClass());WebApplicationContextrootAppContext=this.createRootApplicationContext(servletContext);if(rootAppContext!=null){servletContext.addListener(newContextLoaderListener(rootAppContext){publicvoidcontextInitialized(ServletContextEventevent){}});}else{this.logger.debug("NoContextLoaderListenerregistered,ascreateRootApplicationContext()didnotreturnanapplicationcontext");}}而JSP的容器是也开始了,这样工厂就初始化好了,这样就可以继续使用/***InitializerfortheJSPEngine.*/publicclassJasperInitializerimplementsServletContain。这个JasperSCI只是为了初始化一个工厂吗?这和Servlet3.x之前没什么区别吧?别急,我们继续看它的onStartup方法publicvoidonStartup(Set>types,ServletContextcontext)throwsServletException{...//scantheapplicationforTLDsTldScannerscanner=newTldScanner(context,true,validate,blockExternal);try{scanner。扫描();}catch(IOException|SAXExceptione){thrownewServletException(e);}本来把TLD文件的扫描搬到这里,WebContainer只需要处理web.xml和web-fragement.xml,JSP的工作就交给他做什么了,各司其职,很好。用spec的话来说,最好把WebContainer和JSPContainer的职责分开。【本文为专栏作家“侯书城”原创稿件,转载请通过作者微信公众号“Tomcat物语”获得授权】点此查看本作者更多好文