Dubbo拓展点加载机制
前言
本篇首先介绍现有Dubbo
加载机制的详情,包含Dubbo
所做的改良及局部个性。而后介绍加载机制曾经存在的一些要害注解,@SPI
、@Adaptive、@Activate
。而后介绍整个接在机制中最外围的ExtensionLoader
的工作流程及实现原理。最初介绍拓展中应用的类动静编译的实现原理。通过本篇的浏览,咱们会对Dubbo SPI
加载机制有深刻的理解,也会对这部分的源码有深刻的意识,后续胖友自行浏览源码也会更容易。
加载机制概述
Dubbo良好的拓展性与两个方面是密不可分的,一是整个框架中针对不同场景,恰到好处的应用了各种设计模式,二就是咱们要说的加载机制。基于Dubbo SPI
的加载机制,让整个框架的接口与具体实现齐全解耦,从而给整个框架良好拓展性奠定了根底。
Dubbo
默认提供了很多能够间接应用的拓展点。Dubbo
简直所有的性能组件都是基于拓展机制(SPI)实现的,这些外围拓展点前面会具体说。
Dubbo SPI
没有间接应用Java SPI
,而是在它的思维上又做了-定的改良,造成了一套本人的配置标准和个性。同时,Dubbo SPI
又兼容Java SPI
。服务在启动的时候,Dubbo
就会查找这些扩大点的所有实现。
Java SPI
在说Dubbo SPI之前,咱们先说一下Java SPI
。SPI的全称是Service Provider Interface
,起初是提供给厂商做插件开发的。对于Java SPI的具体定义和解释,能够参见此处。Java SPI应用了策略模式,一个接口多种实现。咱们只申明接口,具体的实现并不在程序中间接确定,而是由程序之外的配置掌控,用于具体实现的拆卸。具体步骤如下:
- 定义一个接口及对应的办法。
- 编写该接口的一个实现类。
- 在
META-INF/services/
目录下,创立一个以接口全门路命名的文件,如com.example.rpc.example.spi.HelloService
。 - 文件内容为具体实现类的全路径名,如果有多个,则用分行符分隔。
- 在代码中通过java. util. ServiceLoader来加载具体的实现类。如此一来,
HelloService
的具体实现就能够由文件com.example.rpc.example.spi.HelloService
中配置的实现类来确定了,这里我配置的类为com.example.rpc.example.spi.HelloServiceImpl
。
我的项目构造如下:
Java SPI示例代码如下:
public interface HelloService { void sayHello();}
public class HelloServiceImpl implements HelloService{ @Override public void sayHello() { System.out.printf("hello world!"); }}
public static void main(String[] args) { ServiceLoader<HelloService> serviceLoader = ServiceLoader.load(HelloService.class); //获取所有的SPI实现, // 循环调用 sayHello()办法, // 会打印出hello world 此处只有一个实现: HelloServiceImpl for (HelloService helloService : serviceLoader) { //此处会输入hello world helloService.sayHello(); }}
从下面的代码清单中能够看到,main
办法里通过java.util.ServiceLoader
能够获取所有的接口实现,具体调用哪个实现,能够通过用户定制的规定来决定。
拓展点加载机制的改良
Dubbo 的扩大点加载从 JDK 规范的 SPI (Service Provider Interface) 扩大点发现机制增强而来。
Dubbo对JDK 规范的SPI做了一些改良:
- JDK 规范的 SPI 会一次性实例化扩大点所有实现,如果有扩大实现初始化很耗时,但如果没用上也加载,会很浪费资源。
- 如果扩大点加载失败,连扩大点的名称都拿不到了。
- 减少了对扩大点 IoC 和 AOP 的反对,一个扩大点能够间接 setter 注入其它扩大点。
HelloService接口的Dubbo SPI革新
- 在目录
META- INF/dubbo/ internal
下建设配置文件com.example.rpc.example.spi.HelloService
,文件内容如下
impl=com.example.rpc.example.spi.HelloServiceImpl
- 为接口类增加SPI注解,设置默认实现为
impl
@SPI("impl")public interface HelloService { void sayHello();}
- 实现类不变
public class HelloServiceImpl implements HelloService{ @Override public void sayHello() { System.out.printf("hello world!"); }}
- 调用Dubbo SPI
public static void main(String[] args) { //通过ExtensionLoader获取接口HelloService.class的默认实现 ExtensionLoader<HelloService> extensionLoader = ExtensionLoader.getExtensionLoader(HelloService.class); HelloService helloService = extensionLoader.getDefaultExtension(); //此处会打印出hello world helloService.sayHello();}
拓展点注解
拓展点注解:@SPI
@SPI注解能够应用在类、接口和枚举类上,Dubbo框架中都是应用在接口上。它的次要作用就是标记这个接口是一个Dubbo SPI 接口,即是一个扩大点,能够有多个不同的内置或用户定义的实现。运行时须要通过配置找到具体的实现类。@SPI 注解的源码如下所示。
@SPI 注解的源码
@Documented@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE})public @interface SPI { /** * 默认实现的key名称 * default extension name */ String value() default "";}
咱们能够看到SPI注解有一个value属性,通过这个属性,咱们能够传入不同的参数来设置这个接口的默认实现类。例如,咱们能够看到Transporter
接口应用Netty
作为默认实现,代码如下。
@SPI("netty")public interface Transporter { ...}
Dubbo中很多中央通过getExtension(Class<T> type, String name)
来获取扩大点接口的具体实现,此时会对传入的Class做校验,判断是否是接口,以及是否有@SPI
注解,两者缺一不可。
拓展点自适应注解:@Adaptive
@Adaptive
注解能够标记在类、接口、枚举类和办法上,然而在整个Dubbo框架中,只有几个中央应用在类级别上,如AdaptiveExtensionFactory
和AdaptiveCompiler
,其余都标注在办法上。如果标注在接口的办法上,即办法级别注解,则能够通过参数动静取得实现类。办法级别注解在第一次getExtension
时,会主动生成和编译一个动静的Adaptive
类,从而达到动静实现类的成果。
上面我举个例子说一下,拿Protocol
接口举例,export
和refer
接口两个办法增加了@Adaptive
注解,代码如下,Dubbo在初始化扩大点时,会生成一个Protocol$Adaptive
类,外面会实现这两个办法,办法里会有一些形象的通用逻辑,通过@Adaptive
中传入的参数,找到并调用真正的实现类。相熟装璜器模式的读者会很容易了解这部分的逻辑。具体实现原理会在前面说。
上面是主动生成的Protocol$Adaptive#export
实现代码,如代码如下,省略局部无关代码。
Protocol$Adaptive#export
public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol { public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0) throws org.apache.dubbo.rpc.RpcException { if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null"); if (arg0.getUrl() == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null"); org.apache.dubbo.common.URL url = arg0.getUrl(); //通过protocol key去寻找实现类的名称 String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol()); if (extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])"); ScopeModel scopeModel = ScopeModelUtil.getOrDefault(url.getScopeModel(), org.apache.dubbo.rpc.Protocol.class); //依据url中的参数 尝试获取真正的拓展点实现类 org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol) scopeModel.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName); //最终会调用具体拓展点的bind办法 return extension.export(arg0); }}
咱们从生成的源码中能够看出,主动生成的代码中实现了很多通用的性能,最终会调用真正的接口实现。
当该注解放在实现类上,则整个实现类会间接作为默认实现,不再主动生成上述代码。在扩大点接口的多个实现里,只能有一个实现上能够加@Adaptive
注解。如果多个实现类都有该注解,则会抛出异样: More than 1 adaptive class found
。 @Adaptive
注解的源代码如下。
Adaptive注解的源代码
@Documented@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE, ElementType.METHOD})public @interface Adaptive { //数组 能够设置多个key,会依照程序一次匹配 String[] value() default {};}
该注解也能够传入value参数,是一个数组。咱们在代码清单4.9中能够看到,Adaptive可 以传入多个key值,在初始化Adaptive注解的接口时,会先对传入的URL进行key值匹配,第一个key没匹配上则匹配第二个,以此类推。直到所有的key匹配结束,如果还没有匹配到, 则会应用"驼峰规定"匹配,如果也没匹配到,则会抛出IllegalStateException异样。
什么是"驼峰规定”呢?如果包装类(Wrapper) 没有用Adaptive指定key值,则Dubbo 会主动把接口名称依据驼峰大小写离开,并用符号连接起来,以此来作为默认实现类的名 称,如 org.apache.dubbo.xxx.YyylnvokerWpapper
中的 YyylnvokerWrapper
会被转换为 yyy.invoker.wrapper
。
为什么有些实现在实现类上会标注@Adaptivene ?
放在实现类上,次要是为了间接固定对 应的实现而不须要动静生成代码实现,就像策略模式间接确定实现类。在代码中的实现形式是: ExtensionLoader
中会缓存两个与@Adaptive
无关的对象,一个缓存在cachedAdaptiveClass
中, 即Adaptive
具体实现类的Class
类型;另外一个缓存在cachedAdaptivelnstance
中,即Class 的具体实例化对象。在扩大点初始化时,如果发现实现类有@Adaptive注解,则间接赋值给 cachedAdaptiveClass
, 后续实例化类的时候,就不会再动静生成代码,间接实例化 cachedAdaptiveClass
,并把实例缓存到cachedAdaptivelnstance
中。如果注解在接口办法上, 则会依据参数,动静取得扩大点的实现,会生成Adaptive类,再缓存到 cachedAdaptivelnstance
中。
拓展点主动激活注解:@Activate
@Activate能够标记在类、接口、枚举类和办法上。次要应用在有多个扩大点实现、须要根 据不同条件被激活的场景中,如Filter须要多个同时激活,因为每个Filter实现的是不同的性能。 ©Activate可传入的参数很多,如下表:
参数名 | 成果 |
---|---|
String[] group() | URL中的分组如果匹配则激活,则能够设置多个 |
String[] value() | 查找URL中如果含有该key值,则会激活 |
String[] before() | 填写扩大点列表,示意哪些扩大点要在本扩大点之前 |
String[] after() | 同上,示意哪些须要在本扩大点之后 |
int order() | 整型,间接的排序信息 |
ExtensionLoader的工作原理
ExtensionLoader
是整个扩大机制的次要逻辑类,在这个类外面卖现了配置的加载、扩大类 缓存、自适应对象生成等所有工作。上面就联合代码讲一下整个ExtensionLoader
的工作流程。
工作流程
ExtensionLoader
的逻辑入口能够分为 getExtension
、getAdaptiveExtension
、 getActivateExtension
三个,别离是获取一般扩大类、获取自适应扩大类、获取主动激活的扩 展类。总体逻辑都是从调用这三个办法开始的,每个办法可能会有不同的重载的办法,依据不 同的传入参数进行调整,具体流程如图所示。
三个入口中,getActivateExtension
对getExtension
的依赖比拟getAdaptiveExtension
则绝对独立。
getActivateExtension
办法只是依据不同的条件同时激活多个一般展类。因而,该办法中只会做一些通用的判断逻辑,如接口是否蕴含@Activate
注解、匹配条件是否合乎等。最终还是通过调用getExtension
办法取得具体扩大点实现类。
getExtension(String name)
是整个扩大加载器中最外围的办法,实现了一个残缺的一般扩 展类加载过程。加载过程中的每一步,都会先查看缓存中是否己经存在所需的数据,如果存在 则间接从缓存中读取,没有则从新加载。这个办法每次只会依据名称返回一个扩大点实现类。 初始化的过程能够分为4 步:
- 框架读取SPI对应门路下的配置文件,并依据配置加载所有扩大类并缓存(不初始化)。
- 依据传入的名称初始化对应的扩大类。
- 尝试查找符合条件的包装类:蕴含扩大点的setter办法,例如
setProtocol(Protocol protocol)
办法会主动注入protocol
扩大点实现;蕴含与扩大点类型雷同的构造函数,为其注入扩 展类实例,例如本次初始化了一个ClassA,初始化实现后,会寻找结构参数中须要ClassA的 包装类(Wrapper
),而后注入ClassA实例,并初始化这个包装类。 - 返回对应的扩大类实例。
getAdaptiveExtension
也绝对独立,只有加载配置信息局部与getExtension
共用了同一个 办法。和获取一般扩大类一样,框架会先查看缓存中是否有曾经初始化化好的Adaptive
实例, 没有则调用createAdaptiveExtension
从新初始化。初始化过程分为4步:
- 和
getExtension
一样先加载配置文件。 - 生成自适应类的代码字符串。
- 获取类加载器和编译器,并用编译器编译方才生成的代码字符串。Dubbo 一共有三种 类型的编译器实现,这些内容会在4.4节解说。
- 返回对应的自适应类实例。
接下来,咱们就具体看一下getExtension
、getAdaptiveExtension
、getActivateExtension
这三个流程的实现。
getExtension的实现原理
getExtension
的主流程下面曾经说过了,上面来具体说下每一步的实现原理。
当调用getExtension(String name)办法时,会先查看缓存中是否有现成的数据,没有则 调用createExtension
开始创立。这里有个非凡点,如果getExtension传入的name是true, 则加载并返回默认扩大类。
在调用createExtension
开始创立的过程中,也会先查看缓存中是否有配置信息,如果不 存在扩大类,则会从META-INF/services/
、META-INF/dubbo/
、META-INF/dubbo/internal/
这几个门路中读取所有的配置文件,通过I/O 读取字符流,而后通过解析字符串,失去配置文件中 对应的扩大点实现类的全称(如 org.apache.dubbo.common.extensionloader.activate.impl. GroupActivateExtImpl)
。扩大点配置信息加载过程的源码如下。
拓展点配置信息加载过程的源码
private Map<String, Class<?>> getExtensionClasses() { // 从缓存中获取已加载的拓展类 Map<String, Class<?>> classes = cachedClasses.get(); // 双重查看 if (classes == null) { synchronized (cachedClasses) { classes = cachedClasses.get(); if (classes == null) { // 开始加载拓展类 classes = loadExtensionClasses(); cachedClasses.set(classes); } } } return classes; } /** * 在GetExtensionClass中同步 * synchronized in getExtensionClasses */ private Map<String, Class<?>> loadExtensionClasses() { //查看是否有SPI注解。如果有,则获取注解中填写的名称,并缓存为默认实现名。 //如@SPI(" impl")会保留impl为默认实现 cacheDefaultExtensionName(); Map<String, Class<?>> extensionClasses = new HashMap<>(); for (LoadingStrategy strategy : strategies) { // 加载指定文件夹下的配置文件 loadDirectory(extensionClasses, strategy, type.getName()); // 兼容旧的ExtensionFactory if (this.type == ExtensionInjector.class) { loadDirectory(extensionClasses, strategy, ExtensionFactory.class.getName()); } } return extensionClasses; } /** * 加载指定文件夹下的配置文件 * @param extensionClasses * @param strategy * @param type */ private void loadDirectory(Map<String, Class<?>> extensionClasses, LoadingStrategy strategy, String type) { //加载org.apache.xxxxx loadDirectory(extensionClasses, strategy.directory(), type, strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages(), strategy.onlyExtensionClassLoaderPackages()); String oldType = type.replace("org.apache", "com.alibaba"); //加载com.alibaba.xxxxx loadDirectory(extensionClasses, strategy.directory(), oldType, strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages(), strategy.onlyExtensionClassLoaderPackages()); }
加载完扩大点配置后,再通过反射取得所有扩大实现类并缓存起来。留神,此处仅仅是把 Class加载到JVM
中,但并没有做Class初始化。在加载Class文件时,会依据Class上的注解来判断扩大点类型,再依据类型分类做缓存。扩大类的缓存分类代码如下:
拓展类的缓存分类
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name, boolean overridden) throws NoSuchMethodException { if (!type.isAssignableFrom(clazz)) { throw new IllegalStateException("Error occurred when loading extension class (interface: " + type + ", class line: " + clazz.getName() + "), class " + clazz.getName() + " is not subtype of interface."); } // 检测指标类上是否有 Adaptive 注解 if (clazz.isAnnotationPresent(Adaptive.class)) { // 如果发现是多个自适应类 设置 cachedAdaptiveClass缓存 cacheAdaptiveClass(clazz, overridden); // 检测 clazz 是否是 Wrapper 类型 } else if (isWrapperClass(clazz)) { // 如果是包装类 存储 clazz 到 cachedWrapperClasses 缓存中 cacheWrapperClass(clazz); } else { if (StringUtils.isEmpty(name)) { name = findAnnotationName(clazz); if (name.length() == 0) { throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL); } } String[] names = NAME_SEPARATOR.split(name); if (ArrayUtils.isNotEmpty(names)) { cacheActivateClass(clazz, names[0]); for (String n : names) { //不是自适应类型,也不是包装类型,剩下的就是一般扩大类了,也会缓存起来 //留神:主动激活也是一般扩大类的-种,只是会依据不同条件同时激活罢了 cacheName(clazz, n); saveInExtensionClass(extensionClasses, clazz, n, overridden); } } } }
最初,依据传入的name找到对应的类并通过Class.forName办法进行初始化,并为其注入 依赖的其余扩大类(主动加载个性)。当扩大类初始化后,会查看一次包装扩大类Set<Class<?> wrapperclasses
,查找蕴含与扩大点类型雷同的构造函数,为其注入刚初始化的扩大类。
依赖注入
injectExtension(instance); // 向拓展类实例中注入依赖 List<Class<?>> wrapperClassesList = new ArrayList<>();wrapperClassesList.addAll(cachedWrapperClasses);if (CollectionUtils.isNotEmpty(wrapperClassesList)) { // 循环创立 Wrapper 实例 for (Class<?> wrapperClass : wrapperClassesList) { Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class); if (wrapper == null || (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) { // 将以后 instance 作为参数传给 Wrapper 的构造方法,并通过反射创立 Wrapper 实例。 // 而后向 Wrapper 实例中注入依赖,最初将 Wrapper 实例再次赋值给 instance 变量 instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)); instance = postProcessAfterInitialization(instance, name); } } }
在injectExtension
办法中能够为类注入依赖属性,它应用了 ExtensionFactory#getExtension (Class<T> type, String name)
来获取对应的bean实例,这个工厂接口会在前面具体阐明。 咱们先来理解一下注入的实现原理。
injectExtension
办法总体实现了相似Spring的IoC机制,其实现原理比较简单:首先通 过反射获取类的所有办法,而后遍历以字符串set结尾的办法,失去set办法的参数类型,再通 过 ExtensionFactory
寻找参数类型雷同的扩大类实例,如果找到,就设值进去,代码如下。
注入依赖拓展类实现代码
for (Method method : instance.getClass().getMethods()) { //找到以set结尾的办法,要求只能有一个参数,并且是public if (!isSetter(method)) { continue; } /** * 查看是否须要主动注入这个属性 * Check {@link DisableInject} to see if we need auto injection for this property */ if (method.getAnnotation(DisableInject.class) != null) { continue; } // 获取 setter 办法参数类型 Class<?> pt = method.getParameterTypes()[0]; if (ReflectUtils.isPrimitives(pt)) { continue; } try { // 获取属性名,比方 setName 办法对应属性名 name String property = getSetterProperty(method); // 从 ObjectFactory 中获取依赖对象 Object object = injector.getInstance(pt, property); if (object != null) { // 通过反射调用 setter 办法设置依赖 method.invoke(instance, object); } } catch (Exception e) { logger.error("Failed to inject via method " + method.getName() + " of interface " + type.getName() + ": " + e.getMessage(), e); }}
从源码中能够晓得,包装类的结构参数注入也是通过injectExtension
办法实现的。
getAdaptiveExtension的实现原理
在getAdaptiveExtension()
办法中,会为扩大点接口主动生 成实现类字符串,实现类次要蕴含以下逻辑:为接口中每个有Adaptive
注解的办法生成默认实现(没有注解的办法则生成空实现),每个默认实现都会从URL
中提取Adaptive
参数值,并 以此为根据动静加载扩大点。而后,框架会应用不同的编译器,把实现类字符串编译为自适应 类并返回。本节次要解说字符串代码生成的实现原理。
生成代码的逻辑次要分为7 步,具体步骤如下:
- 生成package、import、类名称等头部信息。此处只会引入一个类
ExtensionLoader
。 为了不写其余类的import办法,其余办法调用时全副应用全门路。类名称会变为“接口名称+ $Adaptive ” 的格局。例如:Protocol
接口会生成Protocol$Adpative
。 - 遍历接口所有办法,获取办法的返回类型、参数类型、异样类型等。为第(3)步判 断是否为空值做筹备。
- 生成参数为空校验代码,如参数是否为空的校验。如果有近程调用,还会增加
Invocation
参数为空的校验。 - 生成默认实现类名称。如果©Adaptive注解中没有设定默认值,则依据类名称生成, 如
YyylnvokerWrapper
会被转换为yyy.invoker.wrapper
。生成的规定是一直找大写字母,并把它 们用连接起来。失去默认实现类名称后,还须要晓得这个实现是哪个扩大点的。 - 生成获取扩大点名称的代码。依据@Adaptive注解中配置的key值生成不同的获取代 码,例如:如果是
@Adaptive(“protocol”)
,则会生成url.getProtocol()
。 - 生成获取具体扩大实现类代码。最终还是通过getExtension(extName)办法获取自适 应扩大类的真正实现。如果依据URL中配置的key没有找到对应的实现类,则会应用第(4) 步中生成的默认实现类名称去找。
- 生成调用后果代码。
上面咱们用Dubbo。源码中自带的一个单元测试来演示代码生成过程,代码如下。
自适应类生成的代码
//SPI 配置文件中的配置impl1=org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl1
@SPIpublic interface HasAdaptiveExt { //自适应接口、echo 办法上有 @Adaptive注解 @Adaptive String echo(URL url, String s);}//在测试方法中调用这个自适应类SimpleExt ext = ExtensionLoader.getExtensionLoader(HasAdaptiveExt.class).getAdaptiveExtension();
生成一下自适应代码
package org.apache.dubbo.common.extension.adaptive;import org.apache.dubbo.rpc.model.ScopeModel;import org.apache.dubbo.rpc.model.ScopeModelUtil;public class HasAdaptiveExt$Adaptive implements org.apache.dubbo.common.extension.adaptive.HasAdaptiveExt { public java.lang.String echo(org.apache.dubbo.common.URL arg0, java.lang.String arg1) { if (arg0 == null) throw new IllegalArgumentException("url == null"); org.apache.dubbo.common.URL url = arg0; String extName = url.getParameter("has.adaptive.ext", "adaptive"); if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.common.extension.adaptive.HasAdaptiveExt) name from url (" + url.toString() + ") use keys([has.adaptive.ext])"); ScopeModel scopeModel = ScopeModelUtil.getOrDefault(url.getScopeModel(), org.apache.dubbo.common.extension.adaptive.HasAdaptiveExt.class); //实现类变为配置文件中的 org.apache.dubbo.common.extension.adaptive.impl.HasAdaptiveExt_ManualAdaptive org.apache.dubbo.common.extension.adaptive.HasAdaptiveExt extension = (org.apache.dubbo.common.extension.adaptive.HasAdaptiveExt)scopeModel.getExtensionLoader(org.apache.dubbo.common.extension.adaptive.HasAdaptiveExt.class).getExtension(extName); return extension.echo(arg0, arg1); }}
生成完代码之后就要对代码进行编译,生成一个新的Classo Dubbo中的编译器也是一个自 适应接口,但@Adaptive
注解是加在实现类AdaptiveCompiler
上的。这样一来AdaptiveCompiler
就会作为该自适应类的默认实现,不须要再做代码生成和编译就能够应用了。
如果一个接口上既有@SPI(”impl”)
注解,办法上又有@Adaptive(”impl2”)
注解,那么会以哪个key作为默认实现呢?由下面动静生成的SAdaptive类能够得悉,最终动静生成的实现办法会 是url.getParameter("impl2", "impl”)
,即优先通过@Adaptive
注解传入的key去查找扩大实现类; 如果没找到,则通过@SPI
注解中的key去查找;如果@SPI注解中没有默认值,则把类名转化 为key,再去查找。
getActivateExtension的实现原理
接下来,@Activate的
实现原理,先从它的入口办法说起。 getActivateExtension(URL url, String key, String group)
办法能够获取所有主动激活扩大点。参数别离是URL、URL中指定的key(多个则用逗号隔开)和URL中指定的组信息(group)0 其实现逻辑非常简单,当调用该办法时,主线流程分为4 步:
(1) 查看缓存,如果缓存中没有,则初始化所有扩大类实现的汇合。
(2) 遍历整个@Activate
注解汇合,依据传入URL匹配条件(匹配group、name等),得 到所有合乎激活条件的扩大类实现。而后依据@Activate
。中配置的before、after、order等参数进 行排序。
(3) 遍历所有用户自定义扩大类名称,依据用户URL配置的程序,调整扩大点激活程序,遵循用户在URL中配置的程序。
(4) 返回所有主动激活类汇合。
获取Activate
扩大类实现,也是通过getExtension
失去的。因而,能够认为getExtension
是其余两种Extension的基石。
此处有一点须要留神,如果URL的参数中传入了-default,
则所有的默认@Activate
都不会被激活,只有URL参数中指定的扩大点会被激活。如果传入了符号结尾的扩大点名, 则该扩大点也不会被主动激活。例如:-xxxx,示意名字为xxxx的扩大点不会被激活。
ExtensionInjector的实现原理
通过后面的介绍,咱们能够晓得ExtensionLoader
类是整个SPI的外围。然而,ExtensionLoader
类自身又是如何被创立的呢?
咱们晓得RegistryFactory
工厂类通过@Adaptive( ("protocol"})
注解动静查找注册核心实 现,依据URL中的protocol
参数动静抉择对应的注册核心工厂,并初始化具体的注册核心客户端。而实现这个个性的ExtensionLoader
类,自身又是通过工厂办法ExtensionInjector#getInstance
创立的,并且这个注射接口上也有SPI注解,还有多个实现。
ExtensionInjector接口
@SPIpublic interface ExtensionInjector { /** * Get instance of specify type and name. * * @param type object type. * @param name object name. * @return object instance. */ <T> T getInstance(Class<T> type, String name); @Override default void setExtensionAccessor(ExtensionAccessor extensionAccessor) { }}
既然注射接口有多个实现,那么是怎么确定应用哪个注射类实现的呢?咱们能够看到 AdaptiveExtensionInjector
这个实现类工厂上有@Adaptive
注解。因而,AdaptiveExtensionInjector
会作为一开始的默认实现。注射类之间的关系如图
<img src="https://qiniu-cdn.janker.top/oneblog/20220108200711372.png" style="zoom:50%;" />
能够看到,除了AdaptiveExtensionInjector
,还有SpiExtensionInjector
和SpringExtensionInjector
两个实现类。也就是说,咱们除了能够从Dubbo SPI
治理的容器中获取扩大点实例,还能够从Spring
容器中获取。
那么Dubbo
和Spring
容器之间是如何买通的呢?咱们先来看SpringExtensionInjector
的 实现,该注射类提供了初始化Spring
上下文的办法,在应用之前会调用init
办法,初始化SpringExtensionInjector
中的spring上下文。 当调用getInstance
获取扩大类时,上下文曾经筹备好,如果没有返回null,如果有就继续执行getOptionalBean
(获取Bean),代码如下
private ApplicationContext context; public ApplicationContext getContext() { return context; } public void init(ApplicationContext context) { this.context = context; } public <T> T getInstance(Class<T> type, String name) { if (context == null) { // ignore if spring context is not bound return null; } //check @SPI annotation if (type.isInterface() && type.isAnnotationPresent(SPI.class)) { return null; } T bean = getOptionalBean(context, name, type); if (bean != null) { return bean; } //logger.warn("No spring extension (bean) named:" + name + ", try to find an extension (bean) of type " + type.getName()); return null; }
那么Spring的上下文又是在什么时候被保存起来的呢?咱们能够通过代码搜寻得悉,在 ReferenceBean和ServiceBean中会调用静态方法保留Spring上下文,即一个服务被公布或被 援用的时候,对应的Spring上下文会被保留下来。
咱们再看一下SpiExtensionInjector
,次要就是获取扩大点接口对应的Adaptive
实现类。例 如:某个扩大点实现类 ClassA 上有@Adaptive
注解,则调用 SpiExtensionInjector#getInstance
会间接返回ClassA实例,代码如下。
SpiExtensionInjector
源码
public <T> T getInstance(Class<T> type, String name) { if (type.isInterface() && type.isAnnotationPresent(SPI.class)) { ExtensionLoader<T> loader = extensionAccessor.getExtensionLoader(type); if (loader == null) { return null; } //依据类型获取所有的扩大点加载器 if (!loader.getSupportedExtensions().isEmpty()) { return loader.getAdaptiveExtension(); } } return null;}
通过一番流转,最终还是回到了默认实现AdaptiveExtensionnInjector
是哪个,因为该注射类上有 @Adaptive
注解。这个默认注射器在初始化办法中就获取了所有扩大注射类并缓存起来,包含SpiExtensionnInjector
和 SpringExtensionnInjector
。 AdaptiveExtensionInjector
初始化放法如下。
//拓展注射器汇合 private List<ExtensionInjector> injectors = Collections.emptyList();public void initialize() throws IllegalStateException { //获取拓展注射器的ExtensionLoader ExtensionLoader<ExtensionInjector> loader = extensionAccessor.getExtensionLoader(ExtensionInjector.class); List<ExtensionInjector> list = new ArrayList<ExtensionInjector>(); //获取曾经反对的拓展类 for (String name : loader.getSupportedExtensions()) { //通过拓展类name 获取拓展类注射器 并增加到汇合中 injectors list.add(loader.getExtension(name)); } injectors = Collections.unmodifiableList(list); }
被AdaptiveExtensionnInjector
缓存的工厂会通过TreeSet
进行排序,SPI排在后面,Spring 排在前面。当调用getInstance
办法时,会遍历所有的工厂,先从SPI容器中获取扩大类;如 果没找到,则再从Spring容器中查找。咱们能够了解为,AdaptiveExtensionnInjector
持有了所 有的具体工厂实现,它的getInstance
办法中只是遍历了它持有的所有工厂,最终还是调用 SPI或Spring工厂实现的getInstance
办法。getInstance
办法代码如下:
public <T> T getInstance(Class<T> type, String name) { //遍历所有工厂进行查找,程序是 SPI ->Spring for (ExtensionInjector injector : injectors) { T extension = injector.getInstance(type, name); if (extension != null) { return extension; } } return null;}
拓展点动静编译的实现原理
Dubbo SPI
的自适应个性让整个框架非常灵活,而动静编译又是自适应个性的根底,因为 动静生成的自适应类只是字符串,须要通过编译能力失去真正的Class。尽管咱们能够应用反射 来动静代理一个类,然而在性能上和间接编译好的Class会有肯定的差距。Dubbo SPI通过代码 的动静生成,并配合动静编译器,灵便地在原始类根底上创立新的自适应类。上面介绍Dubbo SPI
动静编译器的品种及对应的实现原理。
总体构造
Dubbo中有三种代码编译器,别离是JDK
编译器、Javassist
编译器和AdaptiveCompiler
编译器。这几种编译器都实现了 Compiler
接口,编译器类之间的关系如图
Compiler
接口上含有一个SPI注解,注解的默认值是@SPI(”javassist”)
, 很显著,Javassist
编译器将作为默认编译器。如果用户想扭转默认编译器,则能够通过 <dubbo:application compiler="jdk" />标签进行配置。
AdaptiveCompiler
下面有@Adaptive
注解,阐明AdaptiveCompiler
会固定为默认实现,这 个Compiler
的次要作用和AdaptiveExtensionInjector
类似,就是为了治理其余Compiler
,代码如下:
AdaptiveCompiler的逻辑
public static void setDefaultCompiler(String compiler) { //设置默认的编译器名称 DEFAULT_COMPILER = compiler; } @Override public Class<?> compile(String code, ClassLoader classLoader) { Compiler compiler; ExtensionLoader<Compiler> loader = frameworkModel.getExtensionLoader(Compiler.class); String name = DEFAULT_COMPILER; // copy reference if (name != null && name.length() > 0) { compiler = loader.getExtension(name); } else { compiler = loader.getDefaultExtension(); } //通过ExtensionLoader获取对应的译器扩大类实现,调用真正的compile做编译 return compiler.compile(code, classLoader); }
AdaptiveCompiler#setDefaultCompiler
办法会在ApplicationConfig 中被调用,也就是Dubbo 在启动时,会解析配置中的<dubbo:application compiler="jdk" />
标签,获取设置的值,初始化对应的编译器。如果没有标签设置,则应用@SPI(“javassist”)
中的设置,即JavassistCompiler
。而后看一下AbstpactCompiler
,它是一个抽象类,无奈实例化,但在外面封装了通用的模 板逻辑。还定义了一个形象办法doCompile
, 留给子类来实现具体的编译逻辑。 JavassistCompiler
和JdkCompiler
都实现了这个形象办法。
AbstractCompiler
的次要形象逻辑如下:
- 通过正则匹配出包门路、类名,再依据包门路、类名拼接出全门路类名。
- 尝试通过
Class.forName
加载该类并返回,避免反复编译。如果类加载器中没有这个类,则进入第3 步。 - 调用
doCompile
办法进行编译。这个形象办法由子类实现。 上面将介绍两种编译器的具体实现。
上面将介绍两种编译器的具体实现。
Javassist动静代码编译
Java中动静生成Class的形式有很多,能够间接基于字节码的形式生成,常见的工具库有CGLIB
、ASM
、Javassist
等。而自适应扩大点应用了生成字符串代码再编译为Class的形式。
Dubbo中JavassistCompiler
的实现原理也很清晰了。因为 咱们之前曾经生成了代码字符串,因而在JavassistCompiler
中,就是一直通过正则表达式匹 配不同部位的代码,而后调用Javassist
库中的API生成不同部位的代码,最初失去一个残缺的 Class对象。具体步骤如下:
(1) 初始化Javassist
,设置默认参数,如设置以后的classpath
。
(2) 通过正则匹配出所有import
的包,并应用Javassist
增加import
。
(3) 通过正则匹配出所有extends
的包,创立Class
对象,并应用Javassist
增加extends
。
(4) 通过正则匹配出所有implements
包,并应用Javassist
增加implements
。
(5) 通过正则匹配出类外面所有内容,即失去{}
中的内容,再通过正则匹配出所有办法, 并应用Javassist
增加类办法。
(6) 生成Class对象。
JavassistCompiler
继承了抽象类Abstractcompiler
,须要实现父类定义的一个形象办法doCompileo
以上步骤就是整个doCompile
办法在JavassistCompiler
中的实现。
JDK动静代码编译
JdkCompiler
是Dubbo
编译器的另一种实现,应用了 JDK
自带的编译器,原生JDK编译器 包位于 javax.tools
下。次要应用了三个货色:JavaFileObject 接口、 ForwardingJavaFileManager
接口、JavaCompiler.CompilationTask
办法。整个动静编译过程 能够简略地总结为:首先初始化一个JavaFileObject
对象,并把代码字符串作为参数传入结构 办法,而后调用JavaCompiler.CompilationTask
办法编译出具体的类。JavaFileManager
负责 治理类文件的输出/输入地位。以下是每个接口/办法的简要介绍:
JavaFileObject
接口。字符串代码会被包装成一个文件对象,并提供获取二进制流 的接口。Dubbo框架中的JavaFileObjectlmpl
类能够看作该接口一种扩大实现,构造方法中需 要传入生成好的字符串代码,此文件对象的输出和输入都是ByteArray
流。因为SimpleJavaFileObject
、JavaFileObject
之间的关系属于JDK中的常识,因而在本篇不深刻解说,有趣味的读者能够自行查看JDK源码。JavaFileManager
接口。次要管理文件的读取和输入地位。JDK中没有能够间接应用 的实现类,惟一的实现类ForwardingJavaFileManager
结构器又是protected
类型。因而Dubbo
中 定制化实现了一个JavaFileManagerlmpl
类,并通过一个自定义类加载器ClassLoaderlmpl
完 成资源的加载。JavaCompiler.CompilationTask
把JavaFileObject
对象编译成具体的类。
小结
本章的内容比拟多,首先介绍了 Dubbo SPI
的一些概要信息,包含与Java SPI
的区别、Dubbo SPI
的新个性、配置标准和外部缓存等。其次介绍了 Dubbo SPI
中最重要的三个注解:@SPI
、 @Adaptive
、@Activate
,解说了这几个注解的作用及实现原理。而后联合ExtensionLoader
类 的源码介绍了整个Dubbo SPI
中最要害的三个入口: getExtension
、getAdaptiveExtension
、 getActivateExtension
,并解说了创立 ExtensionLoader
的工厂(ExtensionInjector
)的工作原 理。最初还解说了自适应机制中动静编译的实现原理。
微信搜寻:爪哇干货分享,交个敌人,进技术交换群