当前位置 博文首页 > 茕祇:Java SPI 与 dubbo SPI

    茕祇:Java SPI 与 dubbo SPI

    作者:茕祇 时间:2021-05-29 18:23

    Java原生SPI

    面向接口编程+策略模式

    实现

    建立接口

    Robot

    public interface Robot {
        /**
         * 测试方法1
         */
        void sayHello();
    }
    
    多个实现类实现接口

    RobotA

    public class RobotA implements Robot {
        public RobotA() {
            System.out.println("Happy RobotA is loaded");
        }
        @Override
        public void sayHello() {
            System.out.println("i am a very very happy Robot ");
        }
        public void sayBye(){}
    }
    

    RobotB

    public class RobotB implements Robot {
        public RobotB() {
            System.out.println("SB RobotB is loaded");
        }
        @Override
        public void sayHello() {
            System.out.println("i am a da sha bi ");
        }
        public void sayBye(){}
    }
    
    配置实现类与接口

    META-INF/services目录下建立一个以接口全限定名为名字的文件,里面的内容是实现类的全限定名

    原理

    通过ServiceLoader与配置文件中的全限定名加载所有实现类,根据迭代器获取具体的某一个类

    我们通过对下面一段代码的分析来说明

    ServiceLoader<Robot> serviceLoader=ServiceLoader.load(Robot.class);
    serviceLoader.forEach(Robot::sayHello);
    

    load(Robot.class)这个方法的目的只是为了设置类加载器为线程上下文加载器,我们当然可以不这么做,直接调用load(Class service,ClassLoader loader)方法

    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
    

    这个load方法其实也没有做什么实质的事,仅仅是实例化了一个ServiceLoad对象返回罢了

    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
        return new ServiceLoader<>(service, loader);
    }
    

    那是不是构造方法做了最核心的事呢?

    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }
    public void reload() {
        //这里的provider是一个对于已实例化对象的缓存,为Map类型
            providers.clear();
            lookupIterator = new LazyIterator(service, loader);
        }
    

    没有,这里仅仅只是检验了参数和权限这样一些准备操作.然后实例化了一个LazyIterator

    这是LazyIterator的构造函数

    private LazyIterator(Class<S> service, ClassLoader loader) {
        this.service = service;
        this.loader = loader;
    }
    

    然后....,没了,ServiceLoader<Robot> serviceLoader=ServiceLoader.load(Robot.class);执行完毕了,到这里,并没有实例化我们所需要的Robot对象,而仅仅只是返回了一个ServiceLoader对象

    这时候如果我们去看serviceLoader的对象方法是这样的

    有用的只有这三个方法,reload上面已经提到过,只是重新实例化一个对象而已.

    而另外两个iterator()是个迭代器,foreach也只是用于迭代的语法糖罢了.如果我们debug的话,会发现foreach的核心依旧会变成iterator(),好了,接下来重点看iterator()

    public Iterator<S> iterator() {
        return new Iterator<S>() {
    
            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();
    
            public boolean hasNext() {
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }
    
            public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }
    
            public void remove() {
                throw new UnsupportedOperationException();
            }
    
        };
    

    这个方法实际上是返回了一个Iterator对象.而通过这个Iterator,我们可以遍历获取我们所需要的Robot对象.

    我们来看其用于获取对象的next方法

     public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }
    

    这个方法是先在缓存里找,缓存里找不到,就需要用最开始的实例化的lookupIterator

    再来看看它的next方法

    public S next() {
        if (acc == null) {
            return nextService();
        } else {
            PrivilegedAction<S> action = new PrivilegedAction<S>() {
                public S run() { return nextService(); }
            };
            return AccessController.doPrivileged(action, acc);
        }
    }
    

    这方法的核心是nextService,我们继续看实现,这个方法比较长,我贴一部分核心

    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,
             "Provider " + cn + " not found");
    }
    

    hasNextService()判断是否还可以继续迭代,通过class.forName反射获取实例,最后再加入到provider缓存中.于是基本逻辑就完成了.那nextName哪来的.是在hasNextService()中获取的.

    依旧只有核心代码

    //获取文件
    String fullName = PREFIX + service.getName();
    if (loader == null)
        configs = ClassLoader.getSystemResources(fullName);
    else
        configs = loader.getResources(fullName);
    //解析文件配置
    while ((pending == null) || !pending.hasNext()) {
                    if (!configs.hasMoreElements()) {
                        return false;
                    }
                    pending = parse(service, configs.nextElement());
                }
                nextName = pending.next();
    

    根据前缀(即META-INF/services)和接口的全限定名去找到对应的配置文件.然后加载里面的配置,获取具体实现类的名字.

    Dubbo增强SPI

    实现

    建立接口

    与原生SPI不同,dubbo需要加入@SPI注解

    Robot

    @SPI
    public interface Robot {
        /**
         * 测试方法1
         */
        void sayHello();
    }
    
    多个实现类实现接口

    RobotA

    public class RobotA implements Robot {
        public RobotA() {
            System.out.println("Happy RobotA is loaded");
        }
        @Override
        public void sayHello() {
            System.out.println("i am a very very happy Robot ");
        }
        public void sayBye(){}
    }
    

    RobotB

    public class RobotB implements Robot {
        public RobotB() {
            System.out.println("SB RobotB is loaded");
        }
        @Override
        public void sayHello() {
            System.out.println("i am a da sha bi ");
        }
        public void sayBye(){}
    }
    
    配置实现类与接口

    META-INF/dubbo目录下建立一个以接口全限定名为名字的文件,里面的内容是自定义名字与类的全限定名的键值对,举个例子

    robotA = cn.testlove.double_dubbo.inter.impl.RobotA
    robotB=cn.testlove.double_dubbo.inter.impl.RobotB
    

    原理

    我们通过对下列代码的调用来进行分析

    ExtensionLoader<Robot> extensionLoader= ExtensionLoader.getExtensionLoader(Robot.class);
    Robot robotB = extensionLoader.getExtension("robotB");
    

    第一句代码没什么好说的,只是获取一个RobotExtensionLoader对象并且缓存在Map中,下次如果是同样的接口可以直接从map中获取

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

    再来看第二句代码

    //从缓存中找
    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);
            }
        }
    }
    

    首先从缓存里找,找不到再创建一个新的对象。

    再看createExtension(name)方法

     Class<?> clazz = getExtensionClasses().get(name);
    
    T instance = (T) EXTENSION_INSTANCES.get(clazz);
    if (instance == null) {
        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;
    

    注意对于Class<?> clazz = getExtensionClasses().get(name);这一句的理解,这一句是获取配置文件中所有类的Class实例,而不是获取所有扩展类的实例。

    接下来的流程其实也就简单了从EXTENSION_INSTANCES缓存中获取instance实例,如果没有,就借助Class对象实例化一个,再放入缓存中

    接着用这个instance去实例化一个包装类然后返回.自此,一个我们需要的对象产生了.

    最后我们看看getExtensionClasses()这个方法

    Map<String, Class<?>> classes = cachedClasses.get();
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
    

    这里的classes就是用来存各个扩展类Class的Map缓存,如果不存在的话,会调用loadExtensionClasses();去加载,剩下的就是找到对应路径下的配置文件,获取全限定名了

    上文我在分析Dubbo SPI时,多次提到Map,缓存二词,我们可以具体有以下这些.其实看名字就大概知道作用了

    private final ConcurrentMap<Class<?>, String> cachedNames = new ConcurrentHashMap<>();
    private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>()    
    private final Map<String, Object> cachedActivates = new ConcurrentHashMap<>();
    private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
    private final Holder<Object> cachedAdaptiveInstance = new Holder<>();
    private volatile Class<?> cachedAdaptiveClass = null;
    private String cachedDefaultName;
    

    对比原生的java SPI,dubbo的无疑更灵活,可以按需去加载某个类,也可以很便捷的通过自定义的名字去获取类.而且Dubbo还支持setter注入.这点以后再讲.

    最后提一个问题,java原生的SPI只有在用iterator遍历到的时候才会实例化对象,那能不能在遇到自己想要的实现对象时就停止遍历,避免不必要的资源消耗呢?

    bk
    下一篇:没有了