首页 > 编程语言 >Dubbo(一)_Java_SPI

Dubbo(一)_Java_SPI

时间:2023-07-29 23:57:26浏览次数:38  
标签:Dubbo Java 实现 IProtocol ServiceLoader SPI protocol public 加载

什么是 SPI?

Dubbo 的源码中大量涉及了 Java SPI设计思想,所以理解 SPI对理解 Dubbo源码有很大帮助。

Java SPI全称 Java Service Provider Interface,是 Java 提供的一种服务提供者发现机制。其核心功能是通过接口找到其实现类。在实际运用中,主要用在程序启动或者运行时,通过 SPI 机制,加载并装配接口的实现类,实现组件替换和动态扩展


数据库驱动——SPI

在使用 MySQL/Oracle 数据库时,只需要引入 MySQL驱动 jar包或者 Oracle驱动 jar包就可以了,它的实现关键点就是 DriverManager

  1. DriverManager 通过 SPI机制加载不同厂商的驱动;
  2. DriverManger 使用厂商的驱动获取连接。
//DriverManager使用SPI加载Driver扩展实现,如com.mysql.jdbc.Driver
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();

//DriverManager使用加载到的厂商驱动获取连接。
Connection con = aDriver.driver.connect(url, info);

这里 ServiceLoader.load(xxx.class) 实现了 Driver 的加载,它会加载我们引入的 mysql-connector-java.jar 里的 /META-INF/services/java.sql.Driver 文件,该文件里的内容为 com.mysql.jdbc.Driver

这样做用什么好处呢?

  • JDK 数据库连接操作和驱动的实现彻底解耦;
  • 使用方只需要关心自己使用的数据库驱动,如果用 MySQL 的驱动,就不用管 Oracle 的驱动;
  • 使用方不需要加载所有的数据库驱动;
  • 使用方几乎不需要配置,只是切换数据库驱动。

使用 Java SPI设计协议组件

根据数据库驱动的例子,我们知道了 Java SPI 的基本用法:

  • 在 jar 包的META-INF/services下面,创建一个接口全限定名的文件;
  • 在接口全限定名文件中,逐行填写具体的实现类;
  • 使用方引入 jar 包;
  • 使用ServiceLoader.load加载实现类;
  • 遍历获取实现类。

假定有这样一个需求:需要实现一个通信软件,它支持 TCP/HTTP 协议,并且使用者能够在两个协议中自由切换,如果只使用 TCP协议,就不需要关注 HTTP协议。

可以按照以下思路实现:

  • 抽象协议接口模块,里面包含 IProtocol协议接口和 RequestUtil对协议调用的封装;
  • 新建 HTTP 协议实现包,用于实现 HTTP 协议,可以把它想象成 MySQL 的驱动包;
  • 新建 TCP 协议实现包,用于实现 TCP 协议,可以把它想象成 Oracle 的驱动包;
  • 使用时,只需要引入自己用到的协议包即可。

新建协议接口包

  • 新建 IProtocol 接口,用于协议的抽象;
  • 新建 RequestUtil,里面使用 SPI 自动加载协议实现类。
/**
 * 通讯协议接口
 */
public interface IProtocol {
    //发送请求
    void sendRequest(String message);
}


// 工具类
public class RequestUtil {

    //根据依赖倒置原则,这里依赖的是IProtocol接口,而不是具体的实现类。
    private IProtocol protocol;

    public void sendRequest(String message){
        //根据里氏替换原则,这个protocol可以使用子类HttpProtocol或TcpProtocol替换
        protocol.sendRequest(message);
    }

    //获取RequestUtil实例
    private static RequestUtil requestUtil;
    public static RequestUtil getInstance(){
        if(requestUtil == null){
            requestUtil = new RequestUtil();
        }
        return requestUtil;
    }

    public RequestUtil() {
        //初始化时,使用Java SPI初始化具体的协议实现类
        ServiceLoader<IProtocol> protocols = ServiceLoader.load(IProtocol.class);
        Iterator<IProtocol> iterator = protocols.iterator();
        if(iterator.hasNext()){
            protocol = iterator.next();
        }
    }
    public RequestUtil(IProtocol protocol) {
        this.protocol = protocol;
    }
    public IProtocol getProtocol() {
        return protocol;
    }

