【小家Spring】由Spring注解驱动开发引发的疑问:ServletContainerInitializer加载机制,以及ServiceLoader的使用(以JDBC为例介绍SPI)
每篇一句
想要变强,首先要站在强者中间。在巨人的肩膀上,站得更高,看得更远
相关阅读
【小家Java】从原理层面理解Java中的类加载器:ClassLoader、双亲委派模型、线程上下文类加载器
【小家Spring】Spring注解驱动开发—Servlet 3.0整合Spring MVC(不使用web.xml部署描述符,使用ServletContainerInitializer)
前言
在之前的一篇文章【小家Spring】Spring注解驱动开发—Servlet 3.0整合Spring MVC(不使用web.xml部署描述符,使用ServletContainerInitializer)
它介绍了基于注解驱动的Servlet容器
的启动。今天刚好回头看到了自己写的这篇文章,自己心里就萌生了几个疑问:原理是啥?为何就能自动的这么样执行呢?通过配置文件就能加载类这肯定涉及到类加载机制吧?
带着这些疑问,就决定深究一番,然后做出如下记录,供读者们参考哈~~~
ServiceLoader
:服务提供者加载器
SPI概念介绍
SPI:Service Provider Interfaces
(服务提供者接口)。正如从SPI的名字去理解SPI就是Service提供者接口
SPI定位:给服务提供厂商
与扩展框架功能
的开发者使用的接口。
比如大名鼎鼎的JDBC驱动,Java只提供了
java.sql.Driver
这个SPI接口,具体的实现由各服务提供厂商(比如MySql、Oracle等)去提供。Mysql的驱动实现类为:com.mysql.jdbc.Driver
,Oracle的驱动实现类为:oracle.jdbc.driver.OracleDriver
,PostgreSQL 的为:org.postgresql.Driver
…
ServiceLoader
首先我们简单的看看javadoc和源码字段说明
/**
* 一个简单的服务提供商加载设施
* 服务 是一个熟知的接口和类(通常为抽象类)集合。服务提供者 是服务的特定实现
* 服务提供者可以以扩展的形式安装在 **Java 平台的实现中**.也就是将 jar 文件放入任意常用的扩展目录中
* 也可通过将提供者加入应用程序类路径,或者通过其他某些特定于平台的方式使其可用(所以并不限定你的方式,不在类路径也无所谓哟)
*/
// @since 1.6 Java6以后才有的工具类
public final class ServiceLoader<S> implements Iterable<S> {
// 这个路径非常的重要,最终就是去此路径读取
private static final String PREFIX = "META-INF/services/";
// 指向对象类型的 Class<S> 对象 最后通过 Class<S> 对象来构造服务实现类 S 的实例 s
private final Class<S> service;
//类加载器 ClassLoader
private final ClassLoader loader;
private final AccessControlContext acc;
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
private LazyIterator lookupIterator;
...
}
ServiceLoader与ClassLoader
ServiceLoader
与ClassLoader
是Java中2个即相互区别又相互联系的加载器
。(ServiceLoader
是一种加载类的规范,底层还是依赖于ClassLoader
的)
JVM利用ClassLoader将类载入内存,这是一个类生命周期的第一步。(一个java类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五个阶段,当然也有在加载或者连接之后没有被初始化就直接被使用的情况。 Tips:初始化
:给类中的静态变量赋予正确的初始值)
ServiceLoader:
上面JavaDoc里已经有了一些概念性的说明。可以说它的使用是非常松散的,没有太多的强制要求。 唯一强制要求的是,提供者类(实现类)
必须具有不带参数的构造方法,以便它们可以在加载中被实例化(因此,子接口肯定不行
(下面Demo会验证))
ServiceLoader位于
java.util
包,ClassLoader位于java.lang
包。因此可议看出它俩的定位也是不一样的,ServiceLoader
被认为是一种工具
我们可以简单的认为:ServiceLoader也像ClassLoader一样,能装载类文件,但是使用时有区别,具体区别如下:
- ServiceLoader装载的是一系列有某种共同特征的实现类,而ClassLoader是个万能加载器;
- ServiceLoader装载时需要特殊的配置,使用时也与ClassLoader有所区别;
- ServiceLoader还实现了Iterator接口。
ServiceLoader使用方式
毕竟光说不练假把式
我从这个问题的抛出,然后逐步讲解:
一般我们使用接口的实现类都是静态new一个实现类赋值给接口引用,如下:
HelloService service = new HelloImpl();
如果需要动态的
获取一个接口的实现类呢?
全局扫描全部的Class,然后判断是否实现了某个接口?代价太大,相信没人愿意去这么做吧。
一种合适的方式就是使用配置文件,把实现类名
配置在某个地方,然后读取这个配置文件,获取实现类名。而JDK为我们提供的工具ServiceLoader
就是采用的这种方式
(该思想其实在Spring体系内,存在大量的使用,并且我觉得比JDK做得还好~~),当然JDK的好处是:它是规范,更容易广而周知,通用性更强
ServiceLoader它的使用方式可列为4个步骤:
- 创建一个接口文件
- 在resources资源目录下创建META-INF/services文件夹
- 在上面services文件夹中创建文件:以接口全类名命名
- 在该文件内,写好实现类的全类名们
使用Demo如下:
// SPI服务接口
public interface IService {
String sayHello();
String getScheme();
}
// 主要为了测试,看看是子接口是否会被加载
public interface MyIService extends IService {
}
准备实现类(两个),模拟服务提供商:
// 服务提供商的具体实现1:HDFS实现
public class HDFSService implements IService {
@Override
public String sayHello() {
return "Hello HDFSService";
}
@Override
public String getScheme() {
return "hdfs";
}
}
// 服务提供商的具体实现2:Local实现
public class LocalService implements IService {
@Override
public String sayHello() {
return "Hello LocalService";
}
@Override
public String getScheme() {
return "local";
}
}
准备一个服务的配置文件:META-INF/services/com.fsx.maintest.IService
,然后在该文件里书写上实现类们,内容如下:
com.fsx.serviceloader.MyIService // 注意这个是接口
com.fsx.serviceloader.HDFSService
com.fsx.serviceloader.LocalService
main函数测试:
public static void main(String[] args) {
// 加载IService下所有的服务
ServiceLoader<IService> serviceLoader = ServiceLoader.load(IService.class);
for (IService service : serviceLoader) {
System.out.println(service.getScheme() + "=" + service.sayHello());
}
}
报错:
java.util.ServiceConfigurationError: com.fsx.serviceloader.IService: Provider com.fsx.serviceloader.MyIService could not be instantiated
很显然写了一个接口,而接口是不能够实例化的。注意到上面说了唯一一个强制要求,就是必须能够实例化(有空的构造函数) 因此做修改如下(只写实现类):
com.fsx.serviceloader.HDFSService
com.fsx.serviceloader.LocalService
运行正常,输出如下:
hdfs=Hello HDFSService
local=Hello LocalService
可以看到ServiceLoader可以根据IService把定义的两个实现类找出来,返回一个ServiceLoader的实现,而ServiceLoader实现了Iterable接口,所以可以通过ServiceLoader来遍历所有在配置文件中定义的类的实例。
几个注意事项:
1、文件名称是服务接口类型的完全限定
2、文件内若有多个实现类,每行一个(末尾不要有空格)
3、文件必须使用 UTF-8 编码
另外ServiceLoader
拿实例提供者是有缓存的,策略如下:
1、服务加载器维护到目前为止已经加载的提供者缓存
2、每次调用 iterator 方法返回一个迭代器,它首先按照实例化顺序生成缓存的所有元素
3、然后以延迟方式查找和实例化所有剩余的提供者,并且依次将每个提供者添加到缓存
4、若清除缓存,可议调用ServiceLoader.reload()
方法
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调用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环境初始化
的。
知识交流
若群二维码失效,请加微信号(或者扫描下方二维码):fsx641385712。
并且备注:“java入群” 字样,会手动邀请入群