类加载机制
在编译成的Class文件最终都要加载到虚拟机中才能被运行和使用,Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。
对于初始化阶段,《Java虚拟机规范》严格规定了只有六种情况必须立即对类进行“初始化”:
- 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶生成这四条指令的典型Java代码场景有:
- 使用new关键字实例化对象的时候
- 读取或设置一个类型的静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外)的时候
- 调用一个类型的静态方法
- 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化
- 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类
- 当使用JDK7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化则需要先触发其初始化。
- 当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接要在其之前被初始化
下面给个例子说明初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public static void main(String[]args){ System.out.println(SuperClass.value); }
public class SuperClass { static { System.out.println("SuperClass init!"); }
public static int value = 123; }
public class SubClass extends SuperClass { static { System.out.println("SubClass init!"); } }
output:
SuperClass init! 123
|
可以看到“SubClass init” 并没有输出,对于静态字段,只有直接定义这个字段才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
这里列出所有初始化的情况,是因为发现实际项目中有一段dubbo的静态代码块没有执行而引发的疑问:dubbo的AbstractConfig里有关于shutdown的静态代码块,因为我发现我们服务在terminating的时候,暴露的服务并没有在注册中心下线,于是就看我们用的dubbo2.6.2的版本下是如何优雅下线的:
1 2 3 4 5 6 7 8 9 10 11
| static { Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { @Override public void run() { if (logger.isInfoEnabled()) { logger.info("Run shutdown hook now."); } ProtocolConfig.destroyAll(); } }, "DubboShutdownHook")); }
|
于是在本地测试,发现是会执行,最后发现外面还有一层pandora,刚开始对pandora并不是很了解,最后认真了解了一下pandora解释,它其实是做到类隔离容器,当我加上-XX:+TraceClassLoading,发现它并不是直接用dubbo2.6.2来加载类数据,从而不会执行这段静态代码块,从这里开始,又把我的视线拉到pandora上,为什么要使用pandora呢,于是就引出了本篇主题----类加载
类加载器
Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段。对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。也就是说,两个类是否“相等”,就是来自于同一个class文件同时是被同一类加载器加载的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public class ClassLoaderTest { public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { try { String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"; InputStream is = getClass().getResourceAsStream(fileName); if (is == null) { return super.loadClass(name); }
byte[] b = new byte[is.available()]; is.read(b); return defineClass(name, b, 0, b.length); } catch (Exception e) { throw new ClassNotFoundException(name); } } };
Object obj = myLoader.loadClass("org.demo.test.ClassLoaderTest") .newInstance();
System.out.println(obj.getClass()); System.out.println(obj instanceof org.demo.test.ClassLoaderTest);
}
}
|
运行结果:
org.demo.test.ClassLoaderTest
false
可以看出虽然来自同一个文件,但在Java虚拟机中仍然是两个互相独立的类。
双亲委派模型
站在Java虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++实现,是虚拟机自身的一部分;另外一种就是其他所有的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
本文讨论的是JDK8及之前版本的Java,其一直保持着三层类加载器、双亲委派的类加载架构,不过在JDK9模块化后,有一些调整。不过本文会关注在三层类加载器:
- 启动类加载器
这个类加载器负责加载存放在<JAVA_HOME>\lib 目录,或者被-Xbootclasspath参数指定的路径中存放的,而且是被Java虚拟机能够识别的(rt.jar、tools.jar)类库加载。
- 扩展类加载器
这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。根据“扩展类加载器”这个名称,就可以推断出这是一种Java系统类库的扩展机制,JDK的开发团队允许用户将具有通用性的类库放置在ext目录里以扩展Java SE的功能,在JDK9之后,这种扩展机制被模块化带来的天然的扩展能力取代。
- 应用程序类加载器
这个类加载器由sun.misc.Launcher$AppClassLoader来实现。它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
JDK9之前的Java应用都是由这三种类加载器互相配合来完成加载的,用户也可以自定义加载器来进行拓展。
图中展示的各种类加载器之间的层次关系被称为类加载器的“双亲委派模型”,双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器外。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当副加载反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { Class c = findLoadedClass(name);
if (c == null) { try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) {
}
if (c == null) { c = findClass(name); } }
if (resolve) { resolveClass(c); }
return c; }
|
破坏双亲委派模型
双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式,下面举个破坏双亲委派模型的例子:
双亲委派模型很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类型之所以被称为“基础”,是因为它们总是作为被用户代码继承、调用的API存在,但程序设计往往没有绝对不变的完美规则,如果有基础类型又要调用回用户的代码,那该怎么办呢?
下面浅析一下JDBC的加载过程:
DriverManager会尝试加载在"jdbc.drivers"系统属性中引用的驱动程序类,这允许用户定制由他们的应用程序使用的JDBC Driver。比如在META-INF/services/下放置驱动加载器:com.mysql.cj.jdbc.Driver。
- ServiceLoader会加载所有META-INF/services/实现Driver的class
- 迭代next()方法去加载实际的实现
其中迭代器的部分就是ServiceLoader的实现,调用Class.forName去加载类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| private Class<?> nextProviderClass() { String fullName; if (this.configs == null) { try { fullName = "META-INF/services/" + ServiceLoader.this.service.getName(); if (ServiceLoader.this.loader == null) { this.configs = ClassLoader.getSystemResources(fullName); } else if (ServiceLoader.this.loader == ClassLoaders.platformClassLoader()) { if (BootLoader.hasClassPath()) { this.configs = BootLoader.findResources(fullName); } else { this.configs = Collections.emptyEnumeration(); } } else { this.configs = ServiceLoader.this.loader.getResources(fullName); } } catch (IOException var4) { ServiceLoader.fail(ServiceLoader.this.service, "Error locating configuration files", var4); } }
while (this.pending == null || !this.pending.hasNext()) { if (!this.configs.hasMoreElements()) { return null; }
this.pending = this.parse((URL) this.configs.nextElement()); }
fullName = (String) this.pending.next();
try { return Class.forName(fullName, false, ServiceLoader.this.loader); } catch (ClassNotFoundException var3) { ServiceLoader.fail(ServiceLoader.this.service, "Provider " + fullName + " not found"); return null; } }
|
而在最开始调用ServiceLoader.load方法时,使用的线程上下文类加载器
1 2 3 4 5 6
| ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return new ServiceLoader<>(Reflection.getCallerClass(), service, cl); }
|
JDBC使用线程上下文类加载器去加载所需的SPI代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则。(具体细节代码可以看网上的剖析)
实现过程:
可以看到实现SPI,是通过调用ServiceLoader.load方法,然后通过迭代器来实例化所有在META-INF/services/下面的实现类,并放入缓存中。
但是它不能按需加载,如果META-INF/services/下面有多个实现,那么就会将多个实现通过迭代器的方式同时进行加载,同时不能直接根据某个参数直接获取某个具体的实现,而是通过遍历的方式获取。
Dubbo SPI机制
可扩展性的优点主要表现模块之间解耦,它符合开闭原则,对扩展开放,对修改关闭。当系统增加新功能时,不需要对现有系统的结构和代码进行修改,仅仅新增一个扩展即可。
用户能够基于 Dubbo 提供的扩展能力,很方便基于自身需求扩展其他协议、过滤器、路由等。Dubbo 扩展能力的特性:
- 按需加载。Dubbo 的扩展能力不会一次性实例化所有实现,而是用扩展类实例化,减少资源浪费。
- 增加扩展类的 IOC 能力。Dubbo 的扩展能力并不仅仅只是发现扩展服务实现类,而是在此基础上更进一步,如果该扩展类的属性依赖其他对象,则 Dubbo 会自动的完成该依赖对象的注入功能。
- 增加扩展类的 AOP 能力。Dubbo 扩展能力会自动的发现扩展类的包装类,完成包装类的构造,增强扩展类的功能。
- 具备动态选择扩展实现的能力。Dubbo 扩展会基于参数,在运行时动态选择对应的扩展类,提高了 Dubbo 的扩展能力。
- 可以对扩展实现进行排序。能够基于用户需求,指定扩展实现的执行顺序。
- 提供扩展点的 Adaptive 能力。该能力可以使的一些扩展类在 consumer 端生效,一些扩展类在 provider 端生效。
Dubbo 实现的一些例如动态选择扩展实现、IOC、AOP 等特性,能够为用户提供非常灵活的扩展能力。
Dubbo 扩展能力使得 Dubbo 项目很方便的切分成一个一个的子模块,实现热插拔特性。用户完全可以基于自身需求,替换 Dubbo 原生实现,来满足自身业务需求。下面是Dubbo官网上关于可扩展的地方:
规范定义
- 接口名:可以随意定义,但接口必须被
@SPI 注解修饰
- 提供者配置文件路径:在依次查找的目录为
- META-INF/dubbo/internal
- META-INF/dubbo
- META-INF/services
- 提供者配置文件名称:接口的全限定性类名,无需扩展名
- 提供者配置文件内容: 文件的内容为
key=value 形式
, value 为该接口的实现类的全限类性类名, key 可以随意,但一般为该实现类的“标识前辍”(首字母小写)。一个类名占一行。
- 提供者加载: ExtensionLoader 类相当于 JDK SPI 中的 ServiceLoader 类,用于加载提供者配置文件中所有的实现类,并创建相应的实例。
- 增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接使用 setter() 方法注入其他扩展点 ,也可 以对扩展点使用 Wrapper 类进行功能增强。
下面举个例子:
1 2 3 4
| @SPI public interface Car { String engineStart(); }
|
1 2 3 4 5 6 7
| public class Porsche implements Car { @Override public String engineStart() { System.out.println("Porsche start"); return "Porsche start"; } }
|
1 2 3 4 5 6 7
| public class Volvo implements Car { @Override public String engineStart() { System.out.println("Volvo start"); return "Volvo start"; } }
|
在META-INF/dubbo下面添加两行:
1 2
| porsche=com.yonhoo.dubbo_server.spi.Porsche volvo=com.yonhoo.dubbo_server.spi.Volvo
|
1 2 3 4 5 6 7 8
| public class DubboSpiTest { public static void main(String[] args) { ExtensionLoader<Car> loader = ExtensionLoader.getExtensionLoader(Car.class);
Car alipay = loader.getExtension("volvo"); System.out.println(alipay.engineStart()); } }
|
output :
Volvo start
Volvo start
自适应机制Adaptive
Adaptive 机制,即扩展类的自适应机制。即其可以指定想要加载的扩展名,也可以不指定。若不指定,则直接加载默认的扩展类。即其会自动匹配,做到自适应。其是通过@Adaptive注解实现的
@Adaptive 修饰方法
被@Adapative修饰的 SPI 接口中的方法称为Adaptive方法。会根据Adaptive方法自动为该SPI接口动态生成一个Adaptive扩展类,并自动将其编译。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| package <SPI 接口所在包>; public class SPI 接口名$Adpative implements SPI 接口 { public adaptiveMethod (arg0, arg1, ...) { if(arg1==null) throw new IllegalArgumentException(异常信息); if(arg1.getUrl()==null) throw new IllegalArgumentException(异常信息); URL url = arg1.getUrl(); String extName = url.get 接口名() == null?默认扩展前辍名:url.get 接口名(); if(extName==null) throw new IllegalStateException(异常信息); SPI 接口 extension = ExtensionLoader.getExtensionLoader(SPI 接口.class) .getExtension(extName); return extension. adaptiveMethod(arg0, arg1, ...); } public unAdaptiveMethod( arg0, arg1, ...) { throw new UnsupportedOperationException(异常信息); } }
|
举个例子:
1 2 3 4 5 6 7
| @SPI public interface Car { String engineStart();
@Adaptive void type(URL url); }
|
1 2 3 4 5 6 7 8 9 10 11 12
| public class Porsche implements Car { @Override public String engineStart() { System.out.println("Porsche start"); return "Porsche start"; }
@Override public void type(URL url) { System.out.println("Porsche"); } }
|
1 2 3 4 5 6 7 8 9 10 11 12
| public class Volvo implements Car { @Override public String engineStart() { System.out.println("Volvo start"); return "Volvo start"; }
@Override public void type(URL url) { System.out.println("Volvo"); } }
|
1 2 3 4 5 6 7 8
| public static void main(String[] args) { ExtensionLoader<Car> loader = ExtensionLoader.getExtensionLoader(Car.class)
Car car = loader.getAdaptiveExtension() URL url = URL.valueOf("dubbo://localhost:20880?car=volvo") car.type(url) }
|
output :
Volvo start
@Adaptive 修饰类
1 2 3 4
| @SPI("volvo") public interface Car { String engineStart(); }
|
1 2 3 4 5 6 7
| public class Porsche implements Car { @Override public String engineStart() { System.out.println("Porsche start"); return "Porsche start"; } }
|
1 2 3 4 5 6 7
| public class Volvo implements Car { @Override public String engineStart() { System.out.println("Volvo start"); return "Volvo start"; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Adaptive public class AdaptiveCar implements Car {
private String type;
public void setType(String type) { this.type = type; }
@Override public String engineStart() { ExtensionLoader<Car> loader = ExtensionLoader.getExtensionLoader(Car.class); Car car;
if (Strings.isBlank(type)) { car = loader.getDefaultExtension(); } else { car = loader.getExtension(type); }
return car.engineStart(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class DubboSpiTest { public static void main(String[] args) { ExtensionLoader<Car> loader = ExtensionLoader.getExtensionLoader(Car.class)
Car car = loader.getAdaptiveExtension()
car.engineStart()
((AdaptiveCar) car).setType("porsche")
car.engineStart()
System.out.println(loader.getSupportedExtensions()) } }
|
output :
Volvo start
Porsche start
[porsche, volvo]
下面看Dubbo是如何通过Adaptive实现SPI的IOC:
1 2 3 4
| @SPI public interface Car { String engineStart(); }
|
1 2 3 4
| @SPI public interface CarFactory { void goingForADrive(); }
|
1 2 3 4 5 6 7 8
| @Adaptive public class Nio implements Car { @Override public String engineStart() { System.out.println("Nio start"); return "Nio start"; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class DreamCarFactory implements CarFactory {
private Car nio;
@Override public void goingForADrive() { System.out.println("start"); if (nio != null) { nio.engineStart(); } System.out.println("end"); }
public void setCar(Car nio) { this.nio = nio; } }
|
1 2 3 4 5 6 7 8 9 10
| public class DubboSpiTest { public static void main(String[] args) {
ExtensionLoader<CarFactory> extensionLoader = ExtensionLoader.getExtensionLoader(CarFactory.class);
CarFactory dream = extensionLoader.getExtension("dream");
dream.goingForADrive(); } }
|
output:
start
Nio start
end
简单描述一下源码,injectExtension
会在createExtension中为依赖的其他对象进行注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| private T injectExtension(T instance) { if (this.injector == null) { return instance; } else { try { Method[] var2 = instance.getClass().getMethods(); int var3 = var2.length;
for(int var4 = 0; var4 < var3; ++var4) { Method method = var2[var4]; if (this.isSetter(method) && !method.isAnnotationPresent(DisableInject.class) && method.getDeclaringClass() != ScopeModelAware.class && (!(instance instanceof ScopeModelAware) && !(instance instanceof ExtensionAccessorAware) || !ignoredInjectMethodsDesc.contains(ReflectUtils.getDesc(method)))) { Class<?> pt = method.getParameterTypes()[0]; if (!ReflectUtils.isPrimitives(pt)) { try { String property = this.getSetterProperty(method); Object object = this.injector.getInstance(pt, property); if (object != null) { method.invoke(instance, object); } } catch (Exception var9) { logger.error("0-15", "", "", "Failed to inject via method " + method.getName() + " of interface " + this.type.getName() + ": " + var9.getMessage(), var9); } } } } } catch (Exception var10) { logger.error("0-15", "", "", var10.getMessage(), var10); }
return instance; } }
|
this.injector.getInstance(pt, property); 会将这遍历ScopeBeanExtensionInjector、SpringExtensionInjector、SpiExtensionInjector来将不同的对象进行注入
这里重点说一下SpiExtensionInjector
1 2 3 4 5 6 7 8 9 10 11 12
| public <T> T getInstance(final Class<T> type, final String name) { if (type.isInterface() && type.isAnnotationPresent(SPI.class)) { ExtensionLoader<T> loader = this.extensionAccessor.getExtensionLoader(type); if (loader == null) { return null; } else { return !loader.getSupportedExtensions().isEmpty() ? loader.getAdaptiveExtension() : null; } } else { return null; } }
|
它会将set方法,这里demo中的是setCar,将扫描Car.class的package,然后从实现interface的@Adaptive标注的实现类进行注入
包装机制 Wrapper
Wrapper通过实现interface,并且将interface的class作为构造参数,来实现interceptor
1 2 3 4
| @SPI public interface Car { String engineStart(); }
|
1 2 3 4 5 6 7
| public class Porsche implements Car { @Override public String engineStart() { System.out.println("Porsche start"); return "Porsche start"; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class CustomizedCar implements Car { private Car car;
public CustomizedCar(Car car) { this.car = car; }
@Override public String engineStart() { System.out.println("begin customized"); String result = car.engineStart(); System.out.println("end customized"); return result; } }
|
1 2 3 4 5 6 7 8 9 10 11 12
| public class DubboSpiTest { public static void main(String[] args) {
ExtensionLoader<Car> loader = ExtensionLoader.getExtensionLoader(Car.class);
Car car = loader.getExtension("porsche");
car.engineStart();
} }
|
output:
begin customized
Porsche start
end customized
激活机制 Activate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface Activate { String[] group() default {};
String[] value() default {};
@Deprecated String[] before() default {};
@Deprecated String[] after() default {};
int order() default 0; }
|
@Activate 有三个还在使用的field:
- group
为扩展类指定所属的组别
- value
为当前扩展类指定key,只有特定key的扩展来才会生效,如果group和key同时设定,则只有key会生效
- order
给当前扩展类设定优先级
具体例子就不再详解
可以看到Dubbo的SPI机制有很多可定制化的扩展,比如loadExtensionByName,通过Adaptive来实现动态加载以及IOC,通过Wrapper来实现Interceptor,可以非常灵活的使用,其底层实现方式差不太多,但是会比ServiceLoader更加灵活和强大
reference:
- dubbo SPI机制 demo和源码详解
- dubbo 官网源码解析