    public void setProtocol(IProtocol protocol) {
        this.protocol = protocol;
    }
}

新建 HTTP 协议实现包

  • 新建 HttpProtocol 实现类,实现 IProtocol 接口;
  • 新建 resources/META-INF/services/com.yuqiao.deeplearningdubbo.javaspi.protocol.v2.IProtocol 文件,并在文件中填写 com.yuqiao.deeplearningdubbo.javaspi.protocol.v2.http.HttpProtocol 项目结构。
/**
 * Http通讯协议
 */
public class HttpProtocol implements IProtocol {
    @Override
    public void sendRequest(String message) {
        //示意代码,省略实现细节
        System.out.println("使用Http发送消息:" + message);
    }
}

同理新建 TCP 协议实现包。

具体使用

  • 引入将要使用的协议包(这里以 HTTTP为例)
<dependency>
  <groupId>com.yuqiao.deeplearningdubbo</groupId>
  <artifactId>java_spi_protocol_v2_http</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>
  • 调用 API
/**
 * 协议调用方
 */
public class JavaSpiProtocolInvoker {
    public static void main(String[] args) {
        RequestUtil.getInstance().sendRequest("hello!");
    }
}

SPI 实现原理分析

Java SPI 的核心实现类是 ServiceLoader,先调用 ServiceLoader.load 加载实现类,然后遍历获取实现类。代码如下:

//第一步:调用ServiceLoader.load加载实现类
ServiceLoader<IProtocol> protocols = ServiceLoader.load(IProtocol.class);
//第二步:通过遍历获取实现类
Iterator<IProtocol> iterator = protocols.iterator();
while (iterator.hasNext()){
    IProtocol protocol = iterator.next();
    System.out.println(protocol);
}

ServiceLoader 类

类的相关核心属性如下:

public final class ServiceLoader<S>
    implements Iterable<S>
{
    // 实现类的默认文件路径前缀
    private static final String PREFIX = "META-INF/services/";

    // 被加载的类或者接口(IProtocol)
    private final Class<S> service;

    // 实现类的类加载器,默认为调用load方法的线程的上下文类加载器
    private final ClassLoader loader;

    // 访问控制上下文,用于控制类加载器的权限
    private final AccessControlContext acc;

    // 缓存已经实例化后的实现类
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 懒迭代器,支持遍历时懒加载
    private LazyIterator lookupIterator;

接下来对 ServiceLoader.load() 方法的具体步骤进行源码分析:

    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
        // 这里实例化了一个 ServiceLoader 对象
        return new ServiceLoader<>(service, loader);
    }

    // ServiceLoader 的构造函数
    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();
    }

    // reload 清空缓存的已加载的实现类实例,初始化 LazyIterator 迭代器
    public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }

可以发现上述代码,都只是做了初始化,并没有真正地加载实现类,实现类的加载在后面。

ServiceLoader 类实现了 Iterator 接口,重写了 iterator() 方法实现,代码如下:

public Iterator<S> iterator() {
    return new Iterator<S>() {
        // knowProviders 缓存已经实例化的实现类
        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();

        // 如果实现类有缓存,直接返回;否则调用 LazyIterator 的 hasNext() 方法
        public boolean hasNext() {
            if (knownProviders.hasNext())
                return true;
            return lookupIterator.hasNext();
        }

        // 不存在已加载的实现类,就调用 LazyIterator 的 next() 方法
        public S next() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

    };
}

LazyIterator 类

LazyIterator 类的核心属性如下:

private class LazyIterator
    implements Iterator<S>
{
    //要加载的类或者接口
    Class<S> service;
    //类加载器,加载文件资源和类
    ClassLoader loader;
    //存储加载到的文件资源(META-INF/services/接口或类全限定名)
    Enumeration<URL> configs = null;
    //从文件路径中加载到的实现类全限定名
    Iterator<String> pending = null;
    String nextName = null;
}

上面提到过,ServiceLoader类在调用 Iterator()接口时如果没有发现实现类缓存,就会调用 LazyIteratornextService()hasNextService()

hasNextService() 方法通过传入的类加载器加载了 META-INF/services/ 文件夹下的文件,文件内容为接口实现类的全限定类名,然后存储到 configs 变量中。

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            //全路径名=META-INF/services/ + 类或接口的全限定名
            String fullName = PREFIX + service.getName();
            //加载所有的全路径名文件
            //如:META-INF/services/com.yuqiao.deeplearningdubbo.javaspi.protocol.v2.IProtocol
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return false;
        }
        //解析文件中所有的实现类名
        //如:com.yuqiao.deeplearningdubbo.javaspi.protocol.v2.tcp.TcpProtocol
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
}

