• Spring SPI


     SPI 服务供给接口(Service Provider Interface)。是Java 1.5新添加的一个内置标准,允许不同的开发者去实现某个特定的服务。

    1 SPI 介绍

    一个接口,可能会有许多个实现,我们在编写代码时希望能动态切换具体实现,例如:

    Interface interface = new Implement1(); // 创建一个具体的interface

    上面是硬编码方式,我们希望在不修改代码的情况下,更换interface的具体实现。当然我们可以使用配置文件方式来实现这个需求,伪代码如下:

    ResourceBundle rb = ResourceBundle.getBundle(“interface.properties”);

    String impName = rb.getString(“impName”);

    Interface interface = (Interface) Class.forName(impName).newInstance();

    SPI 的实现则类似于上面的方法。让系统找到具体的实现。

     1.1 SPI的使用

    图 示例代码的项目结构说明

    1)定义一个接口,在spi_example_interface项目中定义MakeMoney接口。

    1. public interface MakeMoney {
    2. void hardWord();
    3. }

    2) 在自定义项目中实现接口,在spi_example_implement项目编写TeacherMakeMoney和ProgrammerMakeMoney两个类并实现MakeMoney接口。

    1. public class ProgrammerMakeMoney implements MakeMoney {
    2. public ProgrammerMakeMoney() {
    3. System.out.println("程序员实例被创建了");
    4. }
    5. @Override
    6. public void hardWord() {
    7. System.out.println("敲代码");
    8. }
    9. }
    10. public class TeacherMakeMoney implements MakeMoney {
    11. public TeacherMakeMoney() {
    12. System.out.println("教师实例被创建了");
    13. }
    14. @Override
    15. public void hardWord() {
    16. System.out.println("教书");
    17. }
    18. }

     3)在spi_example_implement项目中,resources文件下新建META-INF/services 文件夹,并在该文件夹下面创建由接口完全限定名命名的文件,在文件中依次列出该接口实现类的完全限定名。

    图 接口实现类说明文件

    4)使用定义的接口,利用Java提供的ServiceLoader类发现这个接口的实现,并使用它们。

    1. public class SpiUse {
    2. public static void main(String[] args) {
    3. ServiceLoader<MakeMoney> makeMonies = ServiceLoader.load(MakeMoney.class);
    4. Iterator<MakeMoney> iterator = makeMonies.iterator();
    5. while (iterator.hasNext()) {
    6. MakeMoney imp = iterator.next(); // 实现被加载的系统
    7. imp.hardWord();
    8. }
    9. }
    10. }
    11. /*
    12. 运行结果:
    13. 程序员实例
    14. 敲代码
    15. 教师实例被创建了
    16. 教书
    17. */

    1.2 java.sql.Driver与SPI

    在Java中定义了接口java.sql.Driver,其并没有具体的实现,具体的实现都是由不同的厂商提供。下面将以mysql的驱动为例,来大致介绍Java如何管理JDBC服务。

    1)实现java.sql.Driver接口。

    图 mysql-connector-java jar包中Driver的定义

    2) 在META-INFA/services文件夹下编写以Driver接口全限定名命名的文档,来引导ServiceLoader发现mysql实现的Driver的接口。

    图 mysql jar包下的引导文件

    3)注册并管理JDBC服务。

    图 jdbc服务的调用过程

    我们在使用jdbc 服务时,第一步是获取对数据库的连接,即执行上图的DriverManager.getConnection(url)方法。

    图 DriverManager的getConnection()方法的部分代码块

    以下代码是模拟数据库厂商实现java.sql.Driver这个接口:

    定义SqlDriver接口,全限定名是 com.huangmingfu.SqlDriver:

    1. public abstract class SqlDriver {
    2. private static List<SqlDriver> driverList = new ArrayList<>();
    3. /**
    4. * 执行sql
    5. */
    6. public abstract void execute(String sql);
    7. public abstract Boolean connect(String url);
    8. public static void register(SqlDriver sqlDriver) {
    9. driverList.add(sqlDriver);
    10. }
    11. public static SqlDriver getConnect(String url) {
    12. for (SqlDriver driver : driverList)
    13. if (driver.connect(url)) return driver;
    14. return null;
    15. }
    16. }

    第三方项目中对SqlDriver接口的实现(mysql和oracle)

    1. public class MySqlDriver extends SqlDriver {
    2. public MySqlDriver() {
    3. System.out.println("MySqlDriver实例被创建");
    4. }
    5. static {
    6. System.out.println("MySqlDriver实例被创建被加载到虚拟机了,进行注册:SqlDriver.register");
    7. SqlDriver.register(new MySqlDriver());
    8. }
    9. @Override
    10. public void execute(String sql) {
    11. System.out.println("mysql数据驱动,执行sql:" + sql);
    12. }
    13. @Override
    14. public Boolean connect(String url) {
    15. return url.startsWith("mysql");
    16. }
    17. }
    18. public class OracleDriver extends SqlDriver {
    19. public OracleDriver() {
    20. System.out.println("OracleDriver 实例被创建");
    21. }
    22. static {
    23. System.out.println("OracleDriver实例被创建被加载到虚拟机了,进行注册:SqlDriver.register");
    24. SqlDriver.register(new OracleDriver());
    25. }
    26. @Override
    27. public void execute(String sql) {
    28. System.out.println("oracle数据驱动,执行sql:" + sql);
    29. }
    30. @Override
    31. public Boolean connect(String url) {
    32. return url.startsWith("oracle");
    33. }
    34. }

     在第三方项目的META-INF/com.huangmingfu.SqlDriver 引导文件中写入实现类的全限定名:

    1. com.custom.MySqlDriver
    2. com.custom.OracleDriver

    使用Driver的实现类,来获取数据库连接:

    1. public class UserDriver {
    2. private static SqlDriver sqlDriver;
    3. public static void main(String[] args) throws Exception{
    4. System.out.println("项目启动....");
    5. // classForName();
    6. spi();
    7. }
    8. /**
    9. * 反射形式
    10. */
    11. private static void classForName() throws Exception {
    12. System.out.println("尝试先通过class.forName的形式");
    13. sqlDriver = (SqlDriver)Class.forName("com.custom.MySqlDriver").newInstance();
    14. sqlDriver.execute("SELECT VERSION();");
    15. }
    16. /**
    17. * spi形式
    18. */
    19. private static void spi() {
    20. ServiceLoader<SqlDriver> serviceLoader = ServiceLoader.load(SqlDriver.class);
    21. Iterator<SqlDriver> iterator = serviceLoader.iterator();
    22. while (iterator.hasNext()) iterator.next(); //只是做加载动作
    23. SqlDriver driver = SqlDriver.getConnect("mysql://");
    24. if (driver != null) driver.execute("SELECT VERSION()");
    25. }
    26. }
    27. /*
    28. 运行结果
    29. 项目启动....
    30. MySqlDriver实例被创建被加载到虚拟机了,进行注册:SqlDriver.register
    31. MySqlDriver实例被创建
    32. MySqlDriver实例被创建
    33. OracleDriver实例被创建被加载到虚拟机了,进行注册:SqlDriver.register
    34. OracleDriver 实例被创建
    35. OracleDriver 实例被创建
    36. mysql数据驱动,执行sql:SELECT VERSION()
    37. */

    2 SPI 原理

    java实现SPI的是ServiceLoader类,其实现步骤一共有两步:1)根据接口的全限定名查找META-INF/services下的接口实现引导文件记录的实现类全限定名集合;2)通过Class.forName(全限定名).newInstance()方法来将这些实现类加载进jvm中。

    图 第一步ServiceLoader获取接口实现类的全限定名

    图 第二步 ServiceLoader创建实现类的实例

    3 SPI的优缺点及应用场景

    spi 能扩展服务,将接口与实现解耦。通过服务接口和服务提供者,实现了服务规范的制定和服务具体实现的分离。

    API

    在大多数情况下,都是实现方制定接口并完成对接口的实现。调用方仅仅依赖接口调用,且无权选择不同实现。API是直接被应用开发人员使用。

    SPI

    是调用方来制定接口规范,提供给外部来实现。调用方在调用时则选择自己需要的外部实现。SPI是被框架扩展人员使用。

    表 API与SPI的对比

    缺点:

    1)不能按需加载,需要遍历所有的实现并实例化,然后在循环中才能找到我们需要的实现。

    2)多个并发多线程使用ServiceLoader类的实例是不安全的。

    应用场景:

    有关组织和公司定义接口标准,第三方提供具体实现。例如JDBC。

    4 Spring Boot 中的spring.factories

    在Spring Boot项目中,怎么将pom.xml文件里添加的依赖中的bean注册到Spring Boot项目的容器中呢?

    在项目中,@ComponentScan注解只会扫描项目包内的bean并注册到Spring容器中,项目依赖包中的bean不会被扫描和注册。此时可以利用SPI来对这些依赖包中的bean进行加载注册。

    META-INF/spring.factories 文件类似于SPI中的接口实现类引导文件。有spring-core包中的SpringFactoriesLoader类充当着类似ServiceLoader的作用。

  • 相关阅读:
    基于食肉植物优化算法的线性规划问题求解matlab程序
    解决zsh远程启动后台进程后无法退出的问题
    使用Selenium的WebDriver进行长截图
    【算法中的Java】— 判断语句
    433MHz自发电无线控制器
    基础入门 - Spring Boot HelloWorld 第二节
    正则表达式.exec()
    gorm 中的事务运用
    RESTful API 设计指南——为什么要用(上)
    (附源码)springboot学生宿舍管理系统 毕业设计 161542
  • 原文地址:https://blog.csdn.net/qq_25308331/article/details/134453616