这篇比较枯燥,先记录下来。等着理解更深后,会使用通俗易懂的语言进行描述。 复制代码
dubbo是一个扩展性特别强的框架,基于JAVA的SPI思路改良后的Dubbo SPI,使接口和实现完全解耦。提供注册中心,通信协议,序列化等的扩展方式。而且自身在设计过程中针对不同的场景选择合理的设计模式。
使用策略模式。只声明接口,具体的实现不在程序中直接确定,而且通过程序外的配置,用于具体实现的装配。 ①定义一个接口以及方法。 ②编写接口的实现类。 ③创建
//定义SPI接口 public interface HelloService { void sayHello(); } //定义实现类 public class ChineseHello implements HelloService { @Override public void sayHello() { System.out.println("你好!!!"); } } //使用ServiceLoader加载接口的所有实现类 public static void main(String[] args) { ServiceLoader<HelloService> helloServices = ServiceLoader.load(HelloService.class); for (HelloService helloService: helloServices){ helloService.sayHello(); } } //输出你好!!!! 复制代码
1、JDK标准的SPI会一次性实例化扩展点所有实现,如果有扩展则初始化很耗时,如果没用上也加载,则浪费资源。
2、如果扩展加载失败,则连扩展的名称都获取不到了。如果JDK标准的ScriptEngine,通过getName()获取脚本类型的名称,如果RubyScriptEngine因为所引来的jruby.jar不存在,导致RubyScriptEngine类加载失败,这个失败原因被“吃掉”了,和Ruby对应不起来,当用户执行Ruby脚本时,会报不支持Ruby,而不是真正失败的原因。
3、增加了对扩展IOC和APO的支持,一个扩展可以直接setter注入其他扩展。在Java SPI中已经看到,java.util.ServiceLoader会一次把PrintService接口下的所有实现类全部初始化,用户直接调用接口。Dubbo SPI只是加载配置文件中的类,并分成不同的种类缓存在内存中,而不全立即全部初始化,在性能上有更好的表现。
//在目录META-INF/dubbo/internal下建立HelloService的默认实现类 impl=com.test.spi.ChineseHello //定义SPI接口 public interface HelloService { void sayHello(); } //定义实现类 public class ChineseHello implements HelloService { @Override public void sayHello() { System.out.println("你好!!!"); } } //调用Dubbo SPI加载配置文件的信息,并且加载默认实现类 public class SayHelloMain { public static void main(String[] args) { HelloService helloService = ExtensionLoader.getExtensionLoader(HelloService.class) .getDefaultExtension(); helloService.sayHello(); } } //输出你好!!!! 复制代码
JAVA SPI加载失败,可能会因为各种原因导致异常信息被“吞掉”,导致问题追踪比较困难。Dubbo SPI在扩展加载失败会先抛出真实异常并打印日志。扩展点在被动加载的时候,即使有部分失败,也不会影响其他扩展点和整个框架的使用。
Dubbo SPI和JAVA SPI类似,需要在META-INF/dubbo/下放置对应的SPI配置文件,文件名称需要命名为接口的全路径名。 配置文件的呢日用为key=扩展点实现类全路径名,有多个用换行符分割。 key为Dubbo SPI注解中的传入参数。 兼容JAVA SPI的配置路径和内容配置方式。在Dubbo启动时,会默认扫META-INF/services/、META-INF/dubbo/、META-INF/dubbo/internal/三个文件。
分为Class缓存、实例缓存。这两种缓存又能根据扩展类的种类分为普通扩展类,包装扩展类(Wrapper)和自适应扩展类(Adaptive)等。
Class缓存:Dubbo SPI获取扩展类时,会先从缓存中读取。如果缓存中不存在,则加载配置文件,根据配置把Class缓存在内存中,并不会直接全部初始化。
实例缓存:基于性能考虑,Dubbo框架不仅缓存Class,也会缓存Class实例化后的对象。每次获取的时候,会先从缓存中读取,如果缓存中读不到,则重新加载并缓存起来。因为缓存的Class并不会全部实例化,而是根据需求实例化并缓存,因此性能更好。
自动包装、自动加载、自适应和自动激活。
}
ProteocolFilterWrapper 实现了Protocol接口,但是构造函数中又传入了Protocol类型的参数。因此ProteocolFilterWrapper会被认定为Wrapper类。这是一种装饰器模式,把通用的抽象逻辑封装或者对子类进行增强,让子类可以更加专注具体的实现。
②自动加载:除了在构造函数中传入其他扩展实例,我们还经常使用setter方法设置属性值。如果某个扩展类是另外一个扩展点类的成员属性,并且拥有setter方法,那么框架也会自动注入对应的扩展点实例。ExtensionLoader在执行扩展点出初始化的时候,会自动通过setter方法注入对应的实现类。如果扩展类属性是一个接口,有多种实现,那么具体注入哪一个呢?涉及第三特性--- 自适应 。
③自适应:在Dubbo SPI中,我们使用@Adaptive注解,可以动态通过URL中的参数来确定要使用哪个具体的实现类。从而解决自动加载中的实例注入问题。 @SPI("netty") public interface Transporter{ @Adaptive({Constants.SERVER_KEY,Constants.TRANSPORTER_KEY}}) Server bind(URL url,ChannelHandler handler);
@Adaptive({Constants.CLIENT_KEY,Constants.TRANSPORTER_KEY}}) Client connect(URL url,ChannelHandler handler); }
@Adaptive传入两个参数,外部调用Transporter#bin方法的时候,会动态从传入参数“URL”中提取key参数“server”的value值,如果能匹配上某个扩展实现类则直接使用对应的实现类;如果未匹配上,则继续通过第二个key。如果都没匹配上,则抛出异常。也就是说@Adaptive传入多个参数,依次进行实现类的匹配,直到最后抛出异常。 如果一个类的多个实现类都要加载怎么办?涉及最后一个特性--- 自动激活 。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) public @interface SPI { String value() default ""; } 复制代码
SPI有个value属性,通过这个属性,可以传入不同的参数来设置这个接口的默认实现类。 例如Protocol的默认为dubbo。
@SPI("dubbo") public interface Protocol { int getDefaultPort(); @Adaptive <T> Exporter<T> export(Invoker<T> var1) throws RpcException; @Adaptive <T> Invoker<T> refer(Class<T> var1, URL var2) throws RpcException; void destroy(); } 复制代码
Dubbo中很多地方通过getExtension(Class type,String name)来获取扩展点接口的具体实现。此时会对class做校验,判断是否是接口,以及是否有@SPI注解,两者缺一不可。
@Adaptive注解可以表示在类,接口,枚举和方法上,但是在整个Dubbo框架中,只有几个地方使用到了类级别上。其他都标注在方法上。如果标注在方法上,为方法级别注解,则可以通过参数动态获取实现类,这一点在自适应特性中已经说明。方法级别注解,在第一次getExtension时,会自动生成和编译一个动态的Adaptive类,从而达到动态实现类的效果。 例如:Protocol接口在export和refer两个接口上添加了@Adaptive注解。Dubbo在初始化扩展点时,会生成Protocol$Adaptive类,里面会实现两个方法,方法里会有一些抽象的通用逻辑,通过@Adaptive中传入的参数,找到并调用真正的实现类。和装饰器模式比较类似。
@Adaptive注解的代码 @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface Adaptive { String[] value() default {}; } 复制代码
该注解也可以传入value值,并且是一个数组。
驼峰规则:如果包装类(wrapper)没有用Adaptive匹配到key值,则Dubbo会自动把接口名称根据驼峰大小写分开,并且用“.”符号连接起来,以此来作为默认实现类的名称,如org.apache.dubbo.xxx.HelloInovkerWrapper中HelloInvokerWrapper会被转移为hello.invoker.wrapper。
为什么有些实现类会标注@Adaptive注解?
public class ExtensionLoader<T> { //实现类类型cachedAdaptiveClass private volatile Class<?> cachedAdaptiveClass = null; private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap(); private String cachedDefaultName; //实例化对象cachedAdaptiveInstance private final Holder<Object> cachedAdaptiveInstance = new Holder(); 复制代码
@Activate可以标识在类、接口、枚举类和方法上。主要使用在多个扩展点实现、需要根据不同条件被激活的场景上。如Filter需要多个同时激活,因为每个Filter的实现是不同的功能。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface Activate { //URL中的分组如果匹配则激活,可以设置多个 String[] group() default {}; //查找URL中如果含有该key值,则激活 String[] value() default {}; //标识哪些扩展点要在本扩展点之前 String[] before() default {}; //标识哪些扩展点要在本扩展点之前 String[] after() default {}; //排序信息 int order() default 0; } 复制代码