Java插件扩展机制之SPI案例讲解

目录
  • 什么是SPI
  • 与 接口类-实现类 提供的RPC 方式有什么区别?
  • 假设我们需要实现RPC,是怎么做的?
  • 那RPC究竟跟SPI什么关系?
  • SPI的应用场景
  • 怎么实现一个SPI?
  • 中间件是怎么实现SPI的?
    • Apollo-Client中的实现
  • JDBC中的实现

什么是SPI

SPI ,全称为 Service Provider Interface,是一种服务发现机制。其为框架提供了一个对外可扩展的能力。

与 接口类-实现类 提供的RPC 方式有什么区别?

  • 传统的接口类实现形式为如下
public interface AdOpFromApolloService {}
public class AdOpFromDbServiceImpl implements AdOpFromDbService {}

假设我们需要实现RPC,是怎么做的?

RPC会在对应的接口类AdOpFromApolloService新增一个注解用于标注是RPC类,然后将当前类放在依赖包提供给其他项目来通过接口类进行调用

简而言之:RPC调用中只提供接口类,然后让第三方调用(第三方只调,不写实现)

那RPC究竟跟SPI什么关系?

  • SPI:提供接口,第三方引用包之后可重写或用默认的SPI接口的实现。
  • RPC:提供接口,但是实现是私有的,不对外开放重写能力。

SPI的应用场景

框架实现案例:

  1. Spring扩展插件实现。如JDCB
  2. 中间件扩展插件实现。如Dubbo、Apollo
  3. 开发过程中举例:实现一个拦截器,并用SPI机制来扩展其拦截方式(比如全量拦截、每分钟拦截多少、拦截比例多少、拦截的日志是log打印还是落库、落es)

怎么实现一个SPI?

接下来用两个项目模块来讲解SPI的用法,先看项目结构图如下

接下来是实现的过程

第一步:创建spi-demo-contract项目,在resources目录下新建如下目录(MATE-INF/services)和文件(com.example.spidemocontract.spi.SpiTestDemoService)

  • 修改com.example.spidemocontract.spi.SpiTestDemoService文件
  • 新增第二步一样位置的接口类,以及对应的实现类

第二步:创建spi-demo项目,然后引入spi-demo-contract依赖

  • 在resources目录下新建如下目录(MATE-INF/services)和文件(com.example.spidemocontract.spi.SpiTestDemoService),文件名跟spi-demo-contract的完全一致
  • 修改com.example.spidemocontract.spi.SpiTestDemoService(与依赖包的文件名称完全一致,但内容指向了当前项目自定义的实现类)文件
  • 实现SPI接口,自定义spi-demo项目的实现类(这里可以把优先级调到最高)

第三步:在spi-demo项目中用ServiceLoader进行加载SPI接口

补充说明:我们可以重写声明类的优先级,来判断需要用哪个实现类来处理。比如重写一个优先级=0最高优先级,然后加载的时候默认只取第一个优先级最高的,那我们重写的自定义实现类就能覆盖掉默认SPI实现类

详细步骤拆分如下

  • 第一步:创建spi-demo-contract项目,在resources目录下新建如下目录(MATE-INF/services)和文件(com.example.spidemocontract.spi.SpiTestDemoService)
-- resources
---- META-INF
-------- services
------------ com.example.spidemocontract.spi.SpiTestDemoService
  • 修改com.example.spidemocontract.spi.SpiTestDemoService文件如下
com.example.spidemocontract.spi.impl.DefaultSpiTestDemoService
  • 新增第二步一样位置的接口类,以及对应的实现类
/**
* 这个接口类完全对应resources/META-INF/services/com.example.spidemocontract.spi.impl.DefaultSpiTestDemoService
**/
public interface SpiTestDemoService {
    void printLog();
    int getOrder();
}

/**
 * 将默认的设置为优先级最低,这是默认的SPI接口的实现类
 */
