• Dubbo源码之-SPI机制


    以下为Dubbo架构图

    image-20201021163305413

    SPI机制

    SPI全程是Service Provider Interface,翻译过来就是服务提供发现。通过扫描指定路径下的配置项达到一种动态的扩展能力。

    Spring-Boot的SPI机制

    在Spring-Boot中,有Spring官方提供的Starter组件,也有第三方需要根据规范来实现自身的Starter组件,所以Spring官方提供了SpringFactoryLoader的SPI扩展机制。

    实现Starter组件,最主要是的实现自动装配,以减少复杂的Bean的配置。下面我们来看下自动装配中SPI机制体现。


    image-20201022101109284

    Dubbo自动装配包的META-INF路径下,会有个spring.factories文件,可以在改文件中指定自动装配的Bean实现。

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=
    org.apache.dubbo.spring.boot.autoconfigure.DubboRelaxedBinding2AutoConfiguration
    

    那么大胆的猜想,在Spring-Boot容器启动过程当中,肯定会有个东西去扫描它让它生效。

    在Spring-Boot启动类中,会有个注解@SpringBootApplication,其内部还集成了@ EnableAutoConfiguration,import了AutoConfigurationImportSelector类来看下这个类。

    protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
    return EMPTY_ENTRY;
    }
    //加载exclude include属性(若显式的配置了)
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    //加载所有META-INF/spring.factories key为org.springframework.boot.autoconfigure.EnableAutoConfiguration的值
    List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
    configurations = removeDuplicates(configurations);
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    checkExcludedClasses(configurations, exclusions);
    configurations.removeAll(exclusions);
    // 过滤去除当前项目无需加载的Bean
    configurations = getConfigurationClassFilter().filter(configurations);
    fireAutoConfigurationImportEvents(configurations, exclusions);
        return new AutoConfigurationEntry(configurations, exclusions);
    }
    
    

    最后会返回一个Map<String, List<String>>结构的数据。key为org.springframework.boot.autoconfigure.EnableAutoConfiguration,value则为所有的路径下的META-INF/spring.factories的指定key的值,包括Spring-Boot自身提供的以及第三方提供的都会扫描进来。

    JDK的SPI机制

    JDK也内置了SPI的机制实现,它的解析类是java.util.ServiceLoader,同样他需要遵循一些标准:

    • 在classpath目录下,创建一个目录为:META-INF/service

    • 该目录下创建一个 properties 文件,该文件需要满足以下几个条件

      1.文件名必须是扩展的接口的全路径名称
      2.文件内部描述的是该扩展接口的所有实现类,多个通过换行符分隔
      3.文件的编码格式是 UTF-8
      

    ServiceLoader会去扫描META-INF/service 路径下的properties文件。

    public final class ServiceLoader<S>
        implements Iterable<S>
    {
    
        private static final String PREFIX = "META-INF/services/";
    

    SPI 在很多地方有应用,可能大家都没有关注,最常用的就是 JDBC 驱动 。来看下它是怎么用的:

    image-20201022105440812

    com.mysql.jdbc.Driver
    com.mysql.fabric.jdbc.FabricMySQLDriver
    

    这个文件里面写的就是 mysql 的驱动实现。我恍然大悟,原来通过 SPI 机制把java.sql.Driver和 mysql 的驱动做了集成。这样就达到了各个数据库厂商自己去实现数据库连接, jdk 本身不关心你怎么实现。

    Class.forName("com.mysql.jdbc.Driver");
    //直接获的数据库连接
    Connection connection = DriverManager.getConnection(url, user, password);
    

    DriverManager在初始化时,会去扫描所有的META-INF/services/路径下的配置驱动,然后进行加载。

     private static void loadInitialDrivers() {
           //略
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
    
                    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    

    那么JDK内置的SPI机制有什么缺点呢?

    • JDK 标准的 SPI 会一次性加载实例化扩展点的所有实现,什么意思呢?就是如果你在 META-INF/service 下的文件里面加了 N个实现类,那么 JDK 启动的时候都会一次性全部加载。那么如果有的扩展点实现初始化很耗时或者如果有些实现类并没有用到,那么会很浪费资源 。
    • 如果扩展点加载失败,会导致调用方报错,而且这个错误很难定位到是这个原因 。
    • 扩展如果依赖其他的扩展,做不到自动注入和装配。
    • 不提供类似于Spring的IOC和AOP功能。

    Dubbo中的SPI机制

    那么针对JDK内置的SPI机制的缺点,Dubbo进行了一定的优化。有如下两个规则:

    1.需要在 resource 目录下配置 META-INF/dubbo 或者 META-INF/dubbo/internal 或者 META-INF/services,并基于 SPI 接口去创建一个文件
    2.文件名称和接口名称保持一致,如org.apache.dubbo.rpc.cluster.LoadBalance,文件内容则是KEY 对应 Value。key为别名,value为实际的自定义的类全路径名。
    

    image-20201023141001713

    @SPI标签

    @SPI(RandomLoadBalance.NAME)
    public interface LoadBalance {
    

    @SPI注解作用于扩展点的接口上,表明该接口是一个扩展点。可以被Dubbo的ExtentionLoader加载。如果没有此ExtensionLoader调用会异常。Dubbo中支持扩展的接口有ConfiguratorFactoryLoadBalanceMergerProtocolClusterInterceptor等。

    可以针对协议、拦截、集群、路由、负载均衡、序列化、容器… 几乎里面用到的所有功能,都可以实现自己的扩展 。

    @SPI注解有一个参数,该参数表示该扩展点的默认实现的别名。如果没有显示的指定扩展,就使用默认实现。RandomLoadBalance.NAME是一个常量,值是"random",是一个随机负载均衡的实现。


    实现一个自定义的协议扩展类

    public class MyProtocol implements Protocol {
    
        @Override
        public int getDefaultPort() {
            return 8087;
        }
    
        @Override
        public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
            return null;
        }
    
        @Override
        public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
            return null;
        }
    
        @Override
        public void destroy() {
    
        }
    }
    
    ##在resource路径下创建 META-INF/dubbo 名称为org.apache.dubbo.rpc.Protocol的文件
    myProtocol=com.anto.dubbo.dubboprovider.protocol.MyProtocol
    
    @SpringBootApplication
    public class DubboProviderApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(DubboProviderApplication.class, args);
            Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("myProtocol");
            //能够正确输出8087  说明成功加载到了自定义的协议扩展类
            System.out.println(protocol.getDefaultPort());
        }
    
    }
    

    总的来说,思路JDK的SPI 是差不多。 都是基于约定的路径下制定配置文件。目的是为了通过配置的方式轻松实现功能的扩展。

    在JDK的SPI机制当中是通过ServiceLoader来加载解析对应的文件,那么在Dubbo中肯定也存在类似的作用的组件----EXtensionLoader

    ExtensionLoader 的实现

    其实可以大胆的猜想,所谓的扩展点就是指定的路径下配置扩展接口的自定义的实现类,然后由ExtensionLoader去查找和解析这个配置文件。

    ExtensionLoader 自身是一个泛型的类,也就是说每一个类型的扩展接口都有且只会实例化一个ExtensionLoader的实例。

    **该方法需要一个Class类型的参数,该参数表示希望加载的扩展点类型,该参数必须是接口,且该接口必须被@SPI注解注释,否则拒绝处理。 **

     public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
            if (type == null) {
                throw new IllegalArgumentException("Extension type == null");
            }
            if (!type.isInterface()) {
                throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
            }
            if (!withExtensionAnnotation(type)) {
                throw new IllegalArgumentException("Extension type (" + type +
                        ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
            }
    		//根据指定的类型查找容器中是否有此类型,无则进行创建,缓存
            ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
            if (loader == null) {
                EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
                loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
            }
            return loader;
        }
    

    从这可以看出,对于每一个扩展,在Dubbo中只会存在一个ExtensionLoader实例。

    实例化ExtensionLoader,初始化了两个参数type,objectFactory。

     private ExtensionLoader(Class<?> type) {
            this.type = type;
         //除了ExtensionFactory,都会返回自适应的扩展点AdaptiveExtension
            objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
        }
    

    获取一个ExtensionLoader实例对象

    Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("myProtocol");
    

    传入一个别名(就是在配置文件中的key值),然后得到一个具体的ExtensionLoader的实例对象。其实根据上面的结构,大胆的猜测

    1.会有个缓存机制 已经创建情况下,直接从缓存中获取

    2.不存在则进行查找创建该实例

    public T getExtension(String name) {
            //...略
        	//当传入的为true时,取得是默认的扩展实现,即@SPI注解的值
            if ("true".equals(name)) {
                return getDefaultExtension();
            }
            final Holder<Object> holder = getOrCreateHolder(name);
            Object instance = holder.get();
            if (instance == null) {
                synchronized (holder) {
                    instance = holder.get();
                    if (instance == null) {
                        instance = createExtension(name);//根据名称进行创建
                        holder.set(instance);
                    }
                }
            }
            return (T) instance;
        }
    
    • createExtension
    private T createExtension(String name) {
        //根据别名获取对应的Class类型 同理,Dubbo会一次性把所有META-INF/dubbo 或者 META-INF/dubbo/internal 或者 META-INF/services的扩展接口加载后,定义成key-vaue结构
        Class<?> clazz = getExtensionClasses().get(name);
        if (clazz == null) {
            throw findException(name);
        }
        try {
            T instance = (T) EXTENSION_INSTANCES.get(clazz);
            if (instance == null) {
                //缓存在ConcurrentHashMap中
                EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
                instance = (T) EXTENSION_INSTANCES.get(clazz);
            }
            //实例注入,可以猜到,这里应该是对这个实例中的成员属性来实现依赖注入的功能
            injectExtension(instance);
            Set<Class<?>> wrapperClasses = cachedWrapperClasses;
            if (CollectionUtils.isNotEmpty(wrapperClasses)) {
                for (Class<?> wrapperClass : wrapperClasses) {
                    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                }
            }
            initExtension(instance);
            return instance;
        } catch (Throwable t) {
            throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
                    type + ") couldn't be instantiated: " + t.getMessage(), t);
        }
    }
    

    injectExtension这个方法是用来实现依赖注入的,如果被加载的实例中,有成员属性本身也是一个扩展点,则会通过 set 方法进行注入。

    若该属性实例未被创建,则会再次触发创建ExtensionLoader过程。


    看到这,可以发现Dubbo的SPI的扩展机制相比JDk的扩展机制有了比较大的改进。

    1.按需扩展 Dubbo内部通过缓存机制,只有当该扩展点被调用时才会触发其创建的过程
    2.自动注入和装配  若扩展类依赖其他的扩展点,可以做到自动注入
    

    Adaptive自适应扩展点

    Dubbo还支持自适应的扩展,在下面这个例子中,我们传入一个 Compiler 接口,它会返回一个AdaptiveCompiler实例对象。这个就叫自适应。

    Compiler compiler=ExtensionLoader.getExtensionLoader(Compiler.class).getAdaptiveExtension();
    System.out.println(compiler.getClass());
    

    自适应扩展的关键是一个注解 ---@Adaptive

    放在类上,说明当前类是一个确定的自适应扩展点的类,则直接返回修饰的类。相当于确定这个类本身具有了自适应的功能,可以直接使用。

    @Adaptive
    public class AdaptiveCompiler implements Compiler {
    
        private static volatile String DEFAULT_COMPILER;
    
        public static void setDefaultCompiler(String compiler) {
            DEFAULT_COMPILER = compiler;
        }
    	//本身AdaptiveCompiler这个类针对不同的场景会选择不同的编译解决方案,所以是一个确定的自适应扩展点的类
        @Override
        public Class<?> compile(String code, ClassLoader classLoader) {
            Compiler compiler;
            ExtensionLoader<Compiler> loader = ExtensionLoader.getExtensionLoader(Compiler.class);
            String name = DEFAULT_COMPILER; // copy reference
            if (name != null && name.length() > 0) {
                compiler = loader.getExtension(name);
            } else {
                compiler = loader.getDefaultExtension();
            }
            return compiler.compile(code, classLoader);
        }
    
    }
    

    如果是放在方法级别,那么需要生成一个动态代理类,来进行转发。

    作用在类层面,整个Dubbo系统目前只有两个AdaptiveCompiler 和AdaptiveExtensionFactory,
    
    spring=org.apache.dubbo.config.spring.extension.SpringExtensionFactory
    adaptive=org.apache.dubbo.common.extension.factory.AdaptiveExtensionFactory
    spi=org.apache.dubbo.common.extension.factory.SpiExtensionFactory
    

    比如拿 Protocol 这个接口来说,它里面定义了 export 和 refer 两个抽象方法,这两个方法分别带有@Adaptive 的标识,标识是一个自适应方法。

    当ExtensionLoader实例对象调用getAdaptiveExtension()方法时,判断需要加载的的Class类型在缓存中存在与否,有则直接返回。

     public T getAdaptiveExtension() {
         //判断对应扩展接口的ExtensionLoader对象是否持有具体的扩展接口实现类对象
            Object instance = cachedAdaptiveInstance.get();
            if (instance == null) {
                //...
    
                synchronized (cachedAdaptiveInstance) {
                    instance = cachedAdaptiveInstance.get();
                    if (instance == null) {
                        try {
                            instance = createAdaptiveExtension();
                            cachedAdaptiveInstance.set(instance);
                        } catch (Throwable t) {
                            createAdaptiveInstanceError = t;
                            throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t);
                        }
                    }
                }
            }
    
            return (T) instance;
        }
    
    • createAdaptiveExtension()
     private T createAdaptiveExtension() {
         //实例化一个具体的扩展接口实现类对象
            try {
                return injectExtension((T) getAdaptiveExtensionClass().newInstance());
            } catch (Exception e) {
                throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
            }
        }
    
        private Class<?> getAdaptiveExtensionClass() {
           //判断是否需要加载当前扩展类的实现类(空则需要加载)
            getExtensionClasses();
             //从缓存中取 是否有@Adaptive标识的扩展接口实现类
            if (cachedAdaptiveClass != null) {
                return cachedAdaptiveClass;
            }
            return cachedAdaptiveClass = createAdaptiveExtensionClass();
        }
    

    cachedAdaptiveClass缓存中不为空,则直接返回该扩展类的实现类。为什么呢?

     private volatile Class<?> cachedAdaptiveClass = null;
    

    实际上,cachedAdaptiveClass是一个私有volatile变量,当第一次扫描加载ExtensionLoader实例时,会设置该值。

       
      public void addExtension(String name, Class<?> clazz) {
            getExtensionClasses(); // load classes
    
          //...略
    
            if (!clazz.isAnnotationPresent(Adaptive.class)) {
                if (StringUtils.isBlank(name)) {
                    throw new IllegalStateException("Extension name is blank (Extension " + type + ")!");
                }
                if (cachedClasses.get().containsKey(name)) {
                    throw new IllegalStateException("Extension name " +
                            name + " already exists (Extension " + type + ")!");
                }
    
                cachedNames.put(clazz, name);
                cachedClasses.get().put(name, clazz);
            } else {
              //当该实现类被@Adaptive修饰时,会设置cachedAdaptiveClass的值
                cachedAdaptiveClass = clazz;
            }
        }
    

    如果是@Adaptive标注在方法级别呢?

    动态生成字节码,然后进行动态加载。那么这个时候锁返回的 Class,如果加载的是 Protocol.class,应该是 Protocol$Adaptive这个 cachedDefaultName 实际上就是扩展点接口的@SPI 注解对应的名字,如果此时加载的是 Protocol.class,那么cachedDefaultName=dubbo 。

        private Class<?> createAdaptiveExtensionClass() {
            String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
            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);
        }
    

    简单的来说,基于方法层面的@Adaptive ,会根据具体的方法传参来决定使用哪个扩展接口实现,示意图如下:

    image-20201026100734651

    Activate自动激活扩展点

    自动激活扩展点有点类似Spring-Boot中的@Conditional标签,当满足什么条件时,才会加载当前Bean。

    @Activate(group = {CONSUMER, PROVIDER}, value = CACHE_KEY)
    public class CacheFilter implements Filter {
    

    如上,在CacheFilter中,group 表示客户端和和服务端都会加载, value 表示 url 中有 cache_key 的时候 ,才会加载当前的CacheFilter对象。

        public static void main(String[] args) { 
            ExtensionLoader<Filter> loader=ExtensionLoader.getExtensionLoader(Filter.class);
            URL url=new URL("","",0);
            //当把cache写进参数中时,会把CacheFilter加载进来
            //url=url.addParameter("cache","cache");
            List<Filter> filters=loader.getActivateExtension(url,"cache");
            System.out.println(filters.size());//结果分别是10 或者11
        }
    
    每天都是一个开始,送给正在奋斗的自己和你!
  • 相关阅读:
    iOS面试题
    iOS-block
    iOS开发设计模式
    iOS-宏定义
    正则表达式(转)
    iOS-textfield控制光标开始位置
    initWithNibName&initWithCoder &awakeFromNib&UIView常见属性方法
    iOS应用生命周期
    iOS-app发布新版本步骤
    iOS从App跳转至系统设置菜单各功能项
  • 原文地址:https://www.cnblogs.com/ant-world/p/13877237.html
Copyright © 2020-2023  润新知