nextService() 方法根据 hasNextService() 方法获取到的类名来加载类,并进行类的实例化,最后将实例化的对象保存到缓存中。

标签:Dubbo,Java,实现,IProtocol,ServiceLoader,SPI,protocol,public,加载
From: https://www.cnblogs.com/istitches/p/17590830.html

相关文章

  • Dubbo(二)_入门
    什么是dubbodubbo最新版本为3.x,ApacheDubbo是一款易用、高性能的web和rpc框架,同时为构建企业级微服务提供服务发现、流量治理、可观测、认证鉴权等能力。Dubbo3替代了阿里运行多年的HSF框架,依托于Dubbo实现自己的微服务解决方案(DNS),即Dubbo、Nacos(服务注册与管理)......
  • Dubbo(五)_服务注册原理
    服务注册是指将服务暴露出来的过程,包括了服务解析、服务启动、服务注册三部分。其中服务解析就是将Dubbo的服务配置解析成Spring的Bean对象;服务启动是启动一个可以处理请求的服务;服务注册是指将服务信息保存到注册中心中,供服务消费方获取。Dubbo的注册中心支持Redis、Zooke......
  • Dubbo(四)_全局架构
    整体架构全局架构分为注册中心,通常为zk/redis;服务提供者Provider,用来提供并注册服务到注册中心;服务消费者Consumer,用来向注册中心订阅服务,当注册中心服务发生变动时notify通知消费者,消费者通过invoke远程调用服务提供者;Monitor监控者用来监控服务,统计服务的调用次数和调......
  • Dubbo(三)_spi
    DubboSPI源码分析DubboSPI的核心实现是ExtensionLoader,分析时先分析ExtensionLoader的成员变量和对公方法,依次分析扩展点的加载、扩展点的依赖注入、扩展点的自适应、扩展点的激活。分析中的名词约定:扩展点————扩展点实例化的对象,如org.apache.dubbo.rpc.protocol.d......
  • 4.JAVA的特性和优势
    4.JAVA的特性和优势跨平台/可移植性这是Java的核心优势。Java在设计时就很注重移植和跨平台性。比如:Java的int永远都是32位。不像C++可能是16,32,可能是根据编译器厂商规定的变化。这样的话程序的移植就会非常麻烦。安全性Java适合于网络/分布式环境,为了达到这个目标,在安全性......
  • JAVA体系结构
    JAVA体系结构JavaSE(Java Standard Edition):标准版,定位在个人计算机上的应用这个版本是Java平台的核心,它提供了非常丰富的API来开发一般个人计算机上的应用程序,包括用户界面接口AWT及Swing,网络功能与国际化、图像处理能力以及输入输出支持等。在上世纪90年代末互联网上大放异......
  • Java面试题 P16:Redis篇:Redis使用场景-缓存-缓存穿透
    缓存涉及问题:1、缓存三兄弟:穿透击穿雪崩2、双写一致3、持久化4、数据过期策略5、数据淘汰策略如果发生了缓存穿透,击穿,雪崩,该如何解决?1、缓存穿透什么是缓存穿透,当查询一个不存在的数据,查redis中没有,mysql查询也没有,数据也不会直接写入到redis,就导致每次都要请求数据库,......
  • 【Java】使用fastjson进行序列化时出现空指针异常问题研究
    最近在使用fastjson的JSONObject.toJSONString()方法将bean对象转为字符串的时候报如下错误:com.alibaba.fastjson.JSONException:writejavaBeanerror,fastjsonversion1.2.58,classcom.sun.proxy.$Proxy395,fieldName:0 atcom.alibaba.fastjson.serializer.JavaBeanS......
  • Java面试题 P15:Redis篇:面试场景
    Redis内容: 面试题总结: ......
  • chrome 翻译功能 与 禁止/拦截JavaScript Disable JavaScript
    在chrome://settings/content/siteDetails?site=中禁止JavaScript再恢复无法使用翻译功能但是在devtools中DisableJavaScript,再取消选中,可以翻译 ......