public class DefaultSpiTestDemoService implements SpiTestDemoService {
    @Override
    public int getOrder() {
        return Integer.MAX_VALUE;
    }
    @Override
    public void printLog() {
        System.out.println("输出 DefaultSpiTestDemoService log");
    }
}
  • 第二步:创建spi-demo项目,然后引入spi-demo-contract依赖
<dependency>
    <groupId>com.example</groupId>
    <artifactId>spi-demo-contract</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <scope>compile</scope>
</dependency>
  • 在resources目录下新建如下目录(MATE-INF/services)和文件(com.example.spidemocontract.spi.SpiTestDemoService),文件名跟spi-demo-contract的完全一致
-- resources
---- META-INF
-------- services
------------ com.example.spidemocontract.spi.SpiTestDemoService
  • 修改com.example.spidemocontract.spi.SpiTestDemoService(与依赖包的文件名称完全一致,但内容指向了当前项目自定义的实现类)文件如下
com.example.spidemo.spi.OtherSpiTestDemoService
  • 实现SPI接口,自定义spi-demo项目的实现类
/**
 * 其他,把他优先级设置的比较高
 */
public class OtherSpiTestDemoService implements SpiTestDemoService {
    // 后面我们用SPI类加载器获取时,会根据order排序,越小优先级越高
    @Override
    public int getOrder() {
        return 0;
    }
    @Override
    public void printLog() {
        System.out.println("输出 OtherSpiTestDemoService log");
    }
}
  • 第三步:在spi-demo项目中用ServiceLoader进行加载SPI接口
public static void main(String[] args) {
    // 加载SPI
    Iterator<SpiTestDemoService> iterator = ServiceLoader.load(SpiTestDemoService.class).iterator();
    // 实现了ordered,会根据ordered返回值排序,优先级越高,越先取出来
    List<SpiTestDemoService> list = Lists.newArrayList(iterator)
            .stream().sorted(Comparator.comparing(SpiTestDemoService::getOrder))
            .collect(Collectors.toList());
    for (SpiTestDemoService item : list) {
        item.printLog();
    }
}

中间件是怎么实现SPI的?

Apollo-Client中的实现

  • Apollo-Client初始化过程中,有一个SPI接口ConfigPropertySourcesProcessorHelper
// 当前接口会在resource/META-INF/services目录下对应文件com.ctrip.framework.apollo.spring.spi.ConfigPropertySourcesProcessorHelper
public interface ConfigPropertySourcesProcessorHelper extends Ordered {
  void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;
}
  • 当前SPI中的默认实现为
public class DefaultConfigPropertySourcesProcessorHelper implements ConfigPropertySourcesProcessorHelper {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        // .....各种注册bean,初始化的流程
    }
    @Override
    public int getOrder() {
        // 优先级排序置为最低。方便其他自定义spi实现类能够覆盖
        return Ordered.LOWEST_PRECEDENCE;
    }
}
  • 怎么选择加载出自定义的SPI实现类

通过ServiceLoader.load(Xxxx.class)加载出所有实例,然后根据order来进行排序优先级,order最小的那个优先级最高,只取第一个数据candidates.get(0)并返回。
因为自定义SPI实现优先级可以设置得很高,所以就可以达到覆盖默认实现的目的

public static <S extends Ordered> S loadPrimary(Class<S> clazz) {
    List<S> candidates = loadAllOrdered(clazz);
    return candidates.get(0);
}
public static <S extends Ordered> List<S> loadAllOrdered(Class<S> clazz) {
    Iterator<S> iterator = loadAll(clazz);

    if (!iterator.hasNext()) {
      throw new IllegalStateException(String.format(
          "No implementation defined in /META-INF/services/%s, please check whether the file exists and has the right implementation class!",
          clazz.getName()));
    }
    // 获取迭代中的所有SPI实现实例,然后进行排序,取优先级最高的那个
    List<S> candidates = Lists.newArrayList(iterator);
    Collections.sort(candidates, new Comparator<S>() {
      @Override
      public int compare(S o1, S o2) {
        // the smaller order has higher priority
        return Integer.compare(o1.getOrder(), o2.getOrder());
      }
    });

    return candidates;
}

