3 SPI中的IOC和AOP
3.1 依赖注入
Dubbo IOC 是通过 setter 方法注入依赖。Dubbo 首先会通过反射获取到实例的所有方法,然后再遍历方法列表,检测方法名是否具有 setter 方法特征。若有,则通过 ObjectFactory 获取依赖对象,最后通过反射调用 setter 方法将依赖设置到目标对象中。整个过程对应的代码如下:
private T injectExtension(T instance) { try { if (objectFactory != null) { // 遍历目标类的所有方法 for (Method method : instance.getClass().getMethods()) { // 检测方法是否以 set 开头,且方法仅有一个参数,且方法访问级别为 public if (method.getName().startsWith("set") && method.getParameterTypes().length == 1 && Modifier.isPublic(method.getModifiers())) { // 获取 setter 方法参数类型 Class<?> pt = method.getParameterTypes()[0]; try { // 获取属性名,比如 setName 方法对应属性名 name String property = getSetterProperty(method); /* getSetterProperty : method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : ""; */ // 从 ObjectFactory 中获取依赖对象 Object object = objectFactory.getExtension(pt, property); if (object != null) { // 通过反射调用 setter 方法设置依赖 method.invoke(instance, object); } } catch (Exception e) { logger.error("fail to inject via method..."); } } } } } catch (Exception e) { logger.error(e.getMessage(), e); } return instance; }
在上面代码中,objectFactory 变量的类型为 AdaptiveExtensionFactory,AdaptiveExtensionFactory 内部维护了一个 ExtensionFactory 列表,用于存储其他类型的 ExtensionFactory。Dubbo 目前提供了两种 ExtensionFactory,分别是 SpiExtensionFactory 和 SpringExtensionFactory。前者用于创建自适应的拓展,后者是用于从 Spring 的 IOC 容器中获取所需的拓展。这两个类的类的代码不是很复杂,这里就不一一分析了。
Dubbo IOC 目前仅支持 setter 方式注入,总的来说,逻辑比较简单易懂。
3.2 动态增强
在用Spring的时候,我们经常会用到AOP功能。在目标类的方法前后插入其他逻辑。比如通常使用Spring AOP来实现日志,监控和鉴权等功能。 Dubbo的扩展机制,是否也支持类似的功能呢?答案是yes。
在Dubbo中,有一种特殊的类,被称为Wrapper类。通过装饰者模式,使用包装类包装原始的扩展点实例。在原始扩展点实现前后插入其他逻辑,实现AOP功能。
装饰者模式
装饰者模式:在不改变原类文件以及不使用继承的情况下,动态地将责任附加到对象上,从而实现动态拓展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。
一般来说装饰者模式有下面几个参与者:
Component:装饰者和被装饰者共同的父类,是一个接口或者抽象类,用来定义基本行为
ConcreteComponent:定义具体对象,即被装饰者
Decorator:抽象装饰者,继承自Component,从外类来扩展ConcreteComponent。对于ConcreteComponent来说,不需要知道Decorator的存在,Decorator是一个接口或抽象类
ConcreteDecorator:具体装饰者,用于扩展ConcreteComponent
注:装饰者和被装饰者对象有相同的超类型,因为装饰者和被装饰者必须是一样的类型,这里利用继承是为了达到类型匹配,而不是利用继承获得行为。
dubbo中的AOP
Dubbo AOP 是通过装饰者模式完成的,接下来通过一个简单的案例来学习dubbo中AOP的实现方式。
首先定义一个接口
package com.oldlu.dubbo; import org.apache.dubbo.common.extension.SPI; @SPI public interface Phone { void call(); }
定义接口的实现类,也就是被装饰者
package com.oldlu.dubbo; public class IphoneX implements Phone { @Override public void call() { System.out.println("iphone正在拨打电话"); } }
为了简单,这里省略了装饰者接口。仅仅定义一个装饰者,实现phone接口,内部配置增强逻辑方法
package com.oldlu.dubbo; public class MusicPhone implements Phone { private Phone phone; public MusicPhone(Phone phone) { this.phone = phone; } @Override public void call() { System.out.println("播放彩铃"); this.phone.call(); } }
添加拓展点配置文件META-INF/dubbo/com.oldlu.dubbo.Phone,内容如下
iphone=com.oldlu.dubbo.IphoneX wrapper=com.oldlu.dubbo.MusicPhone
配置测试方法
public static void main(String[] args) { ExtensionLoader<Phone> extensionLoader = ExtensionLoader.getExtensionLoader(Phone.class); Phone phone = extensionLoader.getExtension("iphone"); phone.call(); }
具体执行效果如下
先调用装饰者增强,再调用目标方法完成业务逻辑。
通过测试案例,可以看到在Dubbo SPI中具有增强AOP的功能,我们只需要关注dubbo源码中这样一行代码就够了
//检查是否具有装饰者类,如果有调用装饰者类的构造方法,并返回实例对象 if (CollectionUtils.isNotEmpty(wrapperClasses)) { for (Class<?> wrapperClass : wrapperClasses) { instance = injectExtension( (T) wrapperClass.getConstructor(type).newInstance(instance)); } }
4 动态编译
4.1 SPI中的自适应
我们知道在 Dubbo 中,很多拓展都是通过 SPI 机制 进行加载的,比如 Protocol、Cluster、LoadBalance、ProxyFactory 等。有时,有些拓展并不想在框架启动阶段被加载,而是希望在拓展方法被调用时,根据运行时参数进行加载,即根据参数动态加载实现类。如下所示:
根据参数动态选择的意思是:
通过ExtensionLoader.getExtensionLoader(XXXClass).getExtension(key)的形式来获取接口的某个实现类。
但这种形式本质上还是通过硬编码的形式在代码中固定的获取了接口的一个实现,诸如Protocol(实现有Dubbo、Redis、Thrift等),或者Transporter(实现有Netty、Mina等)这些接口,我们是可以在Dubbo服务声明时指定具体实现的
这种在运行时,根据方法参数才动态决定使用具体的拓展,在dubbo中就叫做扩展点自适应实例。其实是一个扩展点的代理,将扩展的选择从Dubbo启动时,延迟到RPC调用时。Dubbo中每一个扩展点都有一个自适应类,如果没有显式提供,Dubbo会自动为我们创建一个,默认使用Javaassist。
自适应拓展机制的实现逻辑是这样的
首先 Dubbo 会为拓展接口生成具有代理功能的代码;
通过 javassist 或 jdk 编译这段代码,得到 Class 类;
通过反射创建代理类;
在代理类中,通过URL对象的参数来确定到底调用哪个实现类;
4.2 javassist入门
Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架。javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。为了方便更好的理解dubbo中的自适应,这里通过案例的形式来熟悉下Javassist的基本使用
package com.oldlu.compiler; import java.io.File; import java.io.FileOutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; import javassist.CtField; import javassist.CtMethod; import javassist.CtNewMethod; /** * Javassist是一个开源的分析、编辑和创建Java字节码的类库 * 能动态改变类的结构,或者动态生成类 */ public class CompilerByJavassist { public static void main(String[] args) throws Exception { // ClassPool:class对象容器 ClassPool pool = ClassPool.getDefault(); // 通过ClassPool生成一个User类 CtClass ctClass = pool.makeClass("com.oldlu.domain.User"); // 添加属性 -- private String username CtField enameField = new CtField(pool.getCtClass("java.lang.String"), "username", ctClass); enameField.setModifiers(Modifier.PRIVATE); ctClass.addField(enameField); // 添加属性 -- private int age CtField enoField = new CtField(pool.getCtClass("int"), "age", ctClass); enoField.setModifiers(Modifier.PRIVATE); ctClass.addField(enoField); //添加方法 ctClass.addMethod(CtNewMethod.getter("getUsername", enameField)); ctClass.addMethod(CtNewMethod.setter("setUsername", enameField)); ctClass.addMethod(CtNewMethod.getter("getAge", enoField)); ctClass.addMethod(CtNewMethod.setter("setAge", enoField)); // 无参构造器 CtConstructor constructor = new CtConstructor(null, ctClass); constructor.setBody("{}"); ctClass.addConstructor(constructor); // 添加构造函数 //ctClass.addConstructor(new CtConstructor(new CtClass[] {}, ctClass)); CtConstructor ctConstructor = new CtConstructor(new CtClass[] {pool.get(String.class.getName()),CtClass.intType}, ctClass); ctConstructor.setBody("{\n this.username=$1; \n this.age=$2;\n}"); ctClass.addConstructor(ctConstructor); // 添加自定义方法 CtMethod ctMethod = new CtMethod(CtClass.voidType, "printUser",new CtClass[] {}, ctClass); // 为自定义方法设置修饰符 ctMethod.setModifiers(Modifier.PUBLIC); // 为自定义方法设置函数体 StringBuffer buffer2 = new StringBuffer(); buffer2.append("{\nSystem.out.println(\"用户信息如下\");\n") .append("System.out.println(\"用户名=\"+username);\n") .append("System.out.println(\"年龄=\"+age);\n").append("}"); ctMethod.setBody(buffer2.toString()); ctClass.addMethod(ctMethod); //生成一个class Class<?> clazz = ctClass.toClass(); Constructor cons2 = clazz.getDeclaredConstructor(String.class,Integer.TYPE); Object obj = cons2.newInstance("oldlu",20); //反射 执行方法 obj.getClass().getMethod("printUser", new Class[] {}) .invoke(obj, new Object[] {}); // 把生成的class文件写入文件 byte[] byteArr = ctClass.toBytecode(); FileOutputStream fos = new FileOutputStream(new File("C://User.class")); fos.write(byteArr); fos.close(); } }
通过以上代码,我们可以知道使用javassist可以方便的在运行时,按需动态的创建java对象,并执行内部方法。而这也是dubbo中动态编译的核心
4.3 源码分析
Adaptive注解
在开始之前,我们有必要先看一下与自适应拓展息息相关的一个注解,即 Adaptive 注解。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface Adaptive { String[] value() default {}; }
从上面的代码中可知,Adaptive 可注解在类或方法上。
标注在类上:Dubbo 不会为该类生成代理类。Adaptive 注解在类上的情况很少,在 Dubbo 中,仅有两个类被 Adaptive 注解了,分别是 AdaptiveCompiler 和 AdaptiveExtensionFactory。此种情况,表示拓展的加载逻辑由人工编码完成。更多时候,Adaptive 是注解在接口方法上的,表示拓展的加载逻辑需由框架自动生成
标注在方法上:Dubbo 则会为该方法生成代理逻辑,表示当前方法需要根据 参数URL 调用对应的扩展点实现。例如 Protocol的SPI类有 injvm dubbo registry filter listener等等 很多扩展未知类, 它设计了Protocol$Adaptive的类,通过ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(spi类);来提取对象
获取自适应拓展类
dubbo中每一个扩展点都有一个自适应类,如果没有显式提供,Dubbo会自动为我们创建一个,默认使用Javaassist。 先来看下创建自适应扩展类的代码:
// 精简代码如下 public T getAdaptiveExtension() { Object instance = cachedAdaptiveInstance.get(); if (instance == null) { synchronized (cachedAdaptiveInstance) { instance = cachedAdaptiveInstance.get(); if (instance == null) { instance = createAdaptiveExtension(); cachedAdaptiveInstance.set(instance); } } } return (T) instance; }
继续看createAdaptiveExtension方法
private T createAdaptiveExtension() { return injectExtension((T) getAdaptiveExtensionClass().newInstance()); }
继续看getAdaptiveExtensionClass方法
private Class<?> getAdaptiveExtensionClass() { getExtensionClasses(); if (cachedAdaptiveClass != null) { return cachedAdaptiveClass; } return cachedAdaptiveClass = createAdaptiveExtensionClass(); }
继续看createAdaptiveExtensionClass方法,绕了一大圈,终于来到了具体的实现了。看这个createAdaptiveExtensionClass方法,它首先会生成自适应类的Java源码,然后再将源码编译成Java的字节码,加载到JVM中。
private Class<?> createAdaptiveExtensionClass() { String code = createAdaptiveExtensionClassCode(); ClassLoader classLoader = findClassLoader(); org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension(); return compiler.compile(code, classLoader); }
Compiler的代码,默认实现是javassist。
@SPI("javassist") public interface Compiler { Class<?> compile(String code, ClassLoader classLoader); }
createAdaptiveExtensionClassCode()方法中使用一个StringBuilder来构建自适应类的Java源码。方法实现比较长,这里就不贴代码了。这种生成字节码的方式也挺有意思的,先生成Java源代码,然后编译,加载到jvm中。通过这种方式,可以更好的控制生成的Java类。而且这样也不用care各个字节码生成框架的api等。因为xxx.java文件是Java通用的,也是我们最熟悉的。只是代码的可读性不强,需要一点一点构建xx.java的内容。
示例:以 Protocol 接口为例,Protocol接口定义如下:
@SPI("dubbo") public interface Protocol { int getDefaultPort(); @Adaptive <T> Exporter<T> export(Invoker<T> invoker) throws RpcException; @Adaptive <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException; void destroy(); default List<ProtocolServer> getServers() { return Collections.emptyList(); } }
下面给大家展示一下生成的 Protocol$Adaptive 的源码,如下
package org.apache.dubbo.rpc; import org.apache.dubbo.common.extension.ExtensionLoader; public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol { public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, org.apache.dubbo.common.URL arg1) throws org.apache.dubbo.rpc.RpcException { if (arg1 == null) throw new IllegalArgumentException("url == null"); org.apache.dubbo.common.URL url = arg1; 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])"); org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName); return extension.refer(arg0, arg1); } public java.util.List getServers() { throw new UnsupportedOperationException("The method public default java.util.List org.apache.dubbo.rpc.Protocol.getServers() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!"); } 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(); 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])"); org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName); return extension.export(arg0); } public void destroy() { throw new UnsupportedOperationException("The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!"); } public int getDefaultPort() { throw new UnsupportedOperationException("The method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!"); } }
以调用 Protocol 的接口的 refer方法为例,下面给大家看一下自适应拓展的整个过程:
1)、先通过 Protocol REF_PROTOCOL = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); 生成了 Protocol$Adaptive 代理类
2)、传参 Url 为:dubbo://192.168.1.247:20887/org.apache.dubbo.config.spring.api.DemoService,调用 Protocol 的 refer 方法,此时直接调用是 Protocol$Adaptive 代理类的 refer 方法
3)、在 Protocol$Adaptive 的 refer 方法中先调用 url 中的 getProtocol() 方法获取拓展类名称,赋值给 extName 变量
4)、然后调用 org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName); 语句获取到具体的实现类的实例
5)、最后执行 extension.refer(arg0, arg1) 语句,调用 4 中获取到的具体实现类的 refer 方法,最终返回结果