1.什么是可扩展
在IT行业的发展过程中,先有传统行业,再有互联网,传统行业和 互联网是少林与武当的关系,其中的技术相辅相成,互联网技术不一定 比传统行业的技术高深很多,而是各有侧重点。传统行业更偏向于企业 级开发,项目具有业务复杂、流程完善、中心化管理、企业级抽象度 高、业务重用率高等特点;而互联网技术则倾向于把复杂的业务拆分成 单一的职责模块,并对各个模块的非功能质量进行大幅度优化,包括高 可用性、高性能、可伸缩、可扩展、安全性、稳定性、可维护性、健壮性等。
- 可伸缩性指横向扩展的能力,也就是随着节点的增加,服务能力能够随着节点增加而线性增加,如果不能,则也可以使用百分比来衡量;
- 可扩展性通常指架构上的灵活性及可插拔性,将来可以不断地在系统上叠加新业务和新功能
摘自《分布式服务架构:原理、设计与实战》
2.什么是SPI
全称 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。SPI机制是可扩展思想的一种实现。
Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制,提供了通过interface寻找implement的方法。类似于IOC的思想,将装配的控制权移到程序之外,从而实现解耦。
2.1 Java SPI约定
使用Java SPI需要符合的约定:
- Service provider提供Interface的具体实现后,在目录META-INF/services下的文件(以Interface全路径命名)中添加具体实现类的全路径名;
- 接口实现类的jar包存放在使用程序的classpath中;
- 使用程序使用ServiceLoader动态加载实现类(根据目录META-INF/services下的配置文件找到实现类的全限定名并调用classloader来加载实现类到JVM);
- SPI的实现类必须具有无参数的构造方法。
2.2 应用场景
适应场景:调用者根据需要,使用、扩展或替换实现策略。
具体使用场景
- common-logging:apache最早提供的日志的门面接口。只有接口,没有实现。具体方案由各提供商实现, 发现日志提供商是通过扫描
META-INF/services/org.apache.commons.logging.LogFactory配置文件,通过读取该文件的内容找到日志提工商实现类。只要我们的日志实现里包含了这个文件,并在文件里制定LogFactory工厂接口的实现类即可。 - JDBC:jdbc4.0以前, 开发人员还需要基于
Class.forName("xxx")的方式来装载驱动,jdbc4也基于spi的机制来发现驱动提供商了,可以通过META-INF/services/java.sql.Driver文件里指定实现类的方式来暴露驱动提供者.
2.3 自己的困惑
产生疑问的主要点在于,
- 为什么什么都没提供给ServLoader,就提供了一个接口,它就能加载所有的实现。它是这么做到的
- 类加载器与SPI机制之间的关系,为什么说Java SPI可以打破类加载机制
public static void main(String[] args) {
ServiceLoader<JDBC> load = ServiceLoader.load(JDBC.class);
for (JDBC driver : load) {
System.out.println("diverName :" + driver.getClass().getName() + " , loadDriver's Loader: " + driver.getClass().getClassLoader());
System.out.println(driver.baseURL());
}
System.out.println("当前线程上下文类加载器 : " + Thread.currentThread().getContextClassLoader());
System.out.println("加载ServiceLoader的类加载 : " + load.getClass().getClassLoader());
}
通过ServiceLoader来获取具体的实现类,执行结果如下
diverName :com.mysql.connection.MysqlJDBC , loadDriver's Loader: sun.misc.Launcher$AppClassLoader@18b4aac2
mysql....
Mysql
diverName :com.oralce.connection.OracleJDBC , loadDriver's Loader: sun.misc.Launcher$AppClassLoader@18b4aac2
oracle....
oracle
当前线程上下文类加载器 : sun.misc.Launcher$AppClassLoader@18b4aac2
加载ServiceLoader的类加载 : null
查看ServiceLoader中的load方法:
public static <S> ServiceLoader<S> load(Class<S> service) {
//获取线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
这里使用了Context Class Loader,为什么要这么使用呢,为什么不直接使用系统类加载器呢?
SPI的接口是Java核心库的一部分,是由Bootstrap Classloader来加载的;SPI的实现类是由System ClassLoader来加载的。Bootstrap Classloader是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派SystemClassLoader来加载类。而线程上下文类加载器(context classloader)破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器
java的双亲委托类加载机制(ClassLoader A -> System classloader -> Extension classloader -> Bootstrap classloader)可以保证核心类的正常安全加载。但是右边的 Bootstrap classloader 所加载的代码需要反过来去找委派链靠左边的 ClassLoader A 去加载东西的时候,就需要委派链左边的 ClassLoader 设置为线程的上下文加载器即可。
什么是线程上下文类加载器
线程上下文类加载器出现的原因
Q: 越基础的类由越上层的加载器进行加载,如果基础类又要调用回用户的代码,那该怎么办?
A: 解决方案:使用“线程上下文类加载器”
为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
到这里我想大家也可以明白为什么上面的demo
System.out.println("当前线程上下文类加载器 : " + Thread.currentThread().getContextClassLoader());\
// 输出
// 当前线程上下文类加载器 : sun.misc.Launcher$AppClassLoader@18b4aac2
有了线程上下文类加载器,也就是父类加载器请求子类加载器去完成类加载的动作(即,父类加载器加载的类,使用线程上下文加载器去加载其无法加载的类),这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则。
Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。
流程图如下:
上面的demo中,通过ServiceLoader.load()对实现类进行加载,而这份代码写在了使用方,在中间件或者模块设计中,是不合理的,那么有没有办法把这份代码放到提供方的代码模块中呢,答案是肯定是有的,JDBC就有相应的实现
在使用旧版JDBC时,我们必须首先调用类似Class.forName("com.mysql.jdbc.Driver")的方法,通过反射来手动加载数据库驱动。但是在新版JDBC中已经不用写了,只需直接调用DriverManager.getConnection()方法即可获得数据库连接。看一下java.sql.DriverManager的静态代码块中调用的loadInitialDrivers()方法的部分代码:
private static void loadInitialDrivers() {
// ......
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) { }
return null;
}
});
// ......
}
可见是利用SPI机制来获取并加载驱动提供类(java.sql.Driver接口的实现类)。以MySQL JDBC驱动为例,在其META-INF/services目录下找到名为java.sql.Driver的文件:
//com.mysql.cj.jdbc.Driver
static {
try {
// 调用DriverManager中的registerDriver()方法将自己注册到DriverManager中
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI 接口中的代码经常需要加载具体的实现类。那么问题来了,SPI 的接口是 Java 核心库的一部分,是由启动类加载器(Bootstrap Classloader)来加载的;SPI 的实现类是由系统类加载器(System ClassLoader)来加载的。引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader 无法委派 AppClassLoader 来加载类。
为了解以上问题,我们可以看到ServiceLoader中获取当前线程的ContextClassLoader ,ContextClassLoader 默认存放了 AppClassLoader 的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader 或是 ExtClassLoader 等),在任何需要的时候都可以用 Thread.currentThread().getContextClassLoader() 取出应用程序类加载器来完成需要的操作。