JDBC中的实现

  • JDBC SPI中配置文件resources/META-INF/services/java.sql.Driver,并设置参数如下
com.example.app.driver.MyDriver
  • 继承SPI接口java.sql.Driver,实现MyDriver
public class MyDriver extends NonRegisteringDriver implements Driver {
    static {
        try {
            java.sql.DriverManager.registerDriver(new MyDriver());
        } catch (SQLException e) {
            throw new RuntimeException("Can't register driver!", e);
        }
    }
    public MyDriver() throws SQLException {}
    @Override
    public Connection connect(String url, Properties info) throws SQLException {
        System.out.println("MyDriver - 准备创建数据库连接.url:" + url);
        System.out.println("JDBC配置信息:" + info);
        info.setProperty("user", "root");
        Connection connection = super.connect(url, info);
        System.out.println("MyDriver - 数据库连接创建完成!" + connection.toString());
        return connection;
    }
}
  • 写main方法调用执行刚实现的自定义SPI实现
String url = "jdbc:mysql://localhost:3306/test?serverTimezone=UTC";
String user = "root";
String password = "root";
Class.forName("com.example.app.driver.MyDriver");
Connection connection = DriverManager.getConnection(url, user, password);
  • JDBC如何使用SPI简单分析如下

获取所有注册的驱动(每个驱动的都遍历一下,其实就是最晚注册那个就用那个了,如果失败就往下一个找)

// 获取所有注册的驱动(每个驱动的都遍历一下,其实就是最晚注册那个就用那个了,如果失败就往下一个找)
 for(DriverInfo aDriver : registeredDrivers) {
    // If the caller does not have permission to load the driver then
    // skip it.
    if(isDriverAllowed(aDriver.driver, callerCL)) {
        try {
            println("    trying " + aDriver.driver.getClass().getName());
            Connection con = aDriver.driver.connect(url, info);
            if (con != null) {
                // Success!
                println("getConnection returning " + aDriver.driver.getClass().getName());
                return (con);
            }
        } catch (SQLException ex) {
            if (reason == null) {
                reason = ex;
            }
        }

    } else {
        println("    skipping: " + aDriver.getClass().getName());
    }
}

