4.1 加载机制概述
4.1.1 Java SPI
在讲Dubbo SPI之前,先来了解一下Java SPI,SPI全称叫Service Provider Interface,起初是提供给厂商做插件开发的。Java SPI使用了策略模式,一个接口多种实现,我们只声明接口,具体实现由程序之外的配置掌控,步骤如下:
- 定义一个接口及对应的方法。
- 编写该接口的一个实现类。
- 在META-INF/services目录下,创建一个以接口全路径命名的文件,如com.test.spi.PrintService。
- 文件内容为具体实现类的全路径名,如果有多个,用分行符隔开。
- 在代码通过java.util.ServiceLoader来加载具体的实现类。
public static void main(String[] args) {
ServiceLoader<PrintService> serviceLoader = ServiceLoader.load(PrintService.class);
for (PrintService printService : serviceLoader) {
printService.printInfo();
}
}
4.1.2 Dubbo对SPI的改进
- Java SPI会一次性实例化扩展点所有实现,如果有扩展实现则初始化很耗时,如果没用上,则造成资源浪费。
- Java SPI如果扩展失败,如ScriptEngine的实现类RubyScriptEngine因为所依赖的jruby.jar不存在,导致加载失败,这个失败原因会被“吃掉”,当用户执行Ruby脚本,会报不支持Ruby,而不是真正的原因。
- Dubbo增加了对扩展IOC和AOP的支持。
// 第一步,在目录/META-INF/dubbo/internal下简历配置文件com.test.spi.PrintService
// 文件内容:impl=com.test.spi.PrintServiceImpl
// 第二步,接口加上SPI注解
@SPI("impl")
public interface PrintService {
void printInfo();
}
public class PrintServiceImpl implements PrintService {
@Override
public void printInfo() {
System.out.println("hello");
}
}
// 第三步,通过ExtensionLoader加载
public static void main(String[] args) {
PrintService printService = ExtensionLoader.getExtensionLoader(PrintService.class).getDefaultExtension():
pritnService.printInfo();
}
4.1.3 Dubbo扩展点的配置规范
Dubbo SPI和Java SPI类似,需要在META-INF/dubbo/下放置对应的SPI配置文件,文件名为接口的全路径名。配置文件内容为key=扩展点实现类全路径名,如果有多个实现类则使用换行符分隔。其中,key会作为@SPI注解中的传入参数。另外,Dubbo SPI还同时兼容Java SPI的配置路径和内容配置方式。在Dubbo启动时,会默认扫四个目录下的配置:META-INF/services、META-INF/dubbo、META-INF/dubbo/internal 、META-INF/dubbo/external。
4.1.4 Dubbo扩展点的分类与缓存
Dubbo SPI可以分为Class缓存、实例缓存。
- Class缓存:Dubbo SPI获取扩展类时,会先从缓存读取,如果缓存中不存在,则加载配置文件,根据配置把Class缓存到内存中,但并不会直接实例化。
- 实例缓存:每次获取实例时,同样是先从实例缓存中读取,如果读不到,则重新加载并缓存。因此Dubbo SPI是按需实例化并缓存的机制,性能更好。
被缓存的Class和对象实例可以根据不同的特性分为不同的类别:
- 普通扩展类。最基础的,配置在SPI配置文件中的扩展类实现。
- 包装扩展类。这种Wrapper类没有具体实现,只是做了通用逻辑的抽象,并且需要在构造方法中传入一个 具体的扩展接口的实现。属于Dubb的自动包装特性。
- 自适应扩展类。一个扩展接口会有多种实现类,具体使用哪个实现类可以不写死在配置或代码中,在运行时,通过传入的URL中的某些参数动态选择实现类,会使用@Adaptive注解。
- 其他缓存,如扩展类加载器缓存、扩展名缓存等。
4.1.5 扩展点的特性
- 自动包装(AOP)
ExtensionLoader在加载扩展时,如果发现这个扩展类的构造方法有其他扩展点作为构造函数,则会被认为是一个Wrapper类。
public class ProtocolFilterWrapper implements Protocol {
private final Protocol protocol;
public ProtocolFilterWrapper(Protocol protocol) {
if (protocol == null) {
throw new IllegalArgumentException("protocol == null");
}
this.protocol = protocol;
}
...
}
- 自动加载(IOC)
如果某个扩展类是另外一个扩展点类的成员属性,并且拥有setter方法,那么框架会自动注入对应的扩展点实例。此处存在一个问题,如果依赖的扩展有多种实现,具体会注入哪一个呢?这就涉及到第3个特性-自适应了。
3.自适应
在Dubbo SPI中,我们可以使用@Adaptive注解,动态地依据URL中的某个参数来确定使用哪个实现类,从而解决自动加载中的实例注入问题。
@SPI("netty")
public interface Transproter {
@Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY})
Server bind(URL url, ChannelHandler handler) throws RemotingException;
@Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
Client connect(URL url, ChannelHandler handler) throws RemotingException;
}
@Adaptive传入了两个参数,分别是“server”和"transporter",外部调用Transporter#bind方法时,会动态从传入的参数URL中提取key参数“server”对应的value值,如果能匹配上唯一一个扩展实现类则直接使用对应的实现类;如果不能匹配,则继续通过第二个key参数"transporter"的value值来匹配。都没有匹配上,则抛出异常。
4.自动激活
使用@Activate注解,可以标记对应的扩展点默认被激活启用。该注解可以通过传入不同的参数,设置扩展点在不同的条件下被自动激活。这个注解的主要使用场景是某个扩展点的多个实现类需要同时启用(比如Filter扩展点)。
4.2 扩展点注解
4.2.1 @SPI
@SPI注解通常用在接口上,标记这个接口是一个Dubbo SPI接口。该注解可以接收一个value参数,用于指定这个接口的默认实现类。
4.2.2 @Adaptive
@Adaptive可以标记在类、接口、方法上,但大多数情况用在方法上更多。方法级别注解在第一次getExtension时,会自动生成和编译一个动态的Adaptive类,从而打到动态实现类的效果。
如前面的例子,Transproter接口在bind和connect方法上添加了@Adaptive注解,Dubbo在初始化扩展点时,会生成一个Transporter$Adaptive类,里面会实现这两个方法,方法里会有一些抽象的通用逻辑,通过@Adaptive中传入的参数找到并调用真正的实现类,跟装饰者模式有点类似。下面是自动生成的Transporter$Adaptive#bind的代码:
public Server bind(URL arg0, ChannelHandler arg1) throws RemotingException {
URL url = arg0;
String extName = url.getParameter("server", url.getParameter("transporter", "netty"));
...
try {
extension = (Transporter)ExtensionLoader.getExtensionLoader(Transporter.class).getExtension(extName);
} catch (Exception e) {
extension = (Transporter)ExtensionLoader.getExtensionLoader(Transporter.class).getExtension("netty");
}
return extension.bind(arg0, arg1);
}
如果该注解放在实现类上,则整个实现类会直接作为默认实现,不需要自动生成上面的动态代码,如果有多个实现类,只有一个实现类可以加上@Adaptive注解,万一多个实现类都注解了会抛异常。因此在实现类上使用这个注解的效果,与@SPI的参数指定实现类类似(但读完后面的源代码其实还是不一样的,@SPI指定的默认实现类用于getExtension方法,@Adaptive指定的用于getAdaptiveExtension方法)。
4.2.3 @Activate
@Activate可以标记在类、接口和方法上。主要用于有多个扩展点实现,需要根据不同条件来激活的场景,比如Filter需要多个同时激活。@Activate可传入的参数很多:
- String[] group 指定URL中的分组,如果匹配则激活,可以设置多个。
- String[] value 查找URL如果含有该key的值,则会激活。
- String[] before 填写扩展点列表,表示这些扩展点要在本扩展点之前
- String[] after 同上,表示之后
- int order 直接的顺序信息
4.3 ExtensionLoader的工作原理
4.3.1 工作流程
ExtensionLoader的逻辑入口可以分为getExtension、getAdaptiveExtension、getActivateExtension三个,总体逻辑都是从调用这三个方法开始的。
getActivateExtension只是根据不同的条件同时激活多个普通扩展类,因此此方法只会做一些通用的判断逻辑,比如是否包含@Activate注解,匹配条件是否符合等。最终还是会调用getExtension方法来获得具体的实现类实例。
getExtension(String name)是整个扩展加载器最核心的方法,实现了一个完整的普通扩展类加载的过程。加载过程中的每一步,都会先检查缓存中是否已经存在所需的数据,如果存在则直接从缓存中读取,没有则重新加载。这个方法每次只会根据名称返回一个扩展点实现类。实例化的过程可以分为4步:
(1) 读取SPI对应路径下的配置文件,并根据配置加载所有扩展类Class并缓存。(这个应该是只读getExtensionLoader(Class)传递的class对象所对应的配置文件)
(2) 根据传入的name参数实例化对应的扩展类。
(3) 尝试查找符合条件的包装类:包含扩展点的setter方法;包含与扩展类型相同的构造函数,例如本次实例化了一个Class A对象。实例化完成后,会寻找构造参数中需要Class A的包装类。(居然是主动查找?)
(4) 返回对应的扩展类实例。getAdaptiveExtension也相对独立,只加载配置信息部分与getExtension共用了同一个方法。和获取普通扩展类一样,框架会先检查缓存中是否有已经实例化好的Adaptive实例,没有则调用createAdaptiveExtension进行实例化。
(1) 和getExtension()一样先加载配置文件。
(2) 生成自适应类的代码字符串。
(3) 获取类加载器和编译器,并用编译器编译刚才生成的代码字符串。Dubbo一共有三种类型的编译器实现,后面会讲。
(4)返回对应的自适应类实例。
4.3.2 getExtension的实现原理
用法示例
ExtensionLoader.getExtensionLoader(XXX.class).getExtension("xxx"):
当调用getExtension(String name)方法时,会先检查缓存中是否有现成的数据,没有则调用createExtension开始创建。这里有个特殊点,如果getExtension的name参数时"true",会加载并返回默认扩展类,内部实质会调用getDefaultExtension()。
在调用createExtension的过程中,也会先检查缓存中是否有配置信息,如果不存在,则会从META-INF/services、META-INF/dubbo、META-INF/dubbo/internal、META-INF/dubbo/external这几个路径中读取所有的配置文件,得到扩展点实现类的全称。
先看createExtension()中通过getExtensionClasses()获取实现类Class的实现:
接下来再回头看createExtension()拿到实现类Class之后,剩余的部分:
个人思考与尝试:
1.有一点不太理解为什么依赖注入要用getAdaptiveExtension而不用getExtension呢?我自己写了两个@SPI接口,实现类和接口都没有@Adaptive注解测试了下依赖注入,发现getAdaptiveExtension确实会抛异常,必须要求依赖注入的@SPI接口是自适应类型的,这个设计有点奇怪。不过通过查看dubbo本身的源码,用到依赖注入的确实基本上都是@Adaptive的。
2.关于循环依赖,我自己写了两个@Adaptive类互相依赖,发现并不会出现异常,看了很久发现原来是因为依赖注入的必然是XXX$Adaptive类,此类是通过代码生成再编译的方式得到的,通过调试和反编译看到生成的类内部不会再有更深层依赖注入,而是在用到依赖的地方使用ExtensionLoader.getExtensionLoader().getExtension()的方式来延迟获取。其实想明白Adaptive本身就是要动态切换所依赖的实现类这层含义,就不会有循环依赖这个疑问。
4.3.3 getAdaptiveExtension的实现原理
由前面的流程我们可以知道,在getAdaptiveExtension()方法中,会为扩展点接口自动生成实现类字符串,实现类主要包含以下逻辑:为接口中每个有@Adaptive注解的方法生成默认实现(没有注解的方法则生成空实现),每个默认实现都会从URL(或者directory中的url)中提取Adaptive参数值,并以此为依据动态加载扩展点。然后,框架会使用不同的编译器,把实现类代码字符串编译成Class并返回。
生成代码的逻辑主要分7步:
(1) 生成package、import、类名称等头部信息。此处import只有ExtensionLoader一个类。为了步import其他类,其他类都用全路径来使用。生成的类名称为“接口名称$Adaptive”。
(2) 遍历接口所有方法,获取方法的返回类型、参数类型、异常类型。为第(3)步判断是否为空做准备。
(3) 生成校验代码,如参数是否为空。如果有远程调用,还会添加Invocation参数是否为空的校验。
(4) 如果@Adaptive注解没有设定value,生成默认url参数名,按照类名称驼峰转点的方式生成,如YyyInvokerWrapper会以yyy.invoker.wrapper作为默认url参数名。
(5) 生成获取扩展点名称的代码。如@Adaptive("protocol"),会生成url.getProtocol()。
(6) 生成获取具体扩展实现类的代码。即通过ExtensionLoader.getExtensionLoader().getExtension(extName)获取到url中自适应参数对于的实现类。如果url中拿不到参数值,则换下一个参数值,如果都不行,则使用默认实现类别名来作为extName去加载实现类实例。
(7) 生成调用实现类方法的代码。
书中居然省略了这么重要的自适应的源码分析。。。无语,具体可参考官网:
https://dubbo.apache.org/zh/docs/v2.7/dev/source/adaptive-extension/
4.3.4 getActivateExtension的实现原理
通过getActivateExtension(URL url, String key, String group)方法可以获取所有自动激活的扩展点。其流程分4步:
(1) 检查缓存,如果缓存中没有,则初始化所有扩展类实现的集合。
(2) 遍历整个@Activate注解集合,根据传入的URL、key、group参数匹配符合条件的扩展类实现。然后根据@Activate中配置的before、after、order等参数进行排序。
(3) 遍历所有用户自定义扩展类名称,根据用户URL配置的顺序,调整扩展点激活顺序(遵循用户在URL中配置的顺序,例如URL为test://localhost/test?ext=order1,default,那么扩展点ext的激活顺序会遵循先order1再default,其中default代表所有@Activate注解的扩展点)
(4) 返回所有自动激活类集合。
注意点1:获取Activate扩展类实现,也是通过调用getExtension得到的。
注意点2:如果URL的参数中传入了-default,则所有默认的@Activate都不会被激活,只有URL参数中指定的扩展点会被激活。如果传入了"-"+扩展点名,则该扩展点也不会被自动激活。
虽然这段代码实现相对不那么重要,但因为不容易看明白,所以还是贴以下代码并加了点方便理解的注释。
4.3.5 ExtensionFactory的实现原理
前面在依赖注入的实现中已经有提到过,通过ExtensionLoader中的objectFactory除了可以注入其他扩展点,还能注入Spring bean,这个又是如何实现的呢?我们知道objectFacotry的类型是ExtensionFactory,这是一个@SPI接口提供了SpiExtensionFactory、SpringExtensionFactory、AdaptiveExtensionFactory三个实现。
那么具体用的是哪个实现呢?翻看源码可以知道AdaptiveExtensionFactory在类上使用了@Adaptive注解,所以在ExtensionLoader的构造方法中调用getAdaptiveExtension()会返回AdaptiveExtensionFactory实例。
再来看SpiExtensionFactory的实现:
最后是SpringExtensionFactory:
那么Spring的上下文又是什么时候被保存起来的呢?我们通过代码搜索得知,在ReferenceBean和ServiceBean中会调用静态方法保存Spring上下文,即一个服务被发布或者被引用的时候。
4.4 扩展点动态编译的实现
动态编译是自适应特性的基础,因为动态生成的自适应类只是字符串,需要通过编译才能得到真正的Class。虽然我们可以通过反射来动态代理一个类,但是在性能上和直接编译好的Class会有一定的差距,所以Dubbo SPI采用代码动态生成,并配合动态编译器,灵活地在原始类的基础上创建新的自适应类。
Dubbo有三种代码编译器,分别是JdkCompiler,JavassistCompiler和AdaptiveCompiler,他们都实现了Complier接口。接口Compiler上含有@SPI注解,默认值为@SPI("javassist"),即使用JavassistCompiler作为默认编译器。用户想修改可以通过配置<dubbo:application compiler="jdk" />进行修改。
AdaptiveCompiler类上面有@Adaptive注解,说明它作为自适应的默认实现,其作用与AdaptiveExtensionFactory类似,用于管理其他Compiler。
然后看一下AbstractCompiler,它是一个抽象类,里面封装了通用的模板逻辑,还定义了一个抽象方法doCompile,留给子类来实现。
AbstractCompiler的主要抽象逻辑:
- 通过正则匹配出包路径、类名,在根据包路径、类名拼接出全路径类名。
- 尝试通过Class.forName加载该类,防止重复编译。如果类加载器中没有,则进入下一步。
- 调用doCompile方法进行编译。
4.4.2 Javassist动态代码编译
先看一个Javassist生成Hello World的使用例子:
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("HelloWord");
CtMethod ctMethod = CtNewMethod.make("
public static void test() {
System.out.println(\"Hello Word\");
}
", ctClass);
ctClass.addMethod(ctMethod);
Class clazz = ctClass.toClass();
Object object = clazz.newInstance();
Method m = clazz.getDeclaredMethod("test", null);
m.invoke(object, null);
看完使用示例,Dubbo的JavassistCompiler的实现就很容易猜到了,就是通过正则匹配不同部位的代码,然后调用Javassist的api来生成添加进ctClass中,最后得到一个完整的Class对象。具体步骤:
(1) 初始化Javassist,如设置classpath。
(2) 正则匹配所有import包,使用Javassist添加import。
(3) 正则匹配所有extends,创建Class对象,并使用Javassist添加extends。
(4) 正则匹配所有implements,并使用Javassist添加implements。
(5) 正则匹配类里面{}中所有的内容,在通过正则匹配所有的方法,并使用Javassist添加类方法。
(6) 生成Class对象。
4.4.3 JDK动态编译
JDK自带的编译器位于javax.tools下,Dubbo主要使用了JavaFileObject接口、JavaFileManager接口、JavaCompiler.CompilationTask方法。整个编译过程可以总结为:先初始化一个JavaFileObject对象,并把代码字符串作为参数传入构造方法,然后调用JavaCompiler.CompilationTask方法编译出具体的类。JavaFileManager负责管理类文件的输入/输出位置。
- JavaFileObject接口。字符串代码会被包装成一个文件对象,并提供二进制流的接口。Dubbo框架中的JavaFileObjectImpl类可以看作该接口一种扩展实现,构造方法中需要传入生成好的字符串代码,此文件对象的输入和输出都是ByteArray流。
- JavaFileManager接口。主要管理文件的读取和输出位置。JDK中没有可以直接使用的实现类,唯一的实现类是ForwardingJavaFileManager构造器又是protect类型。因此Dubbo中定制化实现了一个JavaFileManagerImpl类,并通过一个自定义加载器ClassLoaderImpl完成资源的加载。
- JavaCompiler.CompilationTask把JavaFileObject对象编译成具体的类。