0%

spi和破坏双亲委派

Java SPI

Java的spi约定是在源代码目录下的META-INF/services中,新建一个文件,文件名是接口的全限定名,内容是我们想要进行动态Loader的类的全限定名。

比如我们有一个NameLog的接口

1
2
3
4
5
6
package cc.lovezhy.spi;

public interface NameLog {

void printName();
}

然后我们设定了两个实现FirstNameLogSecondNameLog

1
2
3
4
5
6
7
8
package cc.lovezhy.spi;

public class FirstNameLog implements NameLog {
@Override
public void printName() {
System.out.println("here is First");
}
}

1
2
3
4
5
6
7
8
package cc.lovezhy.spi;

public class SecondNameLog implements NameLog {
@Override
public void printName() {
System.out.println("here is second");
}
}

在源码的目录下新建META-INF/services目录
再新建一个名字为cc.lovezhy.spi.NameLog的文件
在里面写上

1
cc.lovezhy.spi.FirstNameLog

测试程序

1
2
3
4
5
6
7
8
public 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
2
here 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
9
public static void main(String[] args) throws ClassNotFoundException {
Thread thread = new Thread(new Runnable() {
@Override
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
8
public 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);
}