到此这篇关于Java插件扩展机制之SPI案例讲解的文章就介绍到这了,更多相关Java插件扩展机制之SPI内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java 自旋锁(spinlock)相关知识总结

    一.前言 谈到『自旋锁』,可能大家会说,这有啥好讲的,不就是等待资源的线程"原地打转"嘛.嗯,字面理解的意思很到位,但能深入具体点吗?自旋锁的设计真就这么简单? 本文或者说本系列的目的,都是让大家不要停留在表面,而是深入分析,做到: 灵活使用 掌握原理 优缺点 二.锁的优化:自旋锁 当多个线程想同时访问同一个资源时,就存在资源冲突,这时,大家最直接想到的就是加锁来互斥访问,加锁会有这么几个问题: 等待资源的线程进入睡眠,发生用户态向内核态的切换,有一定的性能开销: 占用资源的线程很快就

  • 深入理解Java中的SPI机制

    本文通过探析JDK提供的,在开源项目中比较常用的Java SPI机制,希望给大家在实际开发实践.学习开源项目提供参考. 1 SPI是什么 SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件. 整体机制图如下: Java SPI 实际上是"基于接口的编程+策略模式+配置文件"组合实现的动态加载机制. 系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接

  • java中spi使用详解

    一.简介 java中spi(service provider interface)是jdk内置的一种服务发现机制,可以基于配置,在运行时加载指定服务.java中提供了很多服务提供接口,如jdbc.jndi等. 1.什么是SPI SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件. SPI的作用就是为这些被扩展的API寻找服务实现. 2.SPI和API的使用场景 API (Application Pr

  • 浅析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和Dubbo的SPI机制原理解析

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

  • Java进阶之SPI机制详解

    一.前言 SPI的英文全称为Service Provider Interface,字面意思为服务提供者接口,它是jdk提供给"服务提供厂商"或者"插件开发者"使用的接口. 在面向对象的设计中,模块之间我们一般会采取面向接口编程的方式,而在实际编程过程过程中,API的实现是封装在jar中,当我们想要换一种实现方法时,还要生成新的jar替换以前的实现类.而通过jdk的SPI机制就可以实现,首先不需要修改原来作为接口的jar的情况下,将原来实现的那个jar替换为另外一种实

  • Java中SPI的一些理解

    前言 最近在面试的时候被问到SPI了,没回答上来,主要也是自己的原因,把自己给带沟里去了,因为讲到了类加载器的双亲委派模型,后面就被问到了有哪些是破坏了双亲委派模型的场景,然后我就说到了SPI,JNDI,以及JDK9的模块化都破坏了双亲委派. 然后就被问,那你说说对Java中的SPI的理解吧.然后我就一脸懵逼了,之前只是知道它会破坏双亲委派,也知道是个怎么回事,但是并没有深入了解,那么这次我就好好的来总结一下这个知识吧. 什么是SPI SPI全称Service Provider Interfac

  • 在java中使用SPI创建可扩展的应用程序操作

    简介 什么是可扩展的应用程序呢?可扩展的意思是不需要修改原始代码,就可以扩展应用程序的功能.我们将应用程序做成插件或者模块. 这样可以在不修改原应用的基础上,对系统功能进行升级或者定制化. 本文将会向大家介绍如何通过java中的SPI机制实现这种可扩展的应用程序. SPI简介 SPI的全称是Java Service Provider Interface.是java提供的一种服务发现的机制. 通过遵循相应的规则编写应用程序之后,就可以使用ServiceLoader来加载相应的服务了. SPI的实现

  • Java插件扩展机制之SPI案例讲解

    目录 什么是SPI 与 接口类-实现类 提供的RPC 方式有什么区别? 假设我们需要实现RPC,是怎么做的? 那RPC究竟跟SPI什么关系? SPI的应用场景 怎么实现一个SPI? 中间件是怎么实现SPI的? Apollo-Client中的实现 JDBC中的实现 什么是SPI SPI ,全称为 Service Provider Interface,是一种服务发现机制.其为框架提供了一个对外可扩展的能力. 与 接口类-实现类 提供的RPC 方式有什么区别? 传统的接口类实现形式为如下 public

  • Java之字节码以及优势案例讲解

    一.Java编译器 Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器,.这台虚拟的机器在任何平台上都提供给编译程序一个共同的接口.编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码来将虚拟机代码转换为特定的系统机器码执行.在Java中,这种提供虚拟机理解的代码叫做字节码(即扩展名为 .class的文件),它不面向任何特定的处理器,仅仅只面向虚拟机. 1.利用记事本写一段Java代码 2.将记事本命名为 hello.java 3.cmd+r

  • Java之Jackson的基本使用案例讲解

    Jackson 是当前用的比较广泛的,用来序列化和反序列化 json 的 Java 的开源框架.Jackson 社 区相对比较活跃,更新速度也比较快, 从 Github 中的统计来看,Jackson 是最流行的 json 解析器之一 . Spring MVC 的默认 json 解析器便是 Jackson. Jackson 优点很多. Jackson 所依赖的 jar 包较少 ,简单易用.与其他 Java 的 json 的框架 Gson 等相比, Jackson 解析大的 json 文件速度比较快

  • Java之SpringBoot集成ActiveMQ消息中间件案例讲解

    ActiveMQ是Apache提供的开源组件,是基于JMS标准的实现组件.下面将利用SpringBoot整合ActiveMQ组件,实现队列消息的发送与接收. 第一步:引入依赖 第二步:修改application.yml文件,进行ActiveMQ的配置 第三步:定义消息消费监听类 第四步:定义消息生产者业务接口 第五步: 定义消息业务实现类 第六步:定义JMS消息发送配置类   第七步:测试发送消息 查看结果: 本文采用ActiveMQ实现了消息的发送与接收处理.每当有消息接收到时,都会自动执行M

  • Java使用elasticsearch基础API使用案例讲解

    1.依赖 我用的是 springboot 2.2.5.RELEASE 版本,这里只贴出主要依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> 2.配置+测试源码 这里根据elasticsearch服务端的地址进行

  • Java之IO流面试题案例讲解

    一.Java中IO流分为几种? 按照流的流向分,可以分为输入流和输出流: 按照操作单元分,可以分为字节流和字符流(字节流可以读写任何单位的数据,字符流只可以读写txt数据): 按照流的角色分,可以分为节点流和处理流: 二.IO中flush()和close()的区别 close()方法具备刷新功能,在关闭流之前就会先刷新缓冲区,将缓冲区的字节全部刷新到文件上,在关闭流.(close()方法包含一次flush()方法) flush()方法可以刷新,并且刷新之后可以继续写,而close()方法刷新之后

  • Java数据结构 递归之迷宫回溯案例讲解

    问题介绍: 用二维数组表示一个迷宫,设置迷宫起点和终点,输出迷宫中的一条通路 实现思路: 二维数组表示迷宫: 0表示路且未走过.1表示墙.2表示通路,3表示已经走过但走不通 设置寻路方法setWay,传入地图和坐标参数 默认方向策略:下.右.上.左 假定传入的店没有走过且可以走通,将其值置为2,然后向下寻路,也就是将坐标 (i + 1, j) 传入寻路方法中 进行递归寻路,向下移动后,再次按照方向策略进行寻路,即再向下寻路,直到遇到死路,即下右左均走不通(因为将走过的路置为2,故向上也走不通,即

  • Mybatis插件扩展及与Spring整合原理分析

    前言 前面几篇文章分析了Mybatis的核心原理,但模块较多,没有一一分析,更多的需要读者自己下来研究.不过Mybatis的插件扩展机制还是非常重要的,像PageHelper就是一个扩展插件,熟悉其扩展原理,才能更好的针对我们的业务作出更合适的扩展.另外,现在Mybatis都是和Spring/SpringBoot一起使用,那么Mybatis又是如何与它们进行整合的呢?一切答案尽在本文之中. 正文 插件扩展 1. Interceptor核心实现原理 熟悉Mybatis配置的都知道,在xml配置中我

  • Java之类加载机制案例讲解

    1.类加载 <1>.父子类执行的顺序 1.父类的静态变量和静态代码块(书写顺序) 2.子类的静态变量和静态代码块(书写顺序) 3.父类的实例代码块(书写顺序) 4.父类的成员变量和构造方法 5.子类的实例代码块 6.子类的成员变量和构造方法 <2>类加载的时机 如果类没有进行初始化,则需要先进行初始化,虚拟机规范则是严格规定有且只有5种情况必须先对类进行初始化(而加载,验证,准备要在这个之前开始) 1.创建类的实例(new的方式),访问某个类的静态变量,或者对该静态变量赋值,调用类

  • PHP反射机制案例讲解

    简介 就算是类成员定义为private也可以在外部访问,不用创建类的实例也可以访问类的成员和方法. PHP自5.0版本以后添加了反射机制,它提供了一套强大的反射API,允许你在PHP运行环境中,访问和使用类.方法.属性.参数和注释等,其功能十分强大,经常用于高扩展的PHP框架,自动加载插件,自动生成文档,甚至可以用来扩展PHP语言.由于它是PHP內建的oop扩展,为语言本身自带的特性,所以不需要额外添加扩展或者配置就可以使用.更多内容见官方文档. 反射类型 PHP反射API会基于类,方法,属性,

随机推荐