浅析Java SPI 与 dubbo SPI

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遍历到的时候才会实例化对象,那能不能在遇到自己想要的实现对象时就停止遍历,避免不必要的资源消耗呢?

补充:下面看下Dubbo SPI 和 Java SPI 区别?

JDK SPI

JDK 标准的 SPI 会一次性加载所有的扩展实现,如果有的扩展吃实话很耗时,但

也没用上,很浪费资源。

所以只希望加载某个的实现,就不现实了

DUBBO SPI

1,对 Dubbo 进行扩展,不需要改动 Dubbo 的源码

2,延迟加载,可以一次只加载自己想要加载的扩展实现。

3,增加了对扩展点 IOC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。

3,Dubbo 的扩展机制能很好的支持第三方 IoC 容器,默认支持 Spring Bean。

以上就是Java SPI 与 dubbo SPI的详细内容,更多关于Java SPI 与 dubbo SPI的资料请关注我们其它相关文章!

(0)

相关推荐

  • Java和Dubbo的SPI机制原理解析

    SPI: 简单理解就是,你一个接口有多种实现,然后在代码运行时候,具体选用那个实现,这时候我们就可以通过一些特定的方式来告诉程序寻用那个实现类,这就是SPI. JAVA的SPI 全称为 Service Provider Interface,是一种服务发现机制.它是约定在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,然后文件里面记录的是此 jar 包提供的具体实现类的全限定名. 这样当我们引用了某个 jar 包的时候就可以去找这个 jar 包

  • Android下拉列表(Spinner)效果(使用C#和Java分别实现)

    效果如下: C#实现代码 using Android.App; using Android.OS; using Android.Widget; namespace SpinnerDemo { [Activity(Label = "@string/ApplicationName", MainLauncher = true, Icon = "@drawable/icon")] public class MainActivity : Activity { private

  • 如何通过Maven仓库安装Spire系列的Java产品

    前言 Spire系列库中已发布的Java产品目前有三个,即Spire.PDF for Java.Spire.Presentation for Java.Spire.Barcode for Java.使用该Java产品,除了可以从官网:https://www.e-iceblue.cn/Downloads.html下载,也可以通过Maven来安装使用.本教程中,以通过Maven安装Spire.PDF for Java为例介绍具体安装方法和步骤. 首先,在pom.xml文件中配置Maven仓库路径.

  • Java利用Sping框架编写RPC远程过程调用服务的教程

    RPC,即 Remote Procedure Call(远程过程调用),说得通俗一点就是:调用远程计算机上的服务,就像调用本地服务一样. RPC 可基于 HTTP 或 TCP 协议,Web Service 就是基于 HTTP 协议的 RPC,它具有良好的跨平台性,但其性能却不如基于 TCP 协议的 RPC.会两方面会直接影响 RPC 的性能,一是传输方式,二是序列化. 众所周知,TCP 是传输层协议,HTTP 是应用层协议,而传输层较应用层更加底层,在数据传输方面,越底层越快,因此,在一般情况下

  • SpingBoot+Dubbo+Zookeeper实现简单分布式开发的应用详解

    开始接触分布式概念,学习之前要准备搭建Dubbo和Zookeeper环境的简单搭建. Window下安装Zookeeper和Dubbo-admin 1.Apache官网下载Zookeeper 点击官网地址下载最新版 下载完成后,打开apache-zookeeper-3.6.2-bin\bin下zkServer.cmd,正常第一次都会闪退的,因为没有配置好zoo.cfg配置文件. 将conf目录下的zoo_sample.cfg文件,复制一份,重命名为zoo.cfg 在apache-zookeepe

  • 浅析Java SPI 与 dubbo SPI

    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 sayHel

  • 浅析Java中的SPI原理

    在面向对象的程序设计中,模块之间交互采用接口编程,通常情况下调用方不需要知道被调用方的内部实现细节,因为一旦涉及到了具体实现,如果需要换一种实现就需要修改代码,这违反了程序设计的"开闭原则".所以我们一般有两种选择:一种是使用API(Application Programming Interface),另一种是SPI(Service Provider Interface),API通常被应用程序开发人员使用,而SPI通常被框架扩展人员使用. 在进入下面学习之前,我们先来再加深一下API和

  • 详解SPI在Dubbo中的应用

    目录 一.概述 二.JDK自带SPI 2.1.代码示例 2.2.简单分析 三.SPI与双亲委派 3.1.SPI加载到何处 3.2.SPI是否破坏了双亲委派 四.Dubbo SPI 4.1.基本概念 4.2.代码示例 4.3.源码分析 五.总结 一.概述 SPI 全称为 Service Provider Interface,是一种模块间组件相互引用的机制.其方案通常是提供方将接口实现类的全名配置在classPath下的指定文件中,由调用方读取并加载.这样需要替换某个组件时,只需要引入新的JAR包并

  • 浅析java 的 static 关键字用法

    本篇浅析java中static的用法,主要五个方面:静态成员变量,静态方法,静态块,静态内部类,静态导包. 首先还是一张表格说一下静态对象和非静态对象的区别: 静态对象 非静态对象 归属 类共同具有 类的各个实例独立拥有 内存分配 内存空间上固定的 附属类分配 分配空间顺序 优先分配静态对象空间 优先分配静态对象空间,初始化也一样 1 静态变量,静态方法,静态块 静态对象,静态方法都是在原对象和方法上加上static关键字修饰,表示类可以直接调用这些,而不需要实例化后再调用.具有的好处是: 1-

  • 浅析Java编程中类和对象的定义

    1,什么是类? 答:类是客观存在的,抽象的,概念的东西. 2,什么事对象? 答:对象是具体的,实际的,代表一个事物.例如:车是一个类,汽车,自行车就是他的对象. 关于类与对象的描述:类是对象的模版,对象是类的一个个体. 3,Java中定义类的方法? class 类名 用Java语法定义人类: public class Person { } 4,对象的定义方法? 1,对象声明:类名 对象名: 2,对象创建 对象名 =  new 类名(): new作用:分配内存空间. 也可以合写为:类名 对象名 =

  • 浅析Java类和数据结构中常用的方法

    1.Object类里面常用的方法: protected Object clone()创建并返回此对象的一个副本. boolean equals(Object obj)指示其他某个对象是否与此对象"相等". protected void finalize()当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法. Class<?> getClass()返回此 Object 的运行时类. int hashCode()返回该对象的哈希码值. void notif

  • 浅析Java内存模型与垃圾回收

    1.Java内存模型 Java虚拟机在执行程序时把它管理的内存分为若干数据区域,这些数据区域分布情况如下图所示: 程序计数器:一块较小内存区域,指向当前所执行的字节码.如果线程正在执行一个Java方法,这个计数器记录正在执行的虚拟机字节码指令的地址,如果执行的是Native方法,这个计算器值为空. Java虚拟机栈:线程私有的,其生命周期和线程一致,每个方法执行时都会创建一个栈帧用于存储局部变量表.操作数栈.动态链接.方法出口等信息. 本地方法栈:与虚拟机栈功能类似,只不过虚拟机栈为虚拟机执行J

  • 深入浅析java web log4j 配置及在web项目中配置Log4j的技巧

    在上篇文章给大家介绍了Java log4j详细教程,本文给大家介绍java web log4j配置及web项目中配置log4j的技巧.具体详情请看下文吧. 首先给大家提供log4j.jar下载:http://logging.apache.org/log4j/1.2/download.html 一.java web项目使用log4j 1.在web.xml文件中添加 <!-- 配置log4j --> <context-param> <param-name>webAppRoo

  • 浅析java中stringBuilder的用法

    String对象是不可改变的.每次使用 System.String类中的方法之一时,都要在内存中创建一个新的字符串对象,这就需要为该新对象分配新的空间.在需要对字符串执行重复修改的情况下,与创建新的 String对象相关的系统开销可能会非常昂贵.如果要修改字符串而不创建新的对象,则可以使用System.Text.StringBuilder类.例如,当在一个循环中将许多字符串连接在一起时,使用 StringBuilder类可以提升性能. 通过用一个重载的构造函数方法初始化变量,可以创建 Strin

  • 浅析java修饰符访问权限(动力节点Java学院整理)

    Java有四种访问权限,其中三种有访问权限修饰符,分别为private,public和protected,还有一种不带任何修饰符: 1. private: Java语言中对访问权限限制的最窄的修饰符,一般称之为"私有的".被其修饰的类.属性以及方法只能被该类的对象访问,其子类不能访问,更不能允许跨包访问. 2. default:即不加任何访问修饰符,通常称为"默认访问模式".该模式下,只允许在同一个包中进行访问. 3. protect: 介于public 和 pri

随机推荐