Dubbo动态代理策略
默认使用javassist 动态字节码生成,创建代理类。但是可以通过 spi 扩展机制配置自己的动态代理策略。
spi
简单来说,就是 service provider interface ,说白了是什么意思呢,比如你有个接口,现在这个接口有 3 个实现类,那么在系统运行的时候对这个接口到底选择哪个实现类呢? 这就需要 spi 了,需要根据指定的配置或者是默认的配置,去找到对应的实现类加载进来,然后用这个实现类的实例对象。
spi 机制一般用在哪里?插件扩展的场景,比如说你开发了一个给别人使用的开源框架,如果你想让别人自己写个插件,插到你的开源框架里去,从而扩展某个功能,这个时候 spi 思想就用上了。
Java spi 思想
比如说 jdbc。 Java 定义了一套 jdbc 的接口,但是 Java 并没有提供 jdbc 的实现类。 但是实际上项目跑的时候,要使用jdbc 接口的哪些实现类呢?一般来说,我们要根据自己使用的数据库,比如 mysql,你就将 mysql-jdbc-connector.jar 引入进来;oracle,你就将oracle-jdbc-connector.jar 引入进来。在系统跑的时候,碰到你使用jdbc 的接口,会在底层使用你引入的那个 jar 中提供的实现类。
dubbo 的 spi 思想
Dubbo SPI和Java SPI类似,需要在META-INF/dubbo/下放置对应的SPI配置文件,文件名称需要命名为接口的全路径名。配置文件的内容为key=扩展点实现类全路径名,如果有多个实现类则使用换行符分隔。其中,key会作为DubboSPI注解中的传入参数。另外,Dubbo SPI还兼容了 Java SPI的配置路径和内容配置方式。在Dubbo启动的时候,会默认扫这三个目录下的配置文件:
META-INF/services/、META-INF/dubbo/、META-INF/dubbo/interna
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).get
Protocol 接口,在系统运行的时候,dubbo 会判断一下应该选用这个 Protocol 接口的哪个实现类来实例化对象来使用。 它会去找一个你配置的 Protocol,将你配置的 Protocol 实现类,加载到 jvm 中来,然后实例化对象,就用你的那个 Protocol 实现类就可以了。 上面那个代码就是 dubbo 里大量使用的,就是对很多组件,都是保留一个接口和多个实现,然 后在系统运行的时候动态根据配置去找到对应的实现类。如果你没配置,那就用默认的实现好了,没问题。
在dubbo 自己的 jar里,在 /META_INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol 文件中:
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol http=com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
其实就是 Protocol 接口, @SPI(“dubbo”) 说的是,通过 SPI 机制来提供实现类,实现类是通过 dubbo 作为默认 key 去 配置文件里找到的,配置文件名称与接口全限定名一样的,通过 dubbo 作为 key 可以找到默认 的实现类就是 com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol 。
如果想要动态替换掉默认的实现类,需要使用 @Adaptive 接口,Protocol 接口中,有两个方法加了 @Adaptive 注解,就是说那俩接口会被代理实现。
比如这个 Protocol 接口搞了俩 @Adaptive 注解标注了方法,在运行的时候会针对 Protocol 生成代理类,这个代理类的那俩方法里面会有代理代码,代理代码会在运行的时候动态根据 url 中的 protocol 来获取那个 key,默认是 dubbo,你也可以自己指定,你如果指定了别的 key,那么就会获取别的实现类的实例了。
如何自己扩展 dubbo 中的组件
自己写个工程,要是那种可以打成 jar 包的,里面的 src/main/resources 目录下,搞一个META-INF/services ,
里面放个文件叫: com.alibaba.dubbo.rpc.Protocol ,
文件里搞一个 my=com.bingo.MyProtocol 。
自己把 jar 弄到 nexus 私服里去。 然后自己搞一个 dubbo provider 工程,在这个工程里面依赖你自己搞的那个 jar,然后在 spring 配置文件里给个配置:
<dubbo:protocol name=”my” port=”20000” />
provider 启动的时候,就会加载到我们 jar 包里的 my=com.bingo.MyProtocol 这行配置里,接着会根据你的配置使用你定义好的 MyProtocol 了,这个就是简单说明一下,你通过上述方式, 可以替换掉大量的 dubbo 内部的组件,就是扔个你自己的 jar 包,然后配置一下即可。
dubbo里面提供了大量的类似上面的扩展点,就是说,你如果要扩展一个东西,只要自己写个 jar,让你的 consumer 或者是 provider工程,依赖你的那个 jar,在你的 jar 里指定目录下配置好接口名称对应的文件,自己通过 key=实现类 。
然后对于对应的组件,类似 dubbo:protocol 用你的那个 key 对应的实现类来实现某个接口,你可以自己去扩展 dubbo 的各种功能,提供你自己的实现。
Dubbo SPI可以分为Class缓存、实例缓存。这两种缓存又能根据扩展类的种类分为普通扩展类、包装扩展类(Wrapper类)、自适应扩展类(Adaptive类)等。
Class缓存:Dubbo SPI获取扩展类时,会先从缓存中读取。如果缓存中不存在,则加载配置文件,根据配置把Class缓存到内存中,并不会直接全部初始化。
实例缓存:基于性能考虑,Dubbo框架中不仅缓存Class,也会缓存Class实例化后的对象。每次获取的时候,会先从缓存中读取,如果缓存中读不到,则重新加载并缓存起来。这也是为什么Dubbo SPI相对Java SPI性能上有优势的原因,因为Dubbo SPI缓存的Class并不会全部实例化,而是按需实例化并缓存,因此性能更好。
被缓存的Class和对象实例可以根据不同的特性分为不同的类别:
普通扩展类。最基础的,配置在SPI配置文件中的扩展类实现。
包装扩展类。这种Wrapper类没有具体的实现,只是做了通用逻辑的抽象,并且需要在构造方法中传入一个具体的扩展接口的实现。属于Dubbo的自动包装特性
自适应扩展类。一个扩展接口会有多种实现类,具体使用哪个实现类可以不写死在配置或代码中,在运行时,通过传入URL中的某些参数动态来确定。这属于扩展点的自适应特性
其他缓存,如扩展类加载器缓存、扩展名缓存等。
扩展类一共包含四种特性:自动包装、自动加载、自适应和自动激活。
自动包装是一种被缓存的扩展类,ExtensionLoader在加载扩展时,如果发现这个扩展类包含其他扩展点作为构造函数的参数,则这个扩展类就会被认为是Wrapper
类
ProtocolFilterWrapper虽然继承了 Protocol接口,但是其构造函数中又注入了一个Protocol类型的参数。因此ProtocolFilterWrapper会被认定为Wrapper类。这是一种装饰器模式,把通用的抽象逻辑进行封装或对子类进行增强,让子类可以更加专注具体的实现。
使用@Adaptive作注解,可以动态地通过URL中的参数来确定要使用哪个具体的实现类。从而解决自动加载中的实例注入问题。
@Adaptive传入了两个Constants中的参数,它们的值分别是"server”和“transporter”。当外部调用Transporter#bind方法时,会动态从传入的参数“URL”中提取key参数“server” 的value值,如果能匹配上某个扩展实现类则直接使用对应的实现类;如果未匹配上,则继续通过第二个key参数“transporter”提取value值。如果都没匹配上,则抛出异常。也就是说, 如果@Adaptive中传入了多个参数,则依次进行实现类的匹配,直到最后抛出异常。
这种动态寻找实现类的方式比较灵活,但只能激活一个具体的实现类,如果需要多个实现类同时被激活,如Filter可以同时有多个过滤器;或者根据不同的条件,同时激活多个实现类, 如何实现?这就涉及最后一个特性一一自动激活。
使用@Activate注解,可以标记对应的扩展点默认被激活启用。该注解还可以通过传入不同的参数,设置扩展点在不同的条件下被自动激活。主要的使用场景是某个扩展点的多个实现类需要同时启用(比如Filter扩展点)。
扩展点注解:@SPI
@SPI注解可以使用在类、接口和枚举类上,Dubbo框架中都是使用在接口上。它的主要作用就是标记这个接口是一个Dubbo SPI接口,即是一个扩展点,可以有多个不同的内置或用户定义的实现。运行时需要通过配置找到具体的实现类。
例如,我们可以看到Transporter接口使用Netty作为默认实现
Dubbo中很多地方通过getExtension (Class type. String name)来获取扩展点接口的具体实现,此时会对传入的Class做校验,判断是否是接口,以及是否有@SPI注解,两者缺一不可。
扩展点自适应注解:@Adaptive
@Adaptive注解可以标记在类、接口、枚举类和方法上,但是在整个Dubbo框架中,只有几个地方使用在类级别上,如AdaptiveExtensionFactory和AdaptiveCompiler,其余都标注在方法上。如果标注在接口的方法上,即方法级别注解,则可以通过参数动态获得实现类。方法级别注解在第一次getExtension时,会自动生成和编译一个动态的Adaptive类,从而达到动态实现类的效果。
例如:Transporter接口在bind和connect两个方法上添加了@Adaptive注解,Dubbo在初始化扩展点时,会生成一个Transporter$Adaptive类,里面会实现这两个方法,方法里会有一些抽象的通用逻辑,通过@Adaptive中传入的参数,找到并调用真正的实现类。熟悉装饰器模式的读者会很容易理解这部分的逻辑。
当该注解放在实现类上,则整个实现类会直接作为默认实现,不再自动生成代码。在扩展点接口的多个实现里,只能有一个实现上可以加@Adaptive注解。如果多个
实现类都有该注解,则会抛出异常:More than 1 adaptive class found。
在代码中的实现方式是: ExtensionLoader中会缓存两个与@Adaptive有关的对象,一个缓存在cachedAdaptiveClass中, 即Adaptive具体实现类的Class类型;另外一个缓存在cachedAdaptivelnstance中,即Class的具体实例化对象。在扩展点初始化时,如果发现实现类有@Adaptive注解,则直接赋值给cachedAdaptiveClass ,后续实例化类的时候,就不会再动态生成代码,直接实例化cachedAdaptiveClass,并把实例缓存到cachedAdaptivelnstance中。如果注解在接口方法上, 则会根据参数,动态获得扩展点的实现,会生成Adaptive类,再缓存到cachedAdaptivelnstance 中。
扩展点自动激活注解:@Activate
@Activate可以标记在类、接口、枚举类和方法上。主要使用在有多个扩展点实现、需要根据不同条件被激活的场景中,如Filter需要多个同时激活,因为每个Filter实现的是不同的功能。@Activate可传入的参数很多
根据传入URL匹配条件(匹配group> name等),得到所有符合激活条件的扩展类实现。然后根据@Activate中配置的before、after、order等参数进行排序
遍历所有用户自定义扩展类名称,根据用户URL配置的顺序,调整扩展点激活顺序
(遵循用户在 URL 中配置的顺序,例如 URL 为 test 😕/localhost/test?ext=orderl,default,则扩展点ext的激活顺序会遵循先orderl再default,其中default代表所有有@Activate注解的扩展点)。
此处有一点需要注意,如果URL的参数中传入了-default,则所有的默认@Activate都不会被激活,只有URL参数中指定的扩展点会被激活。如果传入了“-”符号开头的扩展点名, 则该扩展点也不会被自动激活。例如:-xxxx,表示名字为xxxx的扩展点不会被激活。
ExtensionLoader 的工作原理
扩展点动态编译的实现
Dubbo SPI的自适应特性让整个框架非常灵活,而动态编译又是自适应特性的基础,因为动态生成的自适应类只是字符串,需要通过编译才能得到真正的Class。虽然我们可以使用反射来动态代理一个类,但是在性能上和直接编译好的Class会有一定的差距。Dubbo SPI通过代码的动态生成,并配合动态编译器,灵活地在原始类基础上创建新的自适应类。
Dubbo中有三种代码编译器,分别是JDK编译器、Javassist编译器和AdaptiveCompiler编译器。这几种编译器都实现了 Compiler接口
Compiler接口上含有一个SPI注解,注解的默认值是@SPI(“javassist”),很明显,Javassist编译器将作为默认编译器。如果用户想改变默认编译器,则可以通过<dubbo:application compiler=“jdk” />标签进行配置。
AdaptiveCompiler上面有@Adaptive注解,说明AdaptiveCompiler会固定为默认实现,这个Compiler的主要作用和AdaptiveExtensionFactory相似,就是为了管理其他Compile。
AdaptiveCompiler#setDefaultCompiler 方法会在 ApplicationConfig 中被调用,也就是 Dubbo在启动时,会解析配置中的<dubbo:application compiler=“jdk” />标签,获取设置的值,初始化对应的编译器。如果没有标签设置,则使用@SPI(“javassist”)中的设置,即javassistCompiler。
AbstpactCompiler,它是一个抽象类,无法实例化,但在里面封装了通用的模
板逻辑。还定义了一个抽象方法doCompile ,留给子类来实现具体的编译逻辑。
JavassistCompiler和JdkCompiler都实现了这个抽象方法。
AbstractCompiler的主要抽象逻辑如下:
通过正则匹配出包路径、类名,再根据包路径、类名拼接出全路径类名。
尝试通过Class.forName加载该类并返回,防止重复编译。如果类加载器中没有这个类,则进入第3步。
调用doCompile方法进行编译。这个抽象方法由子类实现。
Javassist动态代码编译
由于我们之前已经生成了代码字符串,因此在JavassistCompiler中,就是不断通过正则表达式匹配不同部位的代码,然后调用Javassist库中的API生成不同部位的代码,最后得到一个完整的Class对象。具体步骤如下:
初始化Javassist,设置默认参数,如设置当前的classpath
通过正则匹配出所有import的包,并使用Javassist添加import
通过正则匹配出所有extends的包,创建Class对象,并使用Javassist添加extends
通过正则匹配出所有implements包,并使用Javassist添加implements
通过正则匹配出类里面所有内容,即得到{}中的内容,再通过正则匹配出所有方法,并使用Javassist添加类方法
生成Class对象
JDK动态代码编译
原生JDK编译器包位于 javax. tools下。主要使用了三个东西:JavaFileObject 接口、ForwardingJavaFileManager 接口、JavaCompiler.getTask 方法。整个动态编译过程可以简单地总结为:首先初始化一个JavaFileObject对象,并把代码字符串作为参数传入构造方法,然后调用JavaCompiler.getTask方法编译出具体的类。JavaFileManager负责管理类文件的输入/输出位置。
ClassLoaderFilter 的实现原理
切换当前工作线程的类加载器到接口的类加载器,以便和接口的类加载器的上下文一起工作。
如果要实现违反双亲委派模型来查找Class,那么通常会使用上下文类加载器(ContextClassLoader)。当前框架线程的类加载器可能和Invoker接口的类加载器不是同一个,而当前框架线程中又需要获取Invoker的类加载器中的一些 Class,为 了避免出现 ClassNotFoundException,此时只需要使用 Thread. currentThread().getContextClassLoader()就可以获取Invoker的类加载器,进而获得这个类加载器中的Class。
常见的使用例子有DubboProtocol#optimizeSerialization方法,会根据Invoker中配置的optimizer参数获取扩展的自定义序列化处理类,这些外部引入的序列化类在框架的类加载器中肯定没有,因此需要使用Invoker的类加载器获取对应的类。