Dubbo源码分析-微内核插件式开发(ExtensionLoader)

1、实现原理说明

上一个章节说道Dubbo的微内核插件式开发其实就是在SPI设计思路上做了优化升级。我们都知道SPI设计思路是当我们要获取某个接口的实现时,使用ServiceLoader去扫描第三方依赖JAR包的META-INF/services/目录,如果找到名字为这个接口全路径的文件,那么就会读取这个文件中的内容,然后SPI约定这个内容就是实现类的全路径名称,所以ServiceLoader就可以根据这个实现类路径来实例化提供服务。而Dubbo是自己实现了加载器,名字叫ExtensionLoader。Dubbo约定只有标记了@SPI注解的接口,才能使用ExtensionLoader去加载实现类。ExtensionLoader会依次扫描META-INF/dubbo/internal/目录、META-INF/dubbo/目录、META-INF/services/目录,扫描出名字为接口的全路径名的文件,然后文件内容约定key=value的形式出现,key就是这个实现类的别名,value才是实现类的全路径。当然当一个项目里面,对同一个接口可能会很多种实现,那到底使用哪种实现,所以这里Dubbo做了特别的设计,每个支持SPI接口会对应需要一个适配实现(可以自己实现,也可以系统帮你生成),  然后用户可以根据这个设配实现来决定最后到底需要选择哪个具体实现。ExtensionLoader在扫描到实现类时,并发现这个实现类上标记了@Adaptive注解时,就会把这个实现类作为适配实现看待,如果发现扫描出来的实现类里面一个都没有标记这个注解,那么ExtensionLoader会自动生成一个实现类作为该接口的设配实现,所以所有的@SPI接口在使用时都会存在一个适配实现。

2、@SPI注解

上面说到了,只有注解了SPI注解的接口,ExtensionLoader才支持加载扩展实现,SPI有个value参数,这个value指定的是扩展实现的的别名,指定后默认使用的就是这个别名对应的扩展实现。SPI源码如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SPI {
	String value() default "";
}

Dubbo内部的很多实现都基于这种方式来扩展实现的。我们都知道Dubbo支持很多序列化协议,我就拿这个例子来说明,看Protocol协议接口,他标记了SPI注解,并指定了默认实现是别名叫dubbo的协议实现。所以系统里面通过ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();获取协议实现时,如果没有特别指定用哪种实现,默认就会使用别名叫dubbo的协议。

@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();
}

3、@Adaptive注解

作用一:@Adaptive注解主要用来标记适配类实现,上面说了,ExtensionLoader在各个依赖JAR寻找实现类时,会检查实现类有没有打上@Adaptive标记,如果打上了,说明该实现类就是适配实现。就会缓存到ExtensionLoader的cachedAdaptiveClass变量里面。例如我想要自己实现一个Protocol接口的适配实现,可以这样写:

@Adaptive
public class AdapterProtocol implements Protocol {
    @Override
    int getDefaultPort(){//省略...}
    
    @Override
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException{//省略...}

    @Override
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException{//省略...}
    
    @Override
    void destroy(){//省略...}
}

然后在META-INF/services/目录下放置文件:com.alibaba.dubbo.rpc.Protocol。里面的内容设置成adapterProtocol=mypackage.AdapterProtocol。这样我们就是自己实现了一个Protocol的设配实现,当系统使用ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();去加载实现时,拿到的就是我们实现的适配类了。

作用二:@Adaptive还可以标记在接口的方法上。这种情况是,当ExtensionLoader扫描了所有实现类之后,发现没有一个是标记了@Adaptive的实现类。于是ExtensionLoader会使用javasist帮我们自动生成一个设配类。在自动实现各个接口的方式时,会根据该方法上标记的@Adaptive注解的value[]数组值来进行生成。如果没有标记@Adaptive的就抛出异常。例如上面的Protocol自动实现的类反编译后长这样:

public class Protocol$Adpative implements com.alibaba.dubbo.rpc.Protocol {
    //没有打上@Adaptive的方法如果被调到抛异常
    public void destroy() {
        throw new UnsupportedOperationException("method public abstract void 
        com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not 
        adaptive method!")
    }
    
    //接口中export方法打上@Adaptive注册
    public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0) throws 
                                                              com.alibaba.dubbo.rpc.Invoker
    {
        if (arg0 == null)
            throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");
        //参数类中要有URL属性
        if(arg0.getUrl() == null) 
            throw newIllegalArgumentException( "com.alibaba.dubbo.rpc.Invoker argument 
            getUrl() == null");
            
        //从入参获取统一数据模型URL
        com.alibaba.dubbo.common.URL url = arg0.getUrl();
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
            
        //从统一数据模型URL获取协议,协议名就是SPI扩展点实现类的key
        if(extName == null) 
            throw new IllegalStateException( "Fail to 
            getextension(com.alibaba.dubbo.rpc.Protocol) name from url("  + url.toString() + 
            ") usekeys([protocol])");
          
        //利用dubbo服务查找机制根据名称找到具体的扩展点实现
        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol)ExtensionLoader.
        getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
    }
    //其他方法省略...
}

自动实现的设配代码大致意思是,如果方法上没有标记@Adaptive注解的,就抛出一个异常,如果有标记@Adaptive注解的,就判断接口方法有没有URL参数,如果没有就找依附在参数中的URL,然后根据@Adaptive指定的value[]数组,合成代码,代码意思大致就是在使用这个适配类时,会依次使用value[]指定的实现,如果都找不到就最后使用@SPI指定的默认实现。

4、@Activate

关于这个注解,我简单说一下。当我们实现扩展时,如果加上这个注解。然后通过这个注解可以配置一些限制条件。当系统查找扩展实现会把标记了这个注解的实现缓存起来。然后可以在适当时候,过滤出要激活的实现。@Activate注解源码:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Activate {
    //group过滤条件,没有group就不过滤
    String[] group() default {};
    //key过滤条件,如没有设置,则不过滤
    String[] value() default {};
    //排序信息,可以不提供
    String[] before() default {};
    //排序信息,可以不提供
    String[] after() default {};
    //排序信息,可以不提供
    int order() default 0;
}

例如,源码中有个CacheFilter的,就使用了这个注解。

@Activate(group = {Constants.CONSUMER, Constants.PROVIDER}, value = Constants.CACHE_KEY)
public class CacheFilter implements Filter {
    //省略...
}

当使用系统要使用时,这样调用就可以过滤出要被激活的实现

ExtensionLoader.getExtensionLoader(Filter.class)
                .getActivateExtension(url, new String[]{}, "value");

5、ExtensionLoader扫描缓存扩展实现流程说明

猜你喜欢

转载自my.oschina.net/u/1271235/blog/893622