1. Java SPI
1.1 概念
SPI全称是Service Provider Interface,是JDK内置的一种服务提供发现功能,一种动态替换发现的机制。
1.2 规范
使用SPI需要遵循下面的规范:
设置META-INF目录,并在下面新建一个services目录
在上面创建的目录下,放置配置文件
使用ServiceLoad.load()方法进行加载
1.3 示例
首先新建一个maven项目,结构如下:下面分步骤进行描述。
首先新建一个HelloService接口:
public interface HelloInterface {
public void sayHello();
}
以及该接口的两个实例:
public class ImageHello implements HelloInterface {
public void sayHello() {
System.out.println("Image hello!");
}
}
public class TextHello implements HelloInterface {
public void sayHello() {
System.out.println("Text hello");
}
}
然后在resources目录下的META-INF.services目录下新建一个配置文件,文件名要求是接口全路径名,内容是要实现的接口实现类:
com.spiexample.impl.ImageHello
com.spiexample.impl.TextHello
并且文件是UTF-8编码的。
最后在SPIMain这个启动类中进行测试:
public class SPIMain {
public static void main(String[] args){
ServiceLoader<HelloInterface> serviceLoader = ServiceLoader.load(HelloInterface.class);
if (serviceLoader != null){
for(HelloInterface helloInterface : serviceLoader){
helloInterface.sayHello();
}
}
}
}
最终输出结果为:
Image hello!
Text hello
说明已经成功创建出接口的两个实现类的对象并执行方法了。
2. Dubbo SPI
2.1 介绍
Dubbo SPI相比于Java SPI而言,做了一定的优化和改进。JDK标准的SPI会一次性实例化扩展点所有实现,如果有扩展实现则初始化很耗时,如果没有用上也加载,则浪费资源。如第一小节的例子中,HelloInterface接口有很多实现类,ServiceLoader.load方法返回的serviceLoader对象中封装了所有实现类的实例。所以ServiceLoader实现了Iterable接口,以遍历出所有的实现类。
Dubbo SPI则不然,它只加载配置文件中的类,并分成不同的种类缓存在内存中,而不会全部初始化,在性能上有更好的表现。
下面是一个例子,在第一小节的例子上通过Dubbo SPI进行改进。
2.2 示例
首先修改接口类:
@SPI("impl")
public interface HelloInterface {
public void sayHello();
}
添加了一个注解,并且设置属性为impl,表示接口默认实现为impl。
@SPI注解:
当该注解标记在接口上时,表示这是一个Dubbo SPI接口,即是一个扩展点。SPI注解有一个value属性,通过这个属性,我们可以传入不同的参数来设置这个接口的默认实现类。
然后再定义一个这个接口的实现类:
public class NewHello implements HelloInterface {
public void sayHello() {
System.out.println("new hello!");
}
}
然后在META-INF目录下新建一个dubbo目录,再新建一个配置文件,文件名也是要求是接口HelloInterface的全路径名,内容是:
impl=com.spiexample.impl.NewHello
impl即对应了接口的一个实现类。
写完这些后,最后main方法中调用如下:
HelloInterface helloInterface = ExtensionLoader.
getExtensionLoader(HelloInterface.class).
getDefaultExtension();
helloInterface.sayHello();
最终就成功打印new hello,说明调用实例的就是指定的NewHello。
2.3 规范
Dubbo SPI兼容了Java SPI的配置路径和内容配置方式。在Dubbo启动的时候,会默认扫这三个目录下的配置文件:META-INF/services/,META-INF/dubbo/,META-INF/dubbo/internal/。
3. 扩展点的特性
扩展类一共包含四种特性:自动包装,自动加载,自适应和自动激活。
3.1 自适应
在第二小节中提到,Dubbo通过@SPI("impl")
注解中的属性来明确默认使用接口的哪个实现类,那如果实际场景中某些条件使用实现类A,其他条件使用实现类B呢?这种灵活的场景再直接硬编码写死就不合适了。为此下面再看下Dubbo提供的另外一个注解Adaptive,它实现了Dubbo扩展点的一个特性:自适应。
3.1.1 示例
这个注解一般声明在接口的方法上。下面在第二节的示例基础上进行修改。
首先还是定义接口:
@SPI
public interface HelloInterface {
@Adaptive({"key"})
void printInfo(URL url);
}
其中:
SPI注解上没有设置默认值了
方法定义上加了一个Adaptive注解,该注解可以传入多个字符串,表示的参数的名字
方法传入了参数,类型为URL。这个URL不是我们常用的那个URL,而是Dubbo自定义的一个类。那这个类有什么用呢?实际上,某一个接口有多个实现类的时候,就是通过往URL里面传参数,来指定我们想调用哪个实现类的该方法的。口说无凭,下面继续看。
之前我们定义了HelloInterface接口的一个实现类,名为NewHello,下面再定义一个:
public class NewHello2 implements HelloInterface {
public void sayHello() {
System.out.println("new hello2!");
}
}
同样的,在dubbo配置文件中加上这个实现类的配置:
impl=com.spiexample.impl.NewHello
impl2=com.spiexample.impl.NewHello2
最后,写一个main方法进行测试:
URL url = URL.valueOf("test://localhost/test").
addParameter("key","impl2");
HelloInterface helloInterface = ExtensionLoader.
getExtensionLoader(HelloInterface.class).
getAdaptiveExtension();
helloInterface.sayHello();
可以看到:
- 在URL中传入参数key,它的值设置为impl2,因此最后执行
helloInterface.sayHello()
的时候,实际执行的就是NewHello2.sayHello()
。 - 要注意上述代码中获取接口实例是通过调用ExtensionLoader类的getAdaptiveExtension()方法,而不是第二小节的getDefaultExtension()方法。这里涉及到一个概念,叫Dubbo扩展点的自适应特性,这个特性在上述代码中也很好的体现出来了,就是通过Adaptive注解,可以动态地通过URL中的参数来确定要使用的是哪个具体的实现类。
3.2 自动包装
自动包装是Dubbo扩展点的另外一个特性,这个特性实现了装饰器设计模式(关于装饰器模式的思想,可以看另一篇博客的介绍)。
3.2.1 装饰器模式
装饰器模式的编程范式如下:
public class XxxWrapper implements Xxx {
private Xxx xxx;
public XxxWrapper(Xxx xxx){
this.xxx = xxx;
}
@Override
... // 重写的方法
}
装饰器模式最重要的特点就是:构造函数的入参和要实现的接口类型一致。
3.2.2 示例
下面我们在前面的示例基础上修改,体验下这个自动包装是如何实现装饰器模式的。
首先添加一个装饰器类:
public class HelloInterfaceWrapper implements HelloInterface {
private HelloInterface helloInterface;
public HelloInterfaceWrapper(HelloInterface helloInterface){
this.helloInterface = helloInterface;
}
@Override
public void sayHello(URL url) {
System.out.println("start print");
helloInterface.sayHello(url);
System.out.println("end print");
}
}
一般如果没有使用dubbo的时候,如果想使用上面这个wrapper类来实现一个装饰器模式的时候,首先要做的就是注入一个接口的实现类,然后调用具体的方法的时候,就可以通过这个wrapper类在方法前后做一些额外的操作,实现类似于切面的效果。引入了Dubbo之后,就不需要自己手动注入这个接口实现类了,而是自动注入的。
为了实现自动包装,还是得把这个Wrapper类定义到dubbo的配置文件中:
impl=com.spiexample.impl.NewHello
impl2=com.spiexample.impl.NewHello
common=com.spiexample.impl.XxxWrapper
这样这个接口就有三个实现类了。
最后再把第三小节示例的main方法原样拷贝过来:
URL url = URL.valueOf("test://localhost/test").
addParameter("key","impl2");
HelloInterface helloInterface = ExtensionLoader.
getExtensionLoader(HelloInterface.class).
getAdaptiveExtension();
helloInterface.sayHello();
最终输入如下:
start print
hello world2
end print
可以看到,在hello的前后都打印了start和end,说明先执行了Wrapper类,里面包含了我们指定的imp2对应的那个类。这个就是扩展点的自动包装功能:自动扫描配置文件中定义的类是否是Wrapper类,是的话就用其对实际要调用的类进行自动包装。
3.3 自动加载
我们知道Spring依赖注入有构造注入和属性注入等形式。上一小节提到了,Dubbo通过自动包装来实现构造注入,将一个扩展类注入到另一个扩展类中。那除了构造注入外,同样也有属性注入的形式,这种特性在Dubbo中可称为扩展点的自动加载。
下面对前一小节的包装类进行修改如下:
public class HelloInterfaceWrapper implements HelloInterface {
private HelloInterface helloInterface;
public void setHelloInterface(HelloInterface helloInterface) {
this.helloInterface = helloInterface;
}
@Override
public void sayHello(URL url) {
System.out.println("start print");
helloInterface.sayHello(url);
System.out.println("end print");
}
}
修改后,删除了构造函数,只写了一个setter方法,其他代码都不用调整,当main方法调用seyHello方法时发现,最终效果和前面自动包装实现的效果是一样的,这就说明自动加载(我更喜欢称为属性注入)成功了。
3.4 自动激活
@Activate注解可以标记在类,接口,枚举类和方法上。主要使用在有多个扩展点实现,需要根据不同条件被激活的场景中。
如我们使用过滤器都是多个同时使用,即Filter需要多个同时激活,因为每个Filter实现的是不同的功能:
// 无条件自动激活
@Activate
public class XxxFilter implements Filter {
// ...
}
// 当配置了xxx参数,并且参数为有效值时激活,比如配了cache="lru",自动激活CacheFilter
@Activate(cache="lru")
public class CacheFilter implements Filter {
// ...
}
// 只对提供方激活,group可选"provider"或"consumer",用这个说明应用已经注册到Dubbo注册中心了
@Activate(group = "provider", value = "xxx")
public class XxxFilter implements Filter {
// ...
}
定义好扩展类后,就可以通过下面的形式进行获取:
URL url = URL.valueOf("test://localhost/test");
url = url.addParameter("cache", "lru");
// 指明了使用参数cache=lru,group=provider的扩展类
List<Filter> filters = ExtensionLoader.
getExtensionLoader(Filter.class).
getActivateExtension(url,new String[]{},"provider");
可以看到此时返回的扩展类实例是List形式的,即多个的。
4. ExtensionLoader原理
ExtensionLoader是扩展机制的主要逻辑类,前面说到获取扩展类有下面三种方式:
ExtensionLoader.getExtensionLoader(HelloInterface.class).getDefaultExtension();
ExtensionLoader.getExtensionLoader(HelloInterface.class).getAdaptiveExtension();
ExtensionLoader.getExtensionLoader(HelloInterface.class).getActivateExtension();
下面就详细讲解getDefaultExtension,getAdaptiveExtension,getActivateExtension这个三个方法的实现。
4.1 getDefaultExtension
该方法源码如下:
public T getDefaultExtension() {
this.getExtensionClasses();
return !StringUtils.isBlank(this.cachedDefaultName) && !"true".equals(this.cachedDefaultName) ? this.getExtension(this.cachedDefaultName) : null;
}
可以看到该方法判断cachedDefaultName变量,进而判断是否调用getExtension方法。在第二小节使用getDefaultExtension方法的示例中,我们给方法添加了SPI注解:
@SPI("impl")
public interface HelloInterface {
public void sayHello();
}
并且在配置文件中定义:
impl=com.spiexample.impl.NewHello
Dubbo在初始化的时候,会自动扫描带有SPI注解的接口,并且根据注解中指定的参数在配置文件中找到默认要加载的类,并将该类名赋值给cachedDefaultName上。
所以getDefaultExtension方法的关键还是在getExtension方法上。下面就看下该方法的实现:
public T getExtension(String name) {
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("Extension name == null");
} else if ("true".equals(name)) {
return this.getDefaultExtension();
} else {
Holder<Object> holder = this.getOrCreateHolder(name);
Object instance = holder.get();
if (instance == null) {
synchronized(holder) {
instance = holder.get();
if (instance == null) {
instance = this.createExtension(name);
holder.set(instance);
}
}
}
return instance;
}
}
该方法中需要关注getOrCreateHolder和createExtension这两个方法的实现。
4.1.1 获取Holder
前面示例中我们传入的参数为:com.spiexample.impl.NewHello
,则会调用getOrCreateHolder方法来获取Holder。什么是Holder呢?
public class Holder<T> {
private volatile T value;
public Holder() {
}
public void set(T value) {
this.value = value;
}
public T get() {
return this.value;
}
}
看来就只是将要加载的对象包裹起来而已。
接下来再看下getOrCreateHolder方法是如何获取Holder的:
private Holder<Object> getOrCreateHolder(String name) {
Holder<Object> holder = (Holder)this.cachedInstances.get(name);
if (holder == null) {
this.cachedInstances.putIfAbsent(name, new Holder());
holder = (Holder)this.cachedInstances.get(name);
}
return holder;
}
private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap();
代码中用到了一个cachedInstances变量来缓存Holder,如果没有就new一个。
4.1.2 创建扩展对象
createExtension方法源码如下:
private T createExtension(String name) {
// 根据类名获取对应的Class对象,该方法中也定义了cachedClasses变量进行缓存
Class<?> clazz = (Class)this.getExtensionClasses().get(name);
if (clazz == null) {
throw this.findException(name); // 获取不到,则抛异常
} else {
try {
// 根据Class对象从缓存对象EXTENSION_INSTANCES中获取实例
T instance = EXTENSION_INSTANCES.get(clazz);
if (instance == null) { // 如果对象不存在,则new一个
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = EXTENSION_INSTANCES.get(clazz);
}
// 实现扩展点的自动加载特性,如果需要其他扩展类,进行注入
this.injectExtension(instance);
// 实现扩展点的自动包装特性,创建包装类,并按需将其他扩展类注入到包装类中
Set<Class<?>> wrapperClasses = this.cachedWrapperClasses;
Class wrapperClass;
if (CollectionUtils.isNotEmpty(wrapperClasses)) {
for(Iterator var5 = wrapperClasses.iterator(); var5.hasNext(); instance = this.injectExtension(wrapperClass.getConstructor(this.type).newInstance(instance))) {
wrapperClass = (Class)var5.next();
}
}
// 调用每个实例本身的初始化方法
this.initExtension(instance);
return instance;
} catch (Throwable var7) {
throw new IllegalStateException("...");
}
}
}
这个方法还算简单,下面贴出injectExtension方法的源码,分析下它是如何实现自动注入的:
private T injectExtension(T instance) {
// objectFactory中保存了所有实例化后的扩展对象
if (this.objectFactory == 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];
//如果是setter方法,并且没有声明禁止注入
if (this.isSetter(method) && method.getAnnotation(DisableInject.class) == null) {
// 获取setter方法入参类型,即要注入的类名
Class<?> pt = method.getParameterTypes()[0];
if (!ReflectUtils.isPrimitives(pt)) {
try {
String property = this.getSetterProperty(method);
// 从objectFactory中获取要注入的类的对象
Object object = this.objectFactory.getExtension(pt, property);
if (object != null) {
// 如果获取了这个要注入的扩展类实例,则调用这个set方法将这个实例注入进去
method.invoke(instance, object);
}
} catch (Exception var9) {
logger.error("...");
}
}
}
}
} catch (Exception var10) {
logger.error(var10.getMessage(), var10);
}
return instance;
}
}
看了injectExtension这个方法的源码,思路也很清楚,就是根据反射将其他扩展类注入到当前扩展类中。也正是这个方法,从而实现了Dubbo扩展点的两个特性:自动包装和自动加载。
4.2 getAdaptiveExtension
当我们使用扩展点的自适应特性时,获取扩展对象实例就需要使用这个方法。该方法定义如下:
public T getAdaptiveExtension() {
// 从缓存中获取实例
Object instance = this.cachedAdaptiveInstance.get();
if (instance == null) {
// 该缓存变量也是以Holder形式来包装扩展类对象的
Holder var2 = this.cachedAdaptiveInstance;
synchronized(this.cachedAdaptiveInstance) {
instance = this.cachedAdaptiveInstance.get();
// 创建实例并放入缓存中
try {
instance = this.createAdaptiveExtension();
this.cachedAdaptiveInstance.set(instance);
} catch (Throwable var5) {
throw new IllegalStateException("...");
}
}
}
return instance;
}
private final Holder<Object> cachedAdaptiveInstance = new Holder();
创建实例主要在方法createAdaptiveExtension中实现,下面进行分析。
4.2.1 创建扩展对象
private T createAdaptiveExtension() {
try {
return this.injectExtension(this.getAdaptiveExtensionClass().newInstance());
} catch (Exception var2) {
throw new IllegalStateException("...");
}
}
这里调用的injectExtension方法前面也已经分析过了,下面主要看下getAdaptiveExtensionClass方法是如何获取Class对象的:
private Class<?> getAdaptiveExtensionClass() {
this.getExtensionClasses();
return this.cachedAdaptiveClass != null ? this.cachedAdaptiveClass : (this.cachedAdaptiveClass = this.createAdaptiveExtensionClass());
}
下面看下createAdaptiveExtensionClass方法的具体实现:
private Class<?> createAdaptiveExtensionClass() {
String code = (new AdaptiveClassCodeGenerator(this.type, this.cachedDefaultName)).generate();
ClassLoader classLoader = findClassLoader();
Compiler compiler = (Compiler)getExtensionLoader(Compiler.class).getAdaptiveExtension();
return compiler.compile(code, classLoader);
}
看到这里会感到奇怪,为什么会涉及到编译器呢?当我们给方法添加@Adaptive("...")注解并指明参数的时候,Dubbo会以此为依据动态加载扩展点,加载的形式就是在原扩展类的基础上进行编码生成新的扩展类,并使用不同的编译器对其进行编译。
所以基于自适应特性使用的扩展类HelloInterface
,最终系统会自动编译生成一个HelloInterface$Adaptive
类对象。
关于编译器的实现原理,这里先不展开了,后面涉及到再进行深入分析。
4.3 getActivateExtension
getActivateExtension方法有多个重载,源码如下:
public List<T> getActivateExtension(URL url, String key, String group) {
String value = url.getParameter(key);
return this.getActivateExtension(url, StringUtils.isEmpty(value) ? null : CommonConstants.COMMA_SPLIT_PATTERN.split(value), group);
}
其中又调用了getActivateExtension方法,该方法最终会调用getExtension方法,具体代码这里先不分析了,后面涉及到再具体分析。
5. ExtensionFactory
前面提到,ExtensionLoader用法如下:
ExtensionLoader.getExtensionLoader(HelloInterface.class).getDefaultExtension();
前面已经分析了getDefaultExtension方法的实现代码,但是并没有分析getExtensionLoader(...)方法,其源码如下:
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
if (type == null) {
throw new IllegalArgumentException("Extension type == null");
} else if (!type.isInterface()) {
throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
} else if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type (" + type + ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
} else {
ExtensionLoader<T> loader = (ExtensionLoader)EXTENSION_LOADERS.get(type);
if (loader == null) {
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type));
loader = (ExtensionLoader)EXTENSION_LOADERS.get(type);
}
return loader;
}
}
当我们传入的Class对象不为空时,最终会new一个ExtensionLoader对象出来,构造函数如下:
private ExtensionLoader(Class<?> type) {
this.type = type;
this.objectFactory = type == ExtensionFactory.class ? null : (ExtensionFactory)getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension();
}
可以看到里面又调用了getExtensionLoader方法,只不过此时指定了Class类型为ExtensionFactory类。所以我们创建的ExtensionLoader对象中的objectFactory变量是一个ExtensionFactory类型的对象,从这个变量的定义也可以看出这点:
private final ExtensionFactory objectFactory;
ExtensionFactory这个接口定义如下:
@SPI
public interface ExtensionFactory {
<T> T getExtension(Class<T> var1, String var2);
}
它也加了SPI注解,它的子类有:SpiExtensionFactory,AdaptiveExtensionFactory,SpringExtensionFactory。实际使用时会指定其中一个子类。
我们前面在分析injectExtension方法的时候曾看到:
Object object = this.objectFactory.getExtension(pt, property);
该句代码从objectFactory中获取扩展对象,从而实现了将扩展对象B自动注入到扩展对象A的效果。下面就以SpringExtensionFactory为例,看下getExtension方法是如何获取扩展对象的。
5.1 SpringExtensionFactory
getExtension方法定义如下:
public <T> T getExtension(Class<T> type, String name) {
...
Object bean;
do {
if (!var3.hasNext()) {
return null;
}
// 获取Spring上下文
ApplicationContext context = (ApplicationContext)var3.next();
// 获取Spring容器中指定的bean对象
bean = BeanFactoryUtils.getOptionalBean(context, name, type);
} while(bean == null);
return bean;
}
从上述代码可知Dubbo可以和Spring进行集成,从Spring容器中获取Bean作为扩展类对象使用。
5.2 SpiExtensionFactory
该类的getExtension方法定义如下:
public <T> T getExtension(Class<T> type, String name) {
if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
ExtensionLoader<T> loader = ExtensionLoader.getExtensionLoader(type);
if (!loader.getSupportedExtensions().isEmpty()) {
return loader.getAdaptiveExtension();
}
}
return null;
}
那这个SpiExtensionFactory用在哪里呢?例如,某个扩展点实现类ClassA上有@Adaptive注解,则调用SpiExtensionFactory#getExtension方法会直接返回ClassA对象。上述方法最终调用了ExtensionLoader.getAdaptiveExtension方法。
5.3 AdaptiveExtensionFactory
6. 扩展点动态编译
扩展点的自适应特性使得Dubbo框架非常灵活,而动态编译是自适应特性的基础。因为动态生成的自适应类只是字符串,需要通过编译才能得到真正的Class。虽然Jdk的动态代理也也可以实现类似的功能,但是在性能上和直接编译好的Class会有一定的差距。
为了动态编译成新的扩展类,Dubbo提供了三种代码编译器:JDK编译器,Javassist编译器和AdaptiveCompiler编译器。它们都实现了Compiler接口。
@SPI("javassist")
public interface Compiler {
Class<?> compile(String var1, ClassLoader var2);
}
可以看到默认加载的是javassist编译器,继承关系如下:6.1 Javassist编译器
Java动态编译生成Class的方式有很多,常见的有:CGLIB,ASM,Javassist等。下面通过一个Javassist的使用示例来帮助理解Dubbo是如何使用它的:
ClassPool classPool = ClassPool.getDefault(); // 初始化Javassist的类池
CtClass ctClass = classPool.makeClass("Hello"); // 创建一个Hello类
CtMethod ctMethod = CtMethod.make("
public static void test(){
System.out.println(\"Hello World!\");
}",ctClass);
ctClass.addMethod(ctMethod);
Class aClass = ctClass.toClass(); // 生成类
Object object = aClass.newInstance(); // 通过反射创建实例
Method m = aClass.getDeclaredMethod("test",null);
m.invoke(object,null); // 调用方法
执行后,打印出“Hello World”。
Dubbo的JavassistCompiler编译器的doCompiler方法大致实现与上类似,这里就不分析了。
6.2 JDK编译器
原生JDK编译器包位于javax.tools下。主要使用JavaFileObject接口,ForwordingJavaFileManager接口,JavaCompiler.CompilationTask方法。Dubbo实现的JdkCompiler类封装了这些原生的编译器实现类,最终将代码字符串编译成具体的类。