前言
本文是**【Dubbo系列】无处不在的SPI** 系列文章的第四小节。
【Dubbo系列】无处不在的SPI:崭露头角 主要介绍了JDK的SPI机制,以及ServiceLoader的使用。
【Dubbo系列】无处不在的SPI之羽翼渐丰 补充了SPI和API的区别,并对JDK的SPI进行改写,通过ExtensionLoader实现了仅加载所需要的扩展实现类,达到节约资源的目的。
【Dubbo系列】无处不在的SPI之独当一面 通过静态工厂生成ExtensionLoader单一实例对象,并通过反射实现了扩展点依赖注入功能。
扩展点自适应
前几文实现了一个简单的SPI机制,但是还有一些不完善的地方,比如在使用扩展点时,如何动态的确定使用的是哪个扩展点实现。
举个例子:
比如一个扩展点SimpleExt
:
@SPI("impl1")
public interface SimpleExt {
@Adaptive
String echo(URL url, String s);
@Adaptive({"key1", "key2"})
String yell(URL url, String s);
String bang(URL url, int i);
}
他有3个实现扩展点实现:SimpleExtImpl1
、SimpleExtImpl2
、SimpleExtImpl3
,那么在调用方法时,如何动态确定使用的是哪个扩展点呢?
Dubbo提出了一种思路,可以通过在运行期动态生成一个 自适应扩展点实现类 来要解决上面的问题:
- 首先需要确定扩展点中哪些方法需要调用不同的扩展点实现,可以通过
@Adaptive
注解来标注需要生成自适应扩展点实现的类和方法。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
String[] value() default {};
}
标注在扩展点实现类上时,表示该类为自适应扩展点实现类;
标注在扩展点方法上时,比如例子中的echo
和yell
方法,那么表示这两个方法需要根据 条件 调用对应的扩展点实现。
-
其次增加使用具体扩展点实现的条件,可以通过参数 URL 来指定。URL 包括协议、权限信息、参数等一系列信息。
-
根据注解和条件,生成自适应扩展点实现类,生成流程图如下:
-
通过
ExtensionLoader.getAdaptiveExtension
方法获得扩展点的是自适应实现类,使用cachedAdaptiveInstance
缓存实例对象。因为我们使用静态工厂来创建单一实例对象ExtensionLoader
,所以需要使用volatile
修饰实例对象以保证线程安全性。// 创建两个缓存,一个保存创建成功的自适应扩展点实现类,一个保存失败后的异常信息, private final Holder<Object> cachedAdaptiveInstance = new Holder<Object> (); private volatile Throwable createAdaptiveInstanceError = null; @SuppressWarnings("unchecked") public T getAdaptiveExtension() { // 查找实例 Object instance = cachedAdaptiveInstance.get (); if (instance == null) { // 创建异常缓存,避免失败后重复创建,浪费系统资源 if (createAdaptiveInstanceError == null) { synchronized (cachedAdaptiveInstance) { instance = cachedAdaptiveInstance.get (); if (instance == null) { try { // 实例对象为空,则新增createAdaptiveExtension() instance = createAdaptiveExtension (); cachedAdaptiveInstance.set (instance); } catch (Throwable t) { // 捕获异常,将异常添加到缓存;再抛出,防止吞掉异常 createAdaptiveInstanceError = t; throw new IllegalStateException ("fail to create adaptive instance: " + t.toString (), t); } } } } else { throw new IllegalStateException ("fail to create adaptive instance: " + createAdaptiveInstanceError.toString (), createAdaptiveInstanceError); } } return (T) instance; }
-
第一步中如果实例对象为空,进入
createAdaptiveExtension
方法:private T createAdaptiveExtension() { try { // newInstance只可以调用无参的构造函数,如果无法创建对象,会抛出InstantiationException异常 return injectExtension ((T) getAdaptiveExtensionClass ().newInstance ()); } catch (Exception e) { throw new IllegalStateException (" can not create adaptiveExtensionClass " + type + ", cause: " + e.getMessage ()); } }
-
接下来调用
getAdaptiveExtensionClass
方法生成扩展类代码:// 这里需要注意,他只持有带有@Adaptive注解的扩展点实现类,其他的都需要通過createAdaptiveExtensionClass动态生成 private volatile Class<?> cachedAdaptiveClass = null; private Class<?> getAdaptiveExtensionClass() { // 获取所有扩展点实现类 getExtensionClasses (); if (cachedAdaptiveClass != null) { return cachedAdaptiveClass; } // 调用createAdaptiveExtensionClass生成AdaptiveClass return cachedAdaptiveClass = createAdaptiveExtensionClass (); }
这里包含两种情况,一种是直接拿
cachedAdaptiveClass
里对象,一种通过createAdaptiveExtensionClass
生成。那么cachedAdaptiveClass
中的值是什么呢?还记得之前我们说过在类上面的
@Adaptive
注解,在getExtensionClasses
方法中,如果遇到类上面有@Adaptive
注解的,将该类放入cachedAdaptiveClass
。
执行流程如下图:
其中loadClass
会把扩展实现类中有@Adaptive
注解的类放入cachedAdaptiveClass
。这样getAdaptiveExtensionClass
时会直接获取。
private void loadClass(Map<String, Class<?>> extensionClasses, Class<?> clazz, String name) {
// 判断是类上是否有Adaptive注解,如果包含Adaptive注解,则加入cachedAdaptiveClass缓存
// Dubbo 目前只有两个扩展点使用类上Adaptive注解,Compile 和 ExtensionFactory
if (clazz.isAnnotationPresent (Adaptive.class)) {
if (cachedAdaptiveClass == null) {
cachedAdaptiveClass = clazz;
} else {
// 判断唯一性
if (cachedAdaptiveClass != clazz) {
throw new IllegalStateException (" adaptiveClass is redundance : " + cachedAdaptiveClass.getClass ().getName ());
}
}
} else{
extensionClasses.put (name, clazz);
}
}
-
如果
cachedAdaptiveClass
为null
,那么需要通过createAdaptiveExtensionClass
来生成自适应扩展点实现类:// 生成自适应扩展点实现类 private Class<?> createAdaptiveExtensionClass() { // 生成自适应类的代码 String code = createAdaptiveExtensionClassCode (); ClassLoader classLoader = findClassLoader (); com.ryan.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader (com.ryan.compiler.Compiler.class).getAdaptiveExtension (); return compiler.compile (code, classLoader); }
最后调用
compiler.compile (code, classLoader);
。
Compiler
也是一个扩展点,用来生成自适应扩展点Class类对象:
@SPI("javassist")
public interface Compiler {
Class<?> compile(String code, ClassLoader classLoader);
}
他默认的自适应扩展点实现类AdaptiveCompiler
,根据第三步可知,该实现类会放入cachedAdaptiveClass
中,在调用getAdaptiveExtension
时,返回该实现类:
// 代理模式,该类的compile方法实际上执行Compile默认扩展点实现类
// JavassistCompiler的Compile方法
@Adaptive
public class AdaptiveCompiler implements Compiler {
//
@Override
public Class<?> compile(String code, ClassLoader classLoader) {
Compiler compiler;
ExtensionLoader<Compiler> loader = ExtensionLoader.getExtensionLoader (Compiler.class);
compiler = loader.getDefaultExtension ();
return compiler.compile (code, classLoader);
}
}
createAdaptiveExtensionClass
方法最后compiler.compile (code, classLoader);
实际上为调用JavassistCompiler
的Complie
方法。
看一下Compile扩展点的类图:
Compile
作为一个扩展点,为了符合框架中对自适应扩展点的统一调用规范,使用注解在AdaptiveCompiler
类上,并使用委托代理模式,实际调用为Compile扩展点的默认实现类JavassistCompiler
的Compile方法。
-
JavassistCompiler
通过compile
方法调用javassist
动态生成class。通过
createAdaptciveExtensionClassCode
生成SimpleExt
的自适应扩展点code如下:
public class SimpleExt$Adaptive implements com.ryan.myspi.adaptive.SimpleExt {
public java.lang.String yell(com.ryan.common.URL arg0, java.lang.String arg1) {
if (arg0 == null) throw new IllegalArgumentException ("url == null");
com.ryan.common.URL url = arg0;
String extName = url.getParameter ("key1", url.getParameter ("key2", "impl1"));
com.ryan.myspi.adaptive.SimpleExt extension = null;
try {
extension = (com.ryan.myspi.adaptive.SimpleExt) ExtensionLoader.getExtensionLoader (com.ryan.myspi.adaptive.SimpleExt.class).getExtension (extName);
} catch (Exception e) {
extension = (com.ryan.myspi.adaptive.SimpleExt) ExtensionLoader.getExtensionLoader (com.ryan.myspi.adaptive.SimpleExt.class).getExtension ("impl1");
}
return extension.yell (arg0, arg1);
}
public java.lang.String echo(com.ryan.common.URL arg0, java.lang.String arg1) {
if (arg0 == null) throw new IllegalArgumentException ("url == null");
com.ryan.common.URL url = arg0;
String extName = url.getParameter ("simple.ext", "impl1");
com.ryan.myspi.adaptive.SimpleExt extension = null;
try {
extension = (com.ryan.myspi.adaptive.SimpleExt) ExtensionLoader.getExtensionLoader (com.ryan.myspi.adaptive.SimpleExt.class).getExtension (extName);
} catch (Exception e) {
extension = (com.ryan.myspi.adaptive.SimpleExt) ExtensionLoader.getExtensionLoader (com.ryan.myspi.adaptive.SimpleExt.class).getExtension ("impl1");
}
return extension.echo (arg0, arg1);
}
public java.lang.String bang(com.ryan.common.URL arg0, int arg1) {
throw new UnsupportedOperationException ("method public abstract java.lang.String com.ryan.myspi.adaptive.SimpleExt.bang(com.ryan.common.URL,int) of interface com.ryan.myspi.adaptive.SimpleExt is not adaptive method!");
}
}
为了节约篇幅,这里直接展示生成的code,如果想了解细节,可以参考原文代码。
在SimpleExt$Adaptive
中:
echo()
:获取SimpleExt
默认扩展点实现类,调用该实现类的echo
方法。
yell()
:根据注解@Adaptive({"key1", "key2"})
中的key1和key2,然后获取URL参数中的对应值也就是扩展点实现类别名,获取该实现类并调用yell
方法。
bang()
:该方法由于没有@Adaptive
注解,直接抛出UnsupportedOperationException
异常。
由于SimpleExt$Adaptive
是在运行时动态生成的,可以看到其类似于代理模式,在方法中根据URL获取对应的扩展点实现类,然后调用对应的方法。
当然,我们也可以手动创建一个自适应扩展点:
//@Adaptive
public class SimpleExt$Adaptive implements com.ryan.myspi.adaptive.SimpleExt {
---
}
那么为什么要使用javassist
呢? 对于框架中大量的扩展点,相同的逻辑则带来了很多重复的工作量,并且及易引入bug。
因此,使用javassist
生成自适应扩展点使得对框架的扩展的变得更加优雅;他将生成自适应扩展点的逻辑进行了抽象,统一通过该方式来生成,减少了框架开发者的工作量。
对自适应扩展点实现机制进行单元测试:
@Test
public void getAdaptiveExtension(){
SimpleExt ext = ExtensionLoader.getExtensionLoader(SimpleExt.class).getAdaptiveExtension();
Map<String, String> map = new HashMap<String, String>();
map.put("key2", "impl2");
URL url = new URL("p1", "1.2.3.4", 1010, "path1", map);
String echo = ext.yell(url, "haha");
assertEquals("Ext1Impl2-yell", echo);
url = url.addParameter("key1", "impl3"); // note: URL is value's type
echo = ext.yell(url, "haha");
assertEquals("Ext1Impl3-yell", echo);
}
IOC注入扩展点依赖
在【Dubbo系列】无处不在的SPI之独当一面 中,对扩展点依赖注入直接使用了ExtensionLoader
的工厂方法。在Dubbo中是不建议这样使用的。作为框架,Dubbo尽可能的支持良好的扩展功能,被注入的扩展点实例可以通过SPI的方式产生,也可以为Spring容器中的bean实例,或者自定义的容器。
因此抽象一个扩展点ExtensionFactory
,用来生产扩展点依赖注入的实例对象,达到控制反转的目的:
@SPI
public interface ExtensionFactory {
<T> T getExtension(Class<T> type, String name);
}
ExtensionFactory
做为一个扩展点可以按需求增加扩展实现类,比如:SpringExtensionFactory
、SpiExtensionFactory
、AdaptiveExtensionFactory
等。
接着通过在ExtensionLoader
中引入Extensionfactory
:
private final ExtensionFactory objectFactory;
// 构造函数改为私有
private ExtensionLoader(Class<T> type) {
this.type = type;
objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}
同时在injectExtension
方法中通过objectFactory.getExtension (pt, property)
获取扩展点注入实例:
private T injectExtension(T instance) {
if (instance == null) {
throw new IllegalArgumentException (" injectExtension param instance == null");
}
// 通过反射注入扩展类
for (Method method : instance.getClass ().getMethods ()) {
if (method.getName ().startsWith ("set")
&& method.getParameterTypes ().length == 1
&& Modifier.isPublic (method.getModifiers ())) {
// 注入
Class<?> pt = method.getParameterTypes ()[0];
// 获取参数
try {
String methodName = method.getName ();
String property = methodName.length () > 3 ? methodName.substring (3, 4).toLowerCase () + methodName.substring (4) : "";
// Object ob = ExtensionLoader.getExtensionLoader (pt).getExtension (property);
Object ob = objectFactory.getExtension (pt, property);
// 反射调用set方法注入该扩展类
if (ob != null) {
method.invoke (instance, ob);
}
} catch (Exception e) {
System.out.println ("fail to inject via method :" + method.getName ());
}
}
}
return instance;
}
至此,我们得到经过依赖注入的扩展点实现类。
注:
ExtensionLoader
中getAdaptiveExtension
和getExtensionLoader
方法返回的对象不同,getAdaptiveExtension
返回自适应扩展点实现类对象;
getExtensionLoader
返回不包含自适应扩展点的所有扩展点实现类对象。在方法级别严格遵守了单一职责原则。
小结
主要研究了Dubbo的自适应扩展点和IOC依赖注入的实现原理。并在前几个小结的基础上对代码进行完善。
参考
文中代码地址:https://github.com/non-trivial/myspi/tree/IOC
JAVA programming ASSISTant :http://www.javassist.org/
Dubbo官方文档 :http://dubbo.apache.org/zh-cn/docs/dev/coding.html
推荐阅读:
线程池的前世今生
了解更多请关注微信公众号