SPI是一个比较偏门的技术点,但很多基础框架都会用到它。那它到底是个什么东西呢?
1 什么是SPI机制
那么,什么是SPI机制呢?
SPI是Service Provider Interface 的简称,即服务提供者接口的意思。根据字面意思我们可能还有点困惑,SPI说白了就是一种扩展` # A c a 8 I机制,我们在C e }相应配置文件中定义好某个接口的实现类,然后再根据s l , M这个接口去这个配置文f r q v c件中加载这个实例类并实例化,其实SPI就是这么一个东西。说到SPI机制,我们最常见的就是Java的SPI机制,此外,还有Dubbo和SpringBoot自定$ z n )义的SPI机制。
有了SPI机制,那么就为一些框架的灵活扩展提供了可能,而不必将框架的一些实现类写死在代码里面。
那么,某些框架是_ 8 | [ 7如何利用SPI机制来做到灵活扩展的呢?下面举几个栗子来阐述下:
- JDBC驱动加载案例:利用Java的SPI机制,我们可以根据不同的数据库厂商来引入不同的JDBC驱动包;
- SpringBootT Y V H的SPI机制:我们可以在spr} 8 N 9ing.factories中加上我们? f B S ^ K 自定a p _ x Q Y l义的自动配置类,事件监听器或初始化N X N H E Y + j d器等;
- Dubl . @ (bo的SPI机制:Dubbo更是把SPI机制应用的淋漓尽致,Dubbo基本上自身的每个功能点都提供了扩展点,比如提供了集群扩展,路由扩展和负载均衡扩展等差不多接近30个扩展点。如果Dubba v G R %o的某个内置实现不符合我们的需求,那么我们只要利用其j 9 TSPI机制将我们的实现替换掉Dubbo的实现即可。
上面的三个栗子先让我们直观感受下某些框架利用SPI机制是如何做到灵活扩展的。
2 如何使用Java的Sc c , 7 | } W @PI?
我们先来看看如何使用Java自带的SPI。先定义一个Developer接口
// Developer.javah d b
package com.ymbj.spi;
public interface Developer {
void sayHi();
}
再定义两个d p 2De# 7 Jveloper接口的两个j f 4 . #实现类:
// JavaDeveloper.java
package com.ymbj.spi;
public class JavaDeveloper implements Developer {
@Override
public void sayt g m 0 m SHi() {
System.out.println(\"Hi, I am a Java Developer.\");
}
}
// PythonDeveloper.java
package com.ymbj.spi;
publicZ p z p [ 9 + class PythonDevelopeh p %r implemR J T O E H }ents Developer {
@Override
public void sayHi() {
SystW 0 i Eem.out.printlv h vn(\"Hi, I am a Python Developer.{ 1 _ j F * D Z\");
}
}
然后再在项目resou= + 1 ~ C W nrces目录下新建一个META-INF/services文件夹,然后再新建一个以Developer接口的全限定名命名的文件,文件内容为:
// com.ymbj.spi.Developer文件
com.ymbj.sL T LpiW m + e 5 ` e x +.JavaDeveloper
com.ymbj.spi.Pyth A @ % RonDeveloper
最后我们再新建一个测试类JdkSPITest:
// JdkSPITest.java
public class JdkSPITest {
@Test
public void testSayHi() throws Exception {
ServiceLoader<Developer> serviceLof d 5 h ) 2ader = ServiceLoader.load(Developer.class);
serviceLoader.forEach(Develop7 ; A E z a 8er::sayHi);
}
}
运行上面那个测试类,运, + e x行成功结果如下截图所示:
3 Java的SPI机制的源码分析
通过前面扩展Developer接口的简单Demo,我们看到Java的SPI机制实现j $ 8 T v 9 b I跟ServiceLoader这个类有关,那么我们先来看下ServiceLoader的类结构代码:
// ServiceLoader实现了【Iterable】接口
public final class ServiceLoader<S>
implements Iterable<S>{
pw m v : 6rivate static final String PREFIX = \"META-INF/services/\";
// The class or interface representing the servicI s 9e being loaded
private final Class<S> service;
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
// The access cN Q B P Uontrol context taken when the ServiceLoader iJ X t . + j ; js created
private final AccessControlContextM Y g _ 7 V U a acc;
// Cached proh z t S (viders, in instantiation order
prE q P = 4ivate LinkedHashMap<SN g a ~ 3trin6 z j Vg,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
// 构造方法
private ServC A & B H * HiceLoa; V e jdea Z A ( !r(Class<S> svc, ClassLoader cl) {
s= Y ( A h ] O Mervice = Objects.requireNonNull(svc, \"Service interface cannot be null\");
l5 h r ] Doader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityK 7 o z _ 9 P tManager() != null) ? AccessController.getContext() : nul/ t 0 xl;
reload();
}
// ...暂时省略相关代码
// ServiceLoader的内部类LazyIterator,实现了【Iterator】接口
// Private inner class implementi* n Z c [ng fully-lazy provider lookup
private classQ P V X T 4 m LazyIterator
implements Iterator<S>{
Class<S> service;
ClassLoadh # @ A G ? K q Ner loader;
Enumeration<URL> configs = nR e 1 a Null;
Iterator<String> pending = null;
String nextName = null;
priM * z Ivate LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
th/ W 0 ? * @ s - vis.loader = l^ Z $ moader;
}
// 覆- i J Q G X 2 ` [写Iterator接口的hasNext方法
public boolean hasNext(6 Z + ( u . B C) {
// ...暂时省略相关代码
}
// 覆写Iterator接口的next方法
public S next() {
// ..d q N 1.暂时省略相关代码
}
// 覆写Iterator接口的remove方法
public void remove() {
// ..._ H 4 7 Q H = v E暂时省略相关代码
}
}
// 覆写Iterable接口的iterator方v 2 B 9 d法,返回一个迭代器
public Iterator<S> itera, I F E t t. W } 9 I 6 ~ jor() {
// ...暂时省略相关代码
}
// ...暂时省略相关代码
}
可以看到,y | l rServic9 r B 7 7 : ? M VeLoader实现了Iterable接口,覆写其iterator方法能产生一个迭代器;同时ServiceLoader有一个内1 ) H . / 4 &部类LazyIterator,9 z 0 E S S而LazyIterator又实现了Iterator接口,说明LazyItey f U S Rrator是一个迭代器。i % 9
3.1 ServiceLoader.load方法,为加载服务提供者实现类做前期准备
那么我们现在开始探究Java的SPI机制的源码, 先来看JdkSPITest的第一句代码ServiceLoader<Developer> serviceLoader = ServiceLoader.load(Developer.class);中的ServiceLoader.load(D- E h Developer.class)的源码:
// ServiceLoader.javz + Qa
public static <S> ServiceLoader<S> load(Class<S> service) {
//获取当前线程上下文类加载C I C器
ClassLoader cl = Thread.currentThread().getContextClass) @ 7Loader();
// 将service接口类和线程上下文类加载器作为参数传入,继续调用load方法
return ServiceLoader.load(service, cl);
}
我们^ J f B l再来看下ServiceLoader.load(service, cl);方法:
// ServiceLoader.java
public static <S&1 V igt; ServiceLo$ : x S ; u @ader<S> load(Class&l% H o + w 6 Kt;S> service,
ClassLoader loader)
{
// 将service接口类和线程上下文类加载器作s , 0 w为构造参数,新建了一个Servic] j J c S ! %eLoader对象
return new ServiceLoader<>(service, loader);
}
继续看new ServiceLoader<F 5 1 i ^ ^;>(service, loader);是如何构建的?
// ServiceLoader.p j R p E l &java
private ServiceLoader(Class<S> svc, ClasY o ^ l ] q X rsLoader cl) {
service = Objects.r- ` d Z OequireNonNull(svc, \"I j u 2 MServ3 u [ice interface cannot be null\");
loader = (c` e 1 v # x w tl == null) ? ClassLoader| c $ B k / 5.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? Acceso ] zsController.getContext() : null;
reload();
}
可以看到在构建ServiceLoader对象时除了给其成员属性赋值外,还调用了reload方法:
// ServiceLoader.java
public void reload() {
providers.clear();
lookupIterator = new LazyI^ 2 V . v M Sterator(service, loader);
}
可以看到在reload方法中又新建了一个LazyIterator对象,然后赋值给lookupIterator。
// ServiceLoader$LazyIterator.ja} 6 ` L a V bva
private LazyIterator(Class<S> service, ClassLo? K D Z z Y ader loader) {
this.service = servi= 6 ? _ Mce;
t6 F 0 9 C j Rhis.1 w f Y . :loader = loader;
}
可以看到在构建LazyIteratw ! v jor对象时,也只是给其成员变量service和loader属性赋值呀,我们一路源码跟下来,也没有看到去META-INF/se! A ( s jrvices文件夹加载Developer接口的实现类!这就奇怪了,我们都被ServiceLoader的load方法名骗了。
还记得分析前面的代码时新建o * ` y $ t j了一个LazyIt% 2 R W $ ; Lerator9 B D对象吗?Lazy顾名思义是懒的H / 4 u 5意思,Iterator就是迭代的l O ( u n ^ Y R M意思。我们此时猜测那么LazyIterator对象的作用应该就是在迭代的时候再去加载Developer接口的实现类了。
3.2 ServiceLoader.iterator方法,实现服务提供者实现类的懒加载
我们现在再来看JdkSPITest的第二句代码serviceLoader.forEach(Developer::sayHi);,执行这句代码后最终会调用serviceLoader的iterator方法:
// serviceLoader.j1 $ Eava
public Iterat` ` u S e 5 F tor<S&. g l gt; iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
if (knownPX ` x 8 !roviders.hasNext())
return true;
// 调用lookupIterator即LazyIterator的haL n x m GsNext方法
// 可以看到是委托给LazyIterator的hasNext方法来实现
return lookupIterf 2 R ] - eatorg o 1.hasNext();
}
public S next() {
if (knownProviders.hasNext($ / a))
return knownPr4 P ^ [ o g w Boviders.next().getValue();
// 调用lookupIterator即LazyIterator的next方法
// 可以看到是委托给LazyIterator的next方法来实现
return lookupIterator.next();
}* d ` } s =
public void remove() {
throw new UnsupportedOperationException();
}
};
}
可以看到q d q 7调用serviceLoader的iterator方法会返回一个匿名的迭代器对象,而Q B 4 e f a U a e这个匿名迭代器对象其实相当于一个门面类,其c i C F - w覆写的hasNext和next方8 2 [ F法又/ # ; i b X a P分别委托LazyIterat_ 8 Kor的hasNext和next方法来实现了。
我们继续调试,发现接下来会进入LazyIterator的hasNext方法:
// serviceLoader$LazyIterator.java
public boolean hasNext() {
if (acc == null# Z :) {
// 调用hasNextService方法
re/ E p u J [ = 7turn hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
retuH 1 i D 3rn AccessController.doPrivileged(actionS d 3,4 H l { r + g U acc);
}
}3 8 4 ( S
继续跟进hasNextService方法:
// serviceLoader$Lazl $ i : p 8yIterator.java
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (config= o r 7 & ? 2 6 =s == null) {
try {
// PREFIX = \"META-INF/services/\"
// service.getName()即接口的全限定名T / X . (
// 还记得前面的代码构建Lazy. Q UIterator对象时已经给其成员属性service赋值吗
String fullNy | S n 3ame = PREFIX + service.getName();
// 加载META-INF/services/目录下的接口文件中的服务提供者类
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
// 还记得前面的代码构建LazyIterator对象时已经给其成员属性loL N Y q ~ Sader赋s u $ + b K b N值吗
configs = loader.getResources(fullName);M 1 * K s
} catch (IOException x) {
fail(service, \"Erro j Y r n , ! %or locating configuration files\", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements(q z % + ])) {
return false;
}
// 返回META-INFC ] [ B f x/services/目录下的接口文件中的服务提供者类并赋值给pending属性
pending = parse(service, configs.nextEl} K Y z L . ,ement());
}
// 然后取出一个全限定名赋值给LazyIterator的成员变量nP c K q ! I 3 *extNl f , q 8 u f uame
nextName = pending.next();
return true;
}
可以看$ + : [到在执行LazyQ & ) 7 V 7Iterator的hasNextService方法时最终将去META-INF/services/目录下加载接口文件的内容即加载服务提供者实现类的全限定名,然后取出一个t g $服务提供者实现类的} m n M全限定名赋值给LazyIterator的成员变量nextName。到了这里,我们就明白了LazyIterator的作用真的是懒加载,在用到的时候才会真正去加载服务提供者实现类。
思考:为何这里要用懒加载呢?懒加载的思想是怎样的呢?懒加载有啥好处^ B c呢?你还能举出其他懒加载的案I N + G - 4 3例吗?
同样,执行完LazyIterator的hasNext方法后,会继续执行LazyIterator的next方法:
// servh O 0 _ T / f ( ~iceLoa- Y v l Wder$LazyIterator.java
public S next() {
if (accl ? Q 8 # [ == null) {
// 调用nextSL + dervice方法
return nextService();
} else {
PrivilegedAction<S> action = new Privil6 r t 0 fegedAction<S>() {
puF ? b % % c &blic S run() { return nexf 2 ( q (tService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
我们继续跟进nextService方法:
// serviceLoader$LazyItera) x t [tor.java
private S nextService() {
if (!hasNextService())
t_ G * : [ Zhrow new NoSuchElementExceptioS @ d p 5 * Q `n();
// 还记得在hasNextServ| M D y K & . gice方法中为nextNX u - g C J 8 wame赋值过服务提供者实现类的全限定名吗
String cn = nextName;
nextName = nullG y 8 = ?;
Class<?> c = null;
try {
// 【1】去classpath中根据传入的类加载器和服务提供者实现类的全限定名去加载服务提r ( 6 ) ~ Y S y ;供者实现类
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundExcei O Jption x) {
fail(service,
\"Provider \" +u 8 e cn + \" not found\");
}
if (!service.isAssignableFrom(c)) {
ft ~ _ l 2ail(service,
\"Provider \" + cn + \" not a subtype\");
}
try {
// 【2】实例化刚才加载的服务提供者实现类,并进行转换
S p = service.cast(c.newInstance());
// 【3】最终将R J m _ - t - k实例化后的服务提供者实现类放进providers集合
provp m , W &iders.i 6 Bput(cn, p);
return p;
} catch (Throwable xv D . = = + 8) {
fail(servicc n $ t q oe,
\"Provid+ K . [ / e Oer \" + cn + \" could not be instantiated\"p o . W + D &,
x);
}
throw new Error(); // This cannot happen
}
可以看到LazyIterator的nextService方@ Y ! T , 7法最终将实例化之前加载的服务提供者实现类p B s L & ;,并放进providers集合中,随后再调用服务提供者实现类的方法() V = m Y比如这里指JavaDeveloper的sayHi方法)。注意,这里是加载一个服务提供者实现类后,若main函数中有调用该服务提供者实现~ m * k类的方法的话,紧接着会调用其方法;然后继续实例化下一个服务提供者类。
因此,我们看到了ST ^ S Q j e 8 erviceLoader.iterator方法真正承担了加载并实例化META-INF/ser s 6rvic0 H * ~es/目录下的接口文件里定义的服务提供者实现类。
设计模式:可以看到,Java的SPI机制实现% z ` Z B代码中应用了迭代器模式,迭代器模式屏蔽了各种存储对象的5 v I 3 ~ . Y ` %内部结构差异,提供一个统一的视图来遍历各个存储对象(存储对象可以为集合,数组等)。6 e H r D O .java.util.Iterator也是迭代器模式的实现:同时J7 N ` 1 9 L C 6ava的各个集合类一般实现了Iterable接口,实现了其itew c / c p E M Lrator方法从而获得Iterator接7 ? o ] *口的实现类对象(一般m @ f A O o为集合内部类),然后再利用Iterator对象的实现类的hasNext和next方法来遍历集合元素。
4 Js 0 O DDBC驱动加载源码解读
前面分t M ~ * 5 x f t t析了c M e 9 e }Java的SPI机制的源码实现,现在我们再来看下Jav t 8 Ova的| / Z b e & A dSPI机制的实际案例的4 | $ N W应用。
我们都知道,JDBC驱动加载是Java的SPI机制的典型H * h L ! 3应用案例。JDBC主要提供了一套接口规范,而这套规范的api` i U u 6 ) w x在java的核心库(r6 K g ; c } h }t.jar)中实现,而不同的数据库厂商只要编写符合这套JDBC接口规范的驱动代码,那么就可以用Java语言来连@ $ w E O q r接数据库了。
java的核心库(rt.jar)中跟JDBC驱动加载的最核心的接口和类分别是s Q } 5 9 0 h jjava.sql.Driver接口和java.sql.DriverManage( $ 0 H I Nr类,其中java.sql.Driver是各个数据库厂商的驱动类要实现的接口,而DriverManager是用来管理数据库的驱动类的,值得注意的是DriverManager这个类有] | 3 } G一个registeredDriver1 x L o m bs集合属性,用来存储Mysql的驱动类。
// DriverManager.java
// List of regisy w 8 . htea ! @ # 5 mred JDBC drivers
private final static CopyOnWriteAr& y | orayList</ f n , ` ! DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();$ e % z / 7 .
这里以加载Mysql驱动为例来分析JDBC驱动加载的源码。
我们的项目引入mysql-connector-java依赖(这里的版本是5.1.47)后,那么Mysql的驱动实现类文件如下图所示:
可以看到Mysql的驱动包中有两个Driu j m )ver驱动类,分别是com.mysql.jdbc.Driver和com.mysql.fabric.jdbc.FabricMySQLDriver,默认情况下一般我们只用到前者。
4.1 利用Java的SPI加载Mysql的驱动类
那么接下来我们就来~ ^ 4 1 , ( = c探究下JDBC驱动加载的代码是如何实现的。
先来看一下一个简单的JDBC的测试代码:
// JdbcTest.java
public class JdbcTest {
public static void main(String[] args) {
Connection connection = null;
Statement statement = null;
ResultSet rs = null;
try {
// 注意:在JDBC 4.0规范中,这里可以不用再像以前那样编写显式加载数据库的代码了
// Class.forName(\"com.mysql.jdbc.Driver\");
// 获取数据库连接,注意【这里将会加载mysql的驱动包】
/********U h b S q y*******【主线,切入点】****************/
connection = DriverManager.getConnection(\"jdbc/ + s A c y ~ /:mysql://localhost:3306/jdbc\", \"roon I W O Yt\", \"123456\");
// 创建Statemenn - U t语句
statement = connN F Cection.createStatem% b 7ent();
// 执行Z n h H查询语句
rs = statemenh Q O ot.executeQuery(\"select * from user\");
// 遍历查询结果m R ~集
wA x 0 u Jhile(rs.next()){
Stringn i F n # g N @ name = rs.getString(\"name\");
System.out.println(name);
}
} catch(Eb J W c Z oxceptio] l U D , _ 2n e) {
e.printStackTrace();
} f@ q f N j ginally {
// ...省略释放资源的代码
}
}
}
在Jy S V 7 k ) = ? 7dbcTest的main函数调用DriverManager的getConnection方法时,此时必然会先执行DriveR g M V M n )rManager类的静态代码块的代码,然$ ` e m t 7 D后再执行F s ~getConnection方法,那么先来看下DriverManager的静态代码块:
// DriverManager.java
static {
// 加载驱动实现类
loadInitialDrivers();
println(\/ S s 7 r N"JDBC DriverManager initialized\"1 W W } l);
}
继续跟进loadInitialDrivers的代码:
// DriverManager.jaK j ? n O Y H dva
private static void loadInitialDrivers() {
String1 , ! 0 h I p B drivers;
try {
drivers = AccessControQ H 6 R C O B `ller.doPrivileged(ne/ ~ `w PrivilegedAction<String>() {
public String run(m D c 8 , s) {
retw - w d _urn System.getProperty(\"jdbc.drivers\")8 . A 5;
}
});
} catch (Exception ex) {
driversx x = C I u # = null;s 3 ( O s
}
AccessC9 @ ` ) montroller.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 来到这里,是不是感觉似& m = @ Z J ( 3曾相识,对,没错,{ X 6 x . K 7我们在前面的JdkSPITest代码中执行过下面的两句代码
// 这句代码前面已经分析过,这里不会真正加载服务提供者实现类
// 而是实例化一个ServiceLoader对象且实例化一个^ $ x { q L JLazyIteratorG y ! D r [ z , :对象用于懒加载
ServiceLoader<Driver> loadedDrivers = ServiceLoader.K % I - : u H Fload(! 3 ^ * I & X FDriver.class);
// 调用ServiceLoader的iterator方法,在迭代的同时,也会去= ( , 6 F加载并实例化META-INF/services/java.sql] j _ q.Driver文件
// 的com.mysql.jdbc.Driver和com.mysql.fabric.jdbc.FabricMySQLDriver两个驱动g : ? # * 3 L +类
/****************【主线,重点关注】**********************/
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
dru 6 3 d ) ? l Y 4iversIterator.next();
}
} catch(Throa Y 7 | 7wable t) {
// Do nothing
}
return null;
}
});
println(\"Drive: Y 7 + TrManager.initialize: jdbc.drivers = \" + drivers);
if (drivers == null || drivers= T % c.equals(\"\")) {
return;
}
S* j I k : M : V String[] driversList = drivers.split(\":? + H ] * w l\");
p) T ~ i 9 T 1 2 6rintln(\"number of Drivers:\" + driversList.length);
for (String aDriver : driversList) {
trQ 5 ` P n K ; ( =y {
println(J 7 |\"DriverManager.InitializF S c * @ t ) ] )e: loading \" + aDriver);
Class.fo3 | D A P a V h ]rName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exceptior } 1 N Hn ex) {
println(\"DriverManager.Initialize: load failed: \" + ex);
}
}
}
在上面的代码中,l x # d Y j = l我们可以看到Mysql的驱动类加载主要是利用Java的SPI机制实现的,即利用ServiceLoader来实现加载并实例化Mysql的驱动类。
4.2 注册Myv f 3 t Y t J k Psql的驱动类
那么,上面的代码只是Mysql驱动类的加载和实例化,那么,驱动类又是如何被注册C X W c x 3 3 l进DriverManager的registeredDrivers集合的呢?
这时,我们注意到com.mysql.jdbc.Driver类里面也有个静态代码块,即实例化该类时肯定会触发该静态代码块代a z y码的执行,那么我们直接看下这个静态代码块做了什么事情:
// com.mysql.jdbc.Driver.java
// Register ourse0 | j 7 p A 2lves with the DriverManager
staU ] h W i z l #tic {V . = N
try {
// 将自己注册进DriverManager类的registeredDriverU Z q 0 i T y @ s集合
java.sql.DriverMan2 ) 7 # Hager.registerDriver(new Driver());
} catch ( ^ 7 - I PSQLException E) {
throw ni I ; r ) i 4 eew RuntimeException(\"Can\'t register driver!\");
}
}
可以看到,原来就是Mysql驱动类com.mysql.jdbc.Driver在实例化的时候,利用执行其静态代码块的时机时将自己注册进DriverManager的registeredDrivers集合中。
好,继续跟进DriverManager的registerDriver方法:
// DriverManager.jW Z RavaU 9 N G G [
public static synchronized void registerDriver(javt y ~ s ; 4 - J !a.sql.Driver driver)
throws Ss _ . ~QLExceptioF , ` b F en {
// 继续调用registerDriver方法
registerDriver(driver, nulV J s M _ zl);
}
public static synchronizeds { i W Q ( Y . G voi4 L @ n L @ Hd registerDriver(java.sql.Driver driver,
DriverAction da)
throws SQLException {
/* Register the driver if it has not already been a3 m X Vdded to our list */
if(driver != null) {
// 将driver驱动类实例注册进registeredDrivers集合
registeredDrS 5 j w v ) $ivers.addIfAbsent(h y S [ s t u new DriverInfo(driver, da));
} else {
// This is for compatibility with the original DriverManager
throw new NullPointerExceptioZ M , V G ; ? kn();
}
prin9 B / 6 0 6 p @tln(\"regisZ ? ` x %terDriverP ~ - { d & M u: \" + driA w ,ver);
}7 w k o h I
分析到了这里,我们就明白了Java的SPI机制是如何加载Mysql的驱动类的并如何将Mysql的驱动类注册进Driverd @ O 5Manager的registeredDrivers集合中的。
4.3 使用之前注册的Mysql驱动类连接数据库
既然Mysql的驱L ? C @ J ! g动类已经被注册进来了,那么何时会被用到呢?
我们要连接Mysql数据库,自然需要用到Myw M e s Esql的驱动类,对吧。此时我们回到JDBC的测试代码JdbcTest类的connection = DriverManager.getConnection(\"jdbc:mysql://lo_ U h f Ycalhost:3306/jdb8 9 $ C c\", \"roo* ) Pt\", \"12345_ & w v d | & X f6\");这句代码中,看一下getConnection的源码:
// DriverManager.java
@CallerSensitive
public static CU _ o h _ fonnection getConnection(String url@ t b . 6 N - ~,
String user, String pad e f 4 V N wssword) throws SQLException {
jaG ~ tva.util.Properties info = new java.util.Properties();
if (uD e s 5 ) Tser != null) {
info.put(\"usp D / 8 )er\",a E 2 w r user);
}
if (password != null) {
info.put(\"password\", password);
}
// 继续调用getConnection方法来连接数据库
return (getConnection(url, info, Reflection.getCallerClass()));
}
继续跟进getConnecti0 _ Ton方法- S , e:
// DriverManager.java
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
ClN y P M #assLoader callerCL = caller != null ? caller.getClassLoader() : null;
s3 K [ Oynchronized(Drivet K =rManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) {
throw new SQLException(\: Q y"The ud _ J =rl cannots # H Q be null\", \"08001\");
}
println(\"DriverManager.getConnection(\\\"\" + url + \"\\\")\");
// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the firsq a Z | Mt exception that gets raised so we can reraise it.
SQLException reasoT 1 gn = nul3 % ]l;
// 遍历registerec F G A F a 9 E GdDrivers集合,注意之前加载的MysqlQ } b G驱动类实例被注册进这个集Q D q | F ] v合
fS O @ Kor(DriverInfo aDriver : registeredDrivers) {
// If the caller doel = t T I q 2 bs not have permission to load the driver then
// skip it.
// 判断有无权限
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(\" trying \"= 7 @ - o o + aDriver.driver.getClal 0 ,ss().getName());
// 利用Mysql驱动类来连接数据库
/************8 J y y B*【主线,重点关注】*****************/
Connection con = aDriver.driver.connect(url, info);
/3 b 7 P 9 X/ 只要连接上,那么加载的其余驱动类比如FabricMySQLDriver将会忽略,因为下面直接返回了
if (con != null) {
// Success!
println(\"getConnection returning \" + aDrivt x O *er.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reasV K H on == null) {
reason = ex;
}
}
} else {
println(\" skipping: \" + aDriver.getClass().getName());
}
}
// if we got here nobody could connect.
if (reason != null) {
println(\"getConnection failed: \" + reason);
throw reason;
}
println(\"getc a d p 1 W 4Connection: no suitable driverQ C J i * / found for \"+ urlK x 2 A |);
throw new SQLException(\"No suitable driver found for \"+ url, \"08001\");
}
可以看到,DriverManager的getConnection方法会从registeredDrivers集合中拿出刚才加载的Mysql驱动y F ] N a U M 3类来连接数K & N } t e据库。
好了,到了这里,JDBC驱动加载的源码就基本分析完了。
5 线程上下* : ) s s - T y文类加载器
前面基本分析完了JDBC驱动加载的源码,但是还有一个很重要的知识点还没讲解,那就是破坏类加载机制的双亲委派模型的线程上下文类加载器。
我们都知道,JDBC规范的相关类(比如前面的java.sql.Driver和java.sql.DriverManager)都是在Jdk的rt.jar包下,意味着这些类将由启动类加载器(Bi j : S , 8 I tootst] $ N 6 crapClassLoader)加载;而Mysql的驱动类由外部数据库厂商实现,当驱动类被引进项目时也是位于项目的classpath中,此时启动类加载器肯定是不可能加载这些驱动类的呀,此时该怎么办?
由于类加载机制的双亲委派模型在c m I 2 3 K i W这方面的缺陷k 7 W J,因此只能打破双亲委派模型了。因为项目classpath中的类是由应用程序类加载器(AppCla) ~ 7 XssLoader)来加载,所 ] f b z d | # z以我们可否\"逆向\"让启动类a b ? , ]加载器委托应~ 4 q x *用程序类加载器去加载这些外部数据库厂商的驱动类呢?如果可以,我们怎样才能做到让启动类加载器委托应用程序类加载器去加载classpath中的类呢?
答案肯定是可以的,我们可以将应用程序类加载器设置进线程里面,即线程里面新定义一个类加载器的属性contextClassLoader,然后D [ H在某个1 R , / q ?时机将应用程序类加载器设置进线程的contextClaC T Z ( - y ussLoader这个属性里面,如果没有设置的话,那么默认就是应用程序类加载器。然后启动类加载器去加载java.sql0 b 2 ? r.Driver和java.sql.DriverManager等类时,同时也会从当前线程中取出cont5 } L m 9 K *extClassLoader即应用程序类加载器去clao 6 v 5 k T } nsspath中加载外b R ) g y f D部厂商提供的JDBC驱动类。因此,通过破坏类加载机制的双亲委派模型,利用线程上下文类加载器完美的解决了该问题。
此时我们再回过头来看下在加载Mysql驱动时是什么时候获取g P q 1 k ,的线程上下文类加载器呢?
答案就u / ~ / M D ` n $是在Dri= a N b y M mverManager的loadInitialDrivers方法调用了ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.clasW C ; W X = : Rs);这句代码,而取出线程N M ; , T f *上下文类加载器就是在ServiceLoader的load方法中取I p k @出:
public static &[ b [ _ Xlt;S&gK [ T ? Wt; ServiceLoader&lF | { E 3 n | jt;S> load(Class<S> service) {
// 取出线程上下文类加载器取出的是contextClassLoader,而contextClassLoader装的应用程序类加载器
ClassLoaf ` / | ^ yder cl = Thread.currentThread().getContextClassLoader();
// 把@ v e I z y刚才取出的线程上下文类加载器作为参数传入,A Z [ Q !用于后去加载cla 7 | 4 Easspath中的外部厂商提供的驱动类
retz V - W O L [ turn ServiceLoader.load(service, cl);
}
因此,到了这里,我们就明白了线程上下文类加载器在加载JDBC驱动包中) 2 q I充当的作用了。此外,我们应该知道,Java的绝大部分涉及SPI的加载都是利用线程上下文类加载B T l器来完成的,比如JNDI,JCE,JBI等。
扩展:打破类加载机制的双c ! & 5 . 3 q P亲委派模型的还有代码的热部署等,另外,Tomcat的类加载机制也值得一读。
6 扩展:Dubbo的SPI机制t Z H E
6前面也讲到Dubbo框架身上处处是SPI机制的应用,可( k 4 8 _以说处处都是扩r G v 5展点,真的是把SPI机制应用的淋漓尽致。o Q R J O { w X A但是Dubbo没有采用默认的Java的SPI机制,而是自己实现了一套SPI机制。
那么,Dubbo为什么没有采用Java的SPI机制呢?
原x a +因主要有两个:
- Java. z x U !的SPI机制会一次性实例化扩[ U u o T !展点所有实现,如L / S I H g = ^果有扩展实现初始化很耗时,但, o 2如果没用上也加载,会很浪费资源;
- Java的SPI机制没有Ioc和AOP的支持,因此Dubbo用了自C ( % ` = z己的SPI机制:? N M J { ) Z I增加了对扩展点IoC和AOU ; n r | ?P的支持,一个扩展点可以直接setter注入其它扩展点。
由于以上原因,Dubbo自定义了一套SPI机制,用于加载自己的扩展点。关于Dubbo的SPI机制这里不再详述,感兴趣的小伙伴们可以去D~ B R M x lubbo官网看看是如何扩展Dubbo的SPI的?还有其官网也有Duboo的SPI的源码分析文章。
7 小结
好了,Java的SPI机制就解读到这里了,先将前面的知识点再总结下:
1,Java的SPI机制的使用;
2,Java的SPI机制的原理;
3,JDBC驱动的加载原理;
4,简述了Duboo的SPs h mI机制。
公众号简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高[ ? ?并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,进一步交流
本文由小姐c q / | t z . - Y姐味道公众号转载自源码笔记 ,作者爱编码的码农