Java SPI
Java的spi约定是在源代码目录下的META-INF/services
中,新建一个文件,文件名是接口的全限定名,内容是我们想要进行动态Loader的类的全限定名。
比如我们有一个NameLog的接口
1 | package cc.lovezhy.spi; |
然后我们设定了两个实现FirstNameLog
和SecondNameLog
1
2
3
4
5
6
7
8package cc.lovezhy.spi;
public class FirstNameLog implements NameLog {
public void printName() {
System.out.println("here is First");
}
}
1 | package cc.lovezhy.spi; |
在源码的目录下新建META-INF/services
目录
再新建一个名字为cc.lovezhy.spi.NameLog
的文件
在里面写上1
cc.lovezhy.spi.FirstNameLog
测试程序1
2
3
4
5
6
7
8public class Main {
public static void main(String[] args) {
ServiceLoader<NameLog> services = ServiceLoader.load(NameLog.class);
for (NameLog nameLog : services) {
nameLog.printName();
}
}
}
结果:here is First
一样的,如果我们改成1
cc.lovezhy.spi.SecondNameLog
那么就是打印出here is second
我们也可以同时写上
就会两个都拿到1
2here is First
here is second
我一开始想的是运行时的实现,后来发现不是,运行时改了时候似乎并没有重新Load
在最近看的pigeon的源代码中其实也看到了很多这样的用法。
经典用法莫过于经常提到的jdbc
驱动了,我们只要引入了数据库的jar包,就可以自动找到数据库驱动。
破坏双亲委派
这也是意外在某个文章里看到的,之前看Java虚拟机书的时候也看到了,但是没太注意。
双亲委派
jvm的类加载机制,最经典的就是双亲委派机制。
jvm类加载器,系统中一般有三个
- BootstrapClassloader
这个是虚拟机层次的,对用户是不可见的,lang包中的类一般是由这个加载器加载。如果我们输出String.class.getClassLoader()
会得到null。 - ExtClassloader 一般是
JAVA_HOME/jre/lib/ext/
目录下的 - AppClassloader 一般我们自定义的类都是由这个加载器加载。
JVM中类确认需要两个条件
- 加载它的类加载器
- 它的全路径名
也就是说,及时两个类类名一样,但是加载它们的类加载器不同,那么还是两个类。
双亲委派就是一个加载器加载一个类的时候,会先让父类加载,如果父类加载不了,才自己加载。
这样就可以保证这个类在虚拟机中不被重复加载,还有唯一性。
还有一个规则,如果一个类引用了另外一个类,那么这两个类的类加载器就必须是一样的。
那么问题就来了,BootStrapClassLoader
并不能认识我们的实现的第三方代码。那怎么办呢?
如果我们看ClassLoader的类加载器,会发现它是null的,也就是它是由BootstrapClassloader
加载的。
但是再去看我们那些实现类的类加载器,会发现它们是由AppClassloader
加载的。
这是怎么做到的呢,这里需要引入线程上下文加载器。
我们可以通过这样得到线程上下文类加载器。1
2
3
4
5
6
7
8
9public static void main(String[] args) throws ClassNotFoundException {
Thread thread = new Thread(new Runnable() {
public void run() {
System.out.println("hello");
}
});
System.out.println(thread.getContextClassLoader());
}
输出结果为sun.misc.Launcher$AppClassLoader@6b482747
由此可见,虽然Thread
的类加载器为BootstrapClassloader
加载的,但是它的上下文类加载器却是AppClassLoader
。
从ServiceLoader
源码中也可以看到1
2
3
4
5
6
7
8public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader){
return new ServiceLoader<>(service, loader);
}