ServiceLoader的应用
一、此处以Hadoop的FileSystem
为例,它的原理有这么一段:
private static void loadFileSystems() { synchronized(FileSystem.class){ if(!FILE_SYSTEMS_LOADED) { // 此处通过ServiceLoader把FileSystem所有的服务提供者都拿出来并且缓存起来了 // 这个概念,特别特别像通过配置文件配置Bean一样,类比Spring吧 只是没那么强大而已 ServiceLoader<FileSystem> serviceLoader = ServiceLoader.load(FileSystem.class); for(FileSystem fs : serviceLoader) { SERVICE_FILE_SYSTEMS.put(fs.getScheme(),fs.getClass()); } FILE_SYSTEMS_LOADED= true; } } }
可以看到FileSystem会把所有的FileSystem的实现都以scheme和class来cache,之后就从这个cache中取相应的值。
因此,以后可以通过ServiceLoader来实现一些类似的功能,而不用依赖像Spring这样的第三方框架(你让Hadoop去强一来Spring显然是非常不合适的)。
二、另外一个应用是责任链设计模式
责任链模式的定义:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
其实Spring底层很多都使用了此模式,但今天主要讲讲平时java中的实现。
其中我们熟悉的JDBC驱动加载就是这个例子。java.sql.DriverManager是用来管理负责加载JDBC驱动的,部分源码展示如下:
public class DriverManager { ... static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } private static void loadInitialDrivers() { ,,, ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); try{ // 只要导入了对应的Jar包,项目启动时候都会加载进来,用户其实并不需要再手动去加载了 while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } } ... }
然后我们此处以导入MySql驱动的Jar为例,它有一个如下文件:
这也就顺便解释了我们前边的一个疑问,其实JDBC驱动我们自己并不需要手动的去Class.forName()去加载了,JDK6以后,只要你导入了包,就自动给你加载进去了
另外我还看到有个哥们写的一个例子,也是典型应用,也分享给大家:消灭成堆的分支语句之类责任链模式
其实JDK的实现方案有的时候并不能满足我们的要求,比如我们希望这个配置文件在任意地方怎么办呢? 这里介绍一个方案:借助google开源的AutoService去自助实现(只不过一般都不这么干,Android应用这么用的可能性会大一点) 这样我们的配置文件就可以像Spring配置文件一下,放在几乎任何地方了
ServletContainerInitializer:和web容器相关的启动器
在web容器启动时为提供给第三方组件机会做一些初始化的工作,例如注册servlet或者filtes等,servlet规范中通过ServletContainerInitializer实现此功能。
使用方式:每个框架要使用ServletContainerInitializer就必须在对应的jar包的META-INF/services 目录创建一个名为javax.servlet.ServletContainerInitializer的文件,文件内容指定具体的ServletContainerInitializer实现类,那么,当web容器启动时就会运行这个初始化器做一些组件内的初始化工作
咋一看,是不是似曾相识呢?对的,思想同JDK的一毛一样,所以说天下技术也是一大抄嘛~~
一般伴随着ServletContainerInitializer一起使用的还有HandlesTypes注解,通过HandlesTypes可以将感兴趣的一些类注入到ServletContainerInitializerde的onStartup方法作为参数传入
这里需要说明一点:即使你类上不标注HandlesTypes注解也是ok的。onStartup方法还是会执行,只不过是public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException { ... }它的c这个属性值为null而已。
Servlet3.0规范的类
// 全类名:javax.servlet.ServletContainerInitializer // @since Servlet 3.0 public interface ServletContainerInitializer { ... }
ServletContainerInitializer 是 Servlet 3.0 新增的一个接口,主要用于在容器启动阶段通过编程风格注册Filter, Servlet以及Listener,以取代通过web.xml配置注册。这样就利于开发内聚的web应用框架.
以SpringMVC举例, servlet3.0之前我们需要在web.xml中依据Spring的规范新建一堆配置。这样就相当于将框架和容器紧耦合了。而在3.x后注册的功能内聚到Spring里,Spring-web就变成一个纯粹的即插即用的组件,不用依据应用环境定义一套新的配置。
原理
- ServletContainerInitializer接口的实现类通过java SPI声明自己是ServletContainerInitializer 的provider
- 容器启动阶段依据java spi获取到所有ServletContainerInitializer的实现类,然后执行其onStartup方法.(参考类:ContextConfig监听器)
- 另外在实现ServletContainerInitializer时还可以通过@HandlesTypes注解定义本实现类希望处理的类型,容器会将当前应用中所有这一类型(继承或者实现)的类放在ServletContainerInitializer接口的集合参数c中传递进来。如果不定义处理类型,或者应用中不存在相应的实现类,则集合参数c为null
- 这一类实现了SCI(全名:ServletContainerInitializer)的接口,如果做为独立的包发布,在打包时,会在JAR 文件的 META-INF/services/javax.servlet.ServletContainerInitializer 文件中进行注册
SCI(全称 ServletContainerInitializer)则是根据约定的标准,扫描META-INF中包含注册信息的 class 并在启动阶段调用其onStartup
以Tomcat为例,源码解释原理
为了一探究竟,我下载了Tomcat9的源码并且编译运行,然后做如下记录。
首先我们看ContextConfig,它是个LifecycleListener,在Tomcat容器启动的时候会执行~
package org.apache.catalina.startup public class ContextConfig implements LifecycleListener { ... @Override public void lifecycleEvent(LifecycleEvent event) { ... // 容器启动前,会执行configureStart配置工作 if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) { configureStart(); } ... } protected synchronized void configureStart() { ... webConfig(); } protected void webConfig() { ... // Step 3. Look for ServletContainerInitializer implementations if (ok) { processServletContainerInitializers(); } } // 这一步就是Tomcat专门寻找,并且执行Servlet规范中的`ServletContainerInitializer`的所有的实现类的 /** * Scan JARs for ServletContainerInitializer implementations. */ protected void processServletContainerInitializers() { List<ServletContainerInitializer> detectedScis; try { // 此处使用的是Tomcat自己实现的 WebappServiceLoader,它使用方式完全同java.util.ServiceLoader,并且相关规范也一样的 // 具体使用方式,请参见ServiceLoader的使用步骤 WebappServiceLoader<ServletContainerInitializer> loader = new WebappServiceLoader<>(context); // 找到所有的SCI detectedScis = loader.load(ServletContainerInitializer.class); } catch (IOException e) { ok = false; return; } // 遍历,逐个处理@HandlesTypes注解~~~ for (ServletContainerInitializer sci : detectedScis) { initializerClassMap.put(sci, new HashSet<Class<?>>()); HandlesTypes ht; try { // 拿到实现类上面标注的@HandlesTypes注解~~~ ht = sci.getClass().getAnnotation(HandlesTypes.class); } catch (Exception e) { continue; } // 如过没有标注此注解,就继续处理下一个 if (ht == null) { continue; } // 标注了注解,但是没有写value值,也继续处理下一个 Class<?>[] types = ht.value(); if (types == null) { continue; } // types:注解里面所关心的types, for (Class<?> type : types) { if (type.isAnnotation()) { handlesTypesAnnotations = true; } else { handlesTypesNonAnnotations = true; } // 以该type为key,对应上对应的SCI 最终onStartup()方法里会把对应Class类型放进去的 Set<ServletContainerInitializer> scis = typeInitializerMap.get(type); if (scis == null) { scis = new HashSet<>(); typeInitializerMap.put(type, scis); } scis.add(sci); } } } }
它的执行在StandardContext#startInternal()这个方法,启动的时候会统一调用。
org.apache.jasper.servlet.JasperInitializer就是Tomcat内部的一个初始化器,用于处理支持JSP页面的。我编译源码运行,发现并 报错:并不支持JSP,所以我只能自己手动添加了此注册器。
protected synchronized void configureStart() { ... webConfig(); // 手动添加的 context.addServletContainerInitializer(new JasperInitializer(), null); ... }
目前本人并不确定是不是Tomcat9默认是不予支持JSP页面的,因为不写页面好久了。希望有知道的可以留言告知我~~~~
Tomcat调用SCI的时机
ServletContainerInitializer的调用时机,可能在绝大部分情况下我们都不必要去了解,只需要知道它会调用就成。但是如果我们了解了回调的先后顺序,可以帮助我们快速定位和理解某些奇怪的问题(因为servlet容器除了会回调SCI之外, 还有回调诸如servlet#init方法, listener等)
这里肯定就以最常用的servlet容器:tomcat举例,回调处理步骤总结如下:
- 解析web.xml(第一还是先解析web.xml,因为必须要保持向下兼容嘛)
- 往ServletContext实例中注入<context-param> 参数
- 回调Servlet3.0的ServletContainerInitializers接口实现类
- 触发 Listener 事件(beforeContextInitialized, afterContextInitialized); 这里只会触发 ServletContextListener 类型的
- 初始化 Filter, 调用其init方法
- 加载 启动时即加载的servlet(servlet最靠后加载~~~)
SCI在Spring中的使用,热插拔的实现基石
SpringBoot 也是这样被点燃的(关于Spring Boot和Tomcat嵌入式容器的关系,后面章节很定还会详细分析)
首先看看Spring Boot中有哪些实现类:
这里面最熟悉的莫过于:SpringServletContainerInitializer。SpringServletContainerInitializer类实现了 ServletContainerInitializer接口。这意味着servlet(3.0以上版本)容器启动时,该类被容器自动加载并执行其onStart方法,这样我们就可以启动我们的Spring容器了
这其中还有有一个LogbackServletContainerInitializer我们看起来非常眼熟,Logback是通过该类进行Web环境初始化的。