美文网首页Java 杂谈Dubbo分布式架构
Dubbo的SPI实现以及与JDK实现的区别

Dubbo的SPI实现以及与JDK实现的区别

作者: 金桔文案 | 来源:发表于2018-06-28 15:55 被阅读104次

    在 Java 里, 为了规范开发,制定了大量的「规范」与「标准」,这些上层的内容,大多是以接口的形式提供出来。那这些接口最终实现是谁呢,在哪里呢?

    规范并不关心这个。

    所谓规范,是指定了一系列内容,来指导我们的开发实现。比如 Servlet规范对于 Servlet 的行为做了说明,具体实现时,可以是 Tomcat,可以是Jetty 等等。

    再比如 Java 的 JDBC 规范,规定了 Driver 提供者需要实现的内容,但具体是 Oracle,或者MySQL 都可以支持。关于JDBC 可以看之前一篇文章(没想到你是这样的 JDBC)。在之前我们可以通过 Class.forName来进行Driver 具体实现类的加载。从JDK1.6开始,官方提供了一个名为 「SPI」 的机制,来更方便快捷的进行对应实现类的加载,不需要我们关心。我们所需要做的,只需要将包含实现类的 JAR 文件放到 classpath中即可。

    正好最近读了一些Dubbo的源码,其中有 Dubbo 的不同于JDK的另一种 SPI实现。所以这篇我们来看 Dubbo 的 「SPI」实现以及与 JDK 实现的区别。

    首先,什么是 SPI 呢?

    SPI(Service Provider Interfaces), 可以理解成一个交给第三方实现的API。JDK文档这样描述

    A service is a well-known set of interfaces and (usually abstract) classes. A service provider is a specific implementation of a service.

    在Java 中使用到SPI的这些地方:

    • JDBC
    • JNDI
    • Java XML Processing API
    • Locael
    • NIO Channel Provider
    • ……

    通过这种SPI 的实现形式,我们的应用仿佛有了可插拔的能力。

    JDK中的SPI 是怎样实现的呢?

    在JDK中包含一个SPI最核心的类:ServiceLoader,在需要加载Provider类的时候,我们所要做的是:

    ServiceLoader.load(Provider.class);

    在JDK中规范了 Service Provider的路径,所有 Provider必须在JAR文件的META-INF/services目录下包含一个文件,文件名就是我们要实现的Service的名称全路径。比如我们熟悉的JDBC 的MySQL实现, 在mysql-connector中,就有这样一个文件
    META-INF/services/java.sql.Driver


    微1.png

    这些provider是什么时候加载的呢?

    由于Provider 的加载和初始化是Lazy的实现,所以需要的时候,可以遍历Provider 的 Iterator,按需要加载,已经加载的会存放到缓存中。

    但有些实现不想Lazy,就直接在 ServiceLoader 的load执行之后直接把所有的实现都加载和初始化了,比如这次说的JDBC,所以这里在Tomcat里有个处理内存泄漏的,可以查看之前的文章(Tomcat与内存泄露处理)

    继续说回具体的加载时机。我们一般在Spring 的配置中会增加一个datasource,这个数据源一般会在启动时做为一个Bean被初始化,此时数据源中配置的driver会被设置。
    这些内容传入Bean中,会调用DriverManager的初始化

    static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
    }

    loadInitialDrivers 执行的的时候,除了ServiceLoader.load外,还进行了初始化

    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    try{
    while(driversIterator.hasNext()) {
    driversIterator.next();
    }
    } catch(Throwable t) {
    // Do nothing
    }
    return null;

    我们再来看 Dubbo 的SPI实现方式。如果你能看下 Dubbo 的源码就会发现,实现时并没有使用 JDK 的SPI,而是自已设计了一种。

    我们以Main class启动来看看具体的实现。
    我们从使用的入口处来看,第一步传入一个接口, 然后再传入期待的实现的名称

    SpringContainer container = (SpringContainer) ExtensionLoader.getExtensionLoader(Container.class).getExtension("spring");
    

    这里传入的是Container.class, 期待的实现是spring。

     1// synchronized in getExtensionClasses
     2    private Map<String, Class<?>> loadExtensionClasses() {
     3        final SPI defaultAnnotation = type.getAnnotation(SPI.class);
     4        if (defaultAnnotation != null) {
     5            String value = defaultAnnotation.value();
     6            if ((value = value.trim()).length() > 0) {
     7                String[] names = NAME_SEPARATOR.split(value);
     8                if (names.length > 1) {
     9                    throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()
    10                            + ": " + Arrays.toString(names));
    11                }
    12                if (names.length == 1) cachedDefaultName = names[0];
    13            }
    14        }
    15
    16        Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
    17        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
    18        loadDirectory(extensionClasses, DUBBO_DIRECTORY);
    19        loadDirectory(extensionClasses, SERVICES_DIRECTORY);
    20        return extensionClasses;
    21    }
    

    共从三个地方加载扩展的class

    • DUBBO_INTERNAL_DIRECTORY META-INF/dubbo/internal/
    • DUBBO_DIRECTORY META-INF/dubbo/
    • SERVICES_DIRECTORY META-INF/services/


      微2.png
     1private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
     2        String fileName = dir + type.getName();
     3        try {
     4            Enumeration<java.net.URL> urls;
     5            ClassLoader classLoader = findClassLoader();
     6            if (classLoader != null) {
     7                urls = classLoader.getResources(fileName);
     8            } else {
     9                urls = ClassLoader.getSystemResources(fileName);
    10            }
    11            if (urls != null) {
    12                while (urls.hasMoreElements()) {
    13                    java.net.URL resourceURL = urls.nextElement();  
    14                    loadResource(extensionClasses, classLoader, resourceURL);
    15                }
    16            }
    17        } catch (Throwable t) {
    18            logger.error("Exception when load extension class(interface: " +
    19                    type + ", description file: " + fileName + ").", t);
    20        }
    21    }
    

    这里通过classLoader,寻找符合传入的特定名称的文件,java.net.URL resourceURL = urls.nextElement();
    此时会得到一个包含该文件的URLPath, 再通过loadResource,将资源加载
    此时得到的文件内容是
    spring=com.alibaba.dubbo.container.spring.SpringContainer
    再进一步,将等号后面的class加载,即可完成。

    loadClass时,并不是直接通过类似Class.forName等形式加载,而是下面这个样子:

     1private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
     2        if (!type.isAssignableFrom(clazz)) {
     3            throw new IllegalStateException("Error when load extension class(interface: " +
     4                    type + ", class line: " + clazz.getName() + "), class "
     5                    + clazz.getName() + "is not subtype of interface.");
     6        }
     7        if (clazz.isAnnotationPresent(Adaptive.class)) {
     8            if (cachedAdaptiveClass == null) {
     9                cachedAdaptiveClass = clazz;
    10            } else if (!cachedAdaptiveClass.equals(clazz)) {
    11                throw new IllegalStateException("More than 1 adaptive class found: "
    12                        + cachedAdaptiveClass.getClass().getName()
    13                        + ", " + clazz.getClass().getName());
    14            }
    15        } else if (isWrapperClass(clazz)) {
    16            Set<Class<?>> wrappers = cachedWrapperClasses;
    17            if (wrappers == null) {
    18                cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
    19                wrappers = cachedWrapperClasses;
    20            }
    21            wrappers.add(clazz);
    22        } else {
    23            clazz.getConstructor();
    24            if (name == null || name.length() == 0) {
    25                name = findAnnotationName(clazz);
    26                if (name.length() == 0) {
    27                    throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
    28                }
    29            }
    30            String[] names = NAME_SEPARATOR.split(name);
    31            if (names != null && names.length > 0) {
    32                Activate activate = clazz.getAnnotation(Activate.class);
    33                if (activate != null) {
    34                    cachedActivates.put(names[0], activate);
    35                }
    36                for (String n : names) {
    37                    if (!cachedNames.containsKey(clazz)) {
    38                        cachedNames.put(clazz, n);
    39                    }
    40                    Class<?> c = extensionClasses.get(n);
    41                    if (c == null) {
    42                        extensionClasses.put(n, clazz);
    43                    } else if (c != clazz) {
    44                        throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + n + " on " + c.getName() + " and " + clazz.getName());
    45                    }
    46                }
    47            }
    48        }
    49    }
    

    加载之后,需要对class进行初始化,此时直接newInstance一个,再通过反射注入的方式将对应的属性设置进去。

     1private T createExtension(String name) {
     2        Class<?> clazz = getExtensionClasses().get(name);
     3        if (clazz == null) {
     4            throw findException(name);
     5        }
     6        try {
     7            T instance = (T) EXTENSION_INSTANCES.get(clazz);
     8            if (instance == null) {
     9                EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
    10                instance = (T) EXTENSION_INSTANCES.get(clazz);
    11            }
    12            injectExtension(instance);
    13            Set<Class<?>> wrapperClasses = cachedWrapperClasses;
    14            if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
    15                for (Class<?> wrapperClass : wrapperClasses) {
    16                    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
    17                }
    18            }
    19            return instance;
    20        } catch (Throwable t) {
    21            throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
    22                    type + ")  could not be instantiated: " + t.getMessage(), t);
    23        }
    24    }
    
     1private T injectExtension(T instance) {
     2        try {
     3            if (objectFactory != null) {
     4                for (Method method : instance.getClass().getMethods()) {
     5                    if (method.getName().startsWith("set")
     6                            && method.getParameterTypes().length == 1
     7                            && Modifier.isPublic(method.getModifiers())) {
     8                        Class<?> pt = method.getParameterTypes()[0];
     9                        try {
    10                            String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
    11                            Object object = objectFactory.getExtension(pt, property);
    12                            if (object != null) {
    13                                method.invoke(instance, object);
    14                            }
    15                        } catch (Exception e) {
    16                            logger.error("fail to inject via method " + method.getName()
    17                                    + " of interface " + type.getName() + ": " + e.getMessage(), e);
    18                        }
    19                    }
    20                }
    21            }
    22        } catch (Exception e) {
    23            logger.error(e.getMessage(), e);
    24        }
    25        return instance;
    26    }
    

    通过上面的描述我们看到,JDK 与 Dubbo的 SPI 实现上,虽然都是从JAR中加载对应的扩展,但还是有些明显的区别,比如:Dubbo 支持更多的加载路径,同时,并不是通过Iterator的形式,而是直接通过名称来定位具体的Provider,按需要加载,效率更高,同时支持Provider以类似IOC的形式提供等等。

    推荐内容:https://www.roncoo.com/course/list.html?courseName=Dubbo

    文章来源:https://mp.weixin.qq.com/s/YzJzx9fwM5YwiWQzuY_9Gw

    相关文章

      网友评论

        本文标题:Dubbo的SPI实现以及与JDK实现的区别

        本文链接:https://www.haomeiwen.com/subject/xoggyftx.html