Java SPI机制源码

2024-09-05 02:20
文章标签 java 源码 机制 spi

本文主要是介绍Java SPI机制源码,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • SPI简介
    • 使用案例
    • SPI的应用
    • SPI机制源码
    • SPI与类加载器
    • 双亲委派机制

SPI简介

Java的SPI(Service Provider Interface)机制允许第三方为应用程序提供插件式的扩展,而不需要修改应用程序本身的代码,从而实现了解耦。Java标准库本身就提供了SPI机制,通常是通过在META-INF/services目录下放置文件来实现的。

在这里插入图片描述
SPI机制的核心组件包括:

  • 服务接口:这是一个Java接口,定义了服务提供者需要实现的方法,应用程序将使用这个接口与具体的服务实现进行交互。

  • 服务实现:这是实现了服务接口的具体类,第三方可以为服务接口提供多个实现。

  • 服务提供者配置文件:这是一个位于META-INF/services目录下的文件,文件名与服务接口的全限定名相同,该文件包含了服务实现类的全限定名,每行一个接口的具体实现类,在运行时就可以加载这些实现类。

  • ServiceLoader:这是Java标准库中的一个类,用于加载服务实现,应用程序可以使用ServiceLoader来获取服务接口的所有具体实现类。

SPI的工作流程如下:

  1. 定义服务接口。

  2. 实现服务接口,创建具体的服务实现类。

  3. 在META-INF/services目录下创建服务提供者配置文件,列出所有服务实现类的全限定名。

  4. 使用ServiceLoader加载服务具体实现类,并根据需要使用它们。

总结就是说SPI机制使得应用程序可以在运行时动态地选择和加载服务实现,从而提高了应用程序的可扩展性和灵活性。

使用案例

首先定义一个服务的接口

public interface Service {void execute();
}

接着创建两个两个服务实现类去实现接口,并重写接口中的方法

public class Implementation1 implements Service {@Overridepublic void execute() {System.out.println("服务实现类1");}
}// Implementation2.java
public class Implementation2 implements Service {@Overridepublic void execute() {System.out.println("服务实现类2");}
}

然后在META-INF/services目录下创建一个名为com.xydp.SPI.Service(全限定名要与接口的名称对应)的文件,用于存储服务实现类的全限定名。文件内容如下:
在这里插入图片描述

com.xydp.SPI.Implementation1
com.xydp.SPI.Implementation2

编写对应的测试类进行验证

public class SPITest {public static void main(String[] args) {ServiceLoader<Service> loader = ServiceLoader.load(Service.class);Iterator<Service> iterator = loader.iterator();while(iterator.hasNext()){System.out.println(iterator.next().getClass());}}
}

输出结果

class com.xydp.SPI.Implementation1
class com.xydp.SPI.Implementation2

SPI的应用

SPI 机制被用于加载和注册 JDBC 驱动程序,JDBC的使用方法如下
首先,导入依赖

 <dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.18</version>
</dependency>
public class JDBCTest {@SneakyThrowspublic static void main(String[] args) {String url = "jdbc:mysql://localhost:3306/hmdp?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8";String username = "root";String password = "1445413748";// 1. 加载JDBC驱动(Driver的静态代码块里面会注册JDBC驱动)//	实际上这行代码是可以省略的Class.forName("com.mysql.cj.jdbc.Driver");// 2. 建立数据库连接Connection connection = DriverManager.getConnection(url, username, password);// 3. 创建Statement对象Statement statement = connection.createStatement();// 4. 执行SQL查询String sql = "SELECT * FROM test";ResultSet resultSet = statement.executeQuery(sql);// 5. 处理查询结果while (resultSet.next()) {int id = resultSet.getInt("id");String name = resultSet.getString("value");System.out.println("ID: " + id + ", value: " + name);}//6. 关闭资源connection.close();}}

运行结果
在这里插入图片描述

可以知道JDBC在加载驱动后,节省了注册驱动这一步骤,这是因为在Driver类的静态代码块中已经注册了。

static {try {//注册驱动DriverManager.registerDriver(new Driver());} catch (SQLException var1) {throw new RuntimeException("Can't register driver!");}
}

除此之外,上面代码Class.forName(“com.mysql.cj.jdbc.Driver”)与SPI机制存在关联,会显示去加载Driver类,实际上这行代码是可以省略的,不写的话,下面的DriverManager的静态代码块就会通过ServiceLoader去加载配置文件下的Driver类。
DriverManager类的静态代码块

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

静态代码块就会执行loadInitialDrivers()方法,

private static void loadInitialDrivers() {String drivers;try {drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {public String run() {return System.getProperty("jdbc.drivers");}});} catch (Exception ex) {drivers = null;}AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {//通过SPI机制加载Driver类ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator();//获取对应的实现类try{while(driversIterator.hasNext()) {driversIterator.next();}} catch(Throwable t) {}return null;}});}

在不显示加载Driver类的情况下,ServiceLoader 会扫描类路径中的 META-INF/services 目录,查找 java.sql.Driver的配置文件,该配置文件中存在com.mysql.cj.jdbc.Driver这一行字符串,找到之后ServiceLoader通过反射的方式去加载Driver类。
在这里插入图片描述
对此我们可以进行验证JDBC的SPI机制,测试类如下

    public static void main(String[] args) {ServiceLoader<Driver> serviceLoader = ServiceLoader.load(Driver.class);Iterator<Driver> iterator = serviceLoader.iterator();while(iterator.hasNext()){ ;Driver driver = (Driver) iterator.next();System.out.println("驱动类的包:"+driver.getClass().getPackage()+"=======加载驱动类"+driver.getClass().getName());}}

运行结果
在这里插入图片描述由此可以得出结论JDBC确实是通过SPI机制去加载com.mysql.cj.jdbc.Driver类。

SPI机制源码

SPI主要是通过 ServiceLoader 类去解析配置文件,然后通过反射的方式创建对应的接口实现类。
ServiceLoader的主要成员

public final class ServiceLoader<S> implements Iterable<S>  
{  // 加载的默认配置文件目录private static final String PREFIX = "META-INF/services/";  // 需要被加载的 SPI 服务实现类private final Class<S> service;  // 该类加载器用于加载服务private final ClassLoader loader;  // 访问控制上下文,用于安全控制private final AccessControlContext acc;  // 按照实例化的顺序缓存服务实例private LinkedHashMap<String,S> providers = new LinkedHashMap<>();  // 懒查询迭代器private LazyIterator lookupIterator;}
  1. 调用ServiceLoader.load 方法时,会根据接口和类加载器创建懒加载迭代器。
ServiceLoader<Service> loader = ServiceLoader.load(Service.class);
//源码如下
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader)  
{  return new ServiceLoader<>(service, loader);  
}// 构造方法重新加载SPI服务
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;// 重新加载SPI的服务reload();  
}public void reload() {// 清空服务提供者的缓存列表providers.clear();// 根据接口类型和类加载器创建懒加载迭代器,达到懒加载的目的,防止资源被浪费lookupIterator = new LazyIterator(service, loader);
}private LazyIterator(Class<S> service, ClassLoader loader) {this.service = service;this.loader = loader;
}
  1. 获取ServiceLoader 的 iterator

ServiceLoader 类的主要成员包含有LinkedHashMap实现的 providers,providers 用于用于缓存被成功加载的服务实例,key 是接口实现类的全限定名,value 是对应实现类的实例对象。

Iterator<Service> iterator = loader.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();}};}

3. 迭代器判断是否有数据

iterator.hasNext()
//源码如下
//判断是否还有数据
public boolean hasNext() {// 一开始缓存为空//(在调用next方法之后才会将服务对象实例放入该缓存,之后调用hasNext就会直接返回true)if (knownProviders.hasNext())return true;//一开始代码会执行到这return lookupIterator.hasNext();
}public boolean hasNext() {//acc访问控制默认为null,代码逻辑执行到这if (acc == null) {return hasNextService();} else {PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {public Boolean run() { return hasNextService(); }};return AccessController.doPrivileged(action, acc);}
}//判断是否还有服务
private boolean hasNextService() {// 判断是否还有服务实现类,有则返回trueif (nextName != null) {return true;}// 配置刚开始为空if (configs == null) {try {//配置文件目录拼接接口名称 META-INF/services + 接口名称// META-INF/services/com.xydp.SPI.ServiceString fullName = PREFIX + service.getName();//根据服务实现类的绝对路径加载配置if (loader == null)configs = ClassLoader.getSystemResources(fullName);elseconfigs = loader.getResources(fullName);} catch (IOException x) {fail(service, "Error locating configuration files", x);}}// 判断集合是否为空或者已经遍历完毕while ((pending == null) || !pending.hasNext()) {if (!configs.hasMoreElements()) {return false;}// 从配置文件解析以下的服务实现类//class com.xydp.SPI.Implementation1//class com.xydp.SPI.Implementation2pending = parse(service, configs.nextElement());}// 遍历集合的服务实现类信息//第一次返回class com.xydp.SPI.Implementation1nextName = pending.next();return true;
}
  1. 获取服务实现类的信息
System.out.println(iterator.next().getClass());
//源码如下
public S next() {// 刚开始缓存为空,所以为false1不执行if (knownProviders.hasNext())return knownProviders.next().getValue();//第一次执行到这return 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);}
}
//通过反射获取服务实例
private S nextService() {//此时 cn = com.xydp.SPI.Implementation1String cn = nextName;nextName = null;Class<?> c = null;// 使用反射,通过类的全限定名和类加载器得到类c = Class.forName(cn, false, loader);// 创建类的实例对象 S p = service.cast(c.newInstance());// 将实例对象放入缓存// key:com.xydp.SPI.Implementation1// value:Implementation1@28d25987providers.put(cn, p);// 返回实例对象return p;
}

SPI弊端:从源码可以知道SPI不能按需加载,需要通过 Iterator 形式遍历所有的服务实现类,无法根据参数名称来获取具体的服务实现类。

SPI与类加载器

在加载 SPI 服务时,需要指定类加载器 ClassLoader,否则无法找到具体的服务实现类,这是受限于双亲委派机制,该机制规定子类加载器可以使用父类已经加载的类,但是父类加载器无法使用子类已经加载的类。
(1) SPI 接口属于 Java 的核心库,是由顶层父类启动类加载器 BootstrapClassLoader所加载的;
(2 )SPI 的具体实现类是由系统类加载器AppClassLoader 加载的,顶层的父类启动类加载器 BootstrapClassLoader 是无法找到具体实现类的,所以需要指定类加载器 ClassLoader来加载。
主要有以下四种类加载器:

1 启动类加载器(Bootstrap ClassLoader):启动类加载器是最顶层的类加载器,它负责加载Java核心API和核心类库。启动类加载器是由JVM实现的,不是由Java类实现的。用来加载java核心类库,无法被java程序直接引用。

2 扩展类加载器(extensions class loader):扩展类加载器是启动类加载器的子类加载器,它用来加载 Java 的扩展库。Java 虚拟机的实现会提供 一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。

3 系统类加载器(system class loader):系统类加载器是扩展类加载器的子类加载器,加载路径主要包括应用程序的CLASSPATH环境变量指定的路径。可以通过ClassLoader.getSystemClassLoader()来获取它。

4 用户自定义类加载器,通过继承 java.lang.ClassLoader类重写findClass()方法的方式实现。

双亲委派机制

1.什么是双亲委派机制?

(1)当加载一个类时,当前类加载器先判断此类是否已经被加载,如果类已经被加载则返回;

(2)如果类没有被加载,则先委托父类加载(父类加载时会判断该类有没有被自己加载过),如果父类加载过则返回;如果没被加载过则继续向上委托;

(3)如果一直委托都无法加载,当前类加载器才会尝试自己加载。

2.双亲委派机制作用/优点

双亲委派机制是Java类加载器中的一个重要机制。它的主要作用有以下几点:

  1. 避免类的重复加载:Java中的类是由类加载器加载的,如果没有双亲委派机制,那么可能会出现多个类加载器加载同一个类的情况,造成资源的浪费。

  2. 保证Java核心API的类型安全:双亲委派机制保证了所有的Java应用都至少会使用java.* 开头的类库,而这些由系统类加载器所加载的类库在程序中有着至关重要的地位,比如java.lang.Object类,没有哪个类可以不使用Object类,所以为了防止用户自定义类篡改这些核心类。

    例如:实现了自定义的String时, 当应用程序通过系统类记载器加载核心类String时,它会首先委托给拓展类加载器,再委托给启动类加载器,启动类加载器就会加载核心String类,由于启动类加载器已经加载了正确的核心String类,所以应用程序不会加载到被篡改的String类。

3.为什么要打破双亲委派机制?

打破双亲委派机制的原因主要有以下几点:

  1. 灵活性:双亲委派机制虽然保证了类的唯一性和安全性,但也限制了Java类加载器的灵活性。有时候,我们需要自定义类加载器来实现一些特殊的功能,比如实现代码的热部署、动态加载第三方插件等。在这些场景下,打破双亲委派机制可以让我们更加灵活地控制类的加载过程。
  2. 隔离性:在某些场景下,**我们可能需要在同一个JVM中运行多个相互隔离的应用。这时候,我们可以使用自定义类加载器来实现类的隔离,从而避免类的冲突和安全问题。**打破双亲委派机制可以让我们更加灵活地控制类的加载过程,从而实现应用的隔离。
  3. 增加了类加载时间:在类加载的过程中,需要不断地查询并委托父类加载器,这意味着类加载所需要的时间可能会增加。在类数量庞大或类加载器层次比较深的情况下,这种时间延迟可能会变得更加明显。

4.如何打破双亲委派机制?

继承java.lang.ClassLoader类重写loadClass()方法来打破双亲委派机制,然后直接从自己的类路径中加载类,而不需要委托给父类加载器进行加载。

5.Tomcat是如何打破双亲委派机制的?

Tomcat是一个Java Web应用服务器,它可以运行多个Web应用。为了实现Web应用的隔离和独立部署,Tomcat使用了自定义类加载器来打破双亲委派机制。

在Tomcat中,每个Web应用都有一个对应的WebappClassLoader,它继承了java.lang.ClassLoader类。WebappClassLoader的主要作用是加载Web应用的类和资源。为了实现类的隔离,WebappClassLoader重写了loadClass方法,而不是findClass方法。在loadClass方法中,WebappClassLoader首先尝试从自己的类路径中加载类,如果找不到,再委托给父类加载器进行加载。这样,每个Web应用都可以使用自己的类加载器来加载类,从而实现了类的隔离

6.如何自定义类加载器?

继承java.lang.ClassLoader类重写findClass()方法实现自定义类加载器。

这篇关于Java SPI机制源码的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1137698

相关文章

Java设计模式---迭代器模式(Iterator)解读

《Java设计模式---迭代器模式(Iterator)解读》:本文主要介绍Java设计模式---迭代器模式(Iterator),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,... 目录1、迭代器(Iterator)1.1、结构1.2、常用方法1.3、本质1、解耦集合与遍历逻辑2、统一

Java内存分配与JVM参数详解(推荐)

《Java内存分配与JVM参数详解(推荐)》本文详解JVM内存结构与参数调整,涵盖堆分代、元空间、GC选择及优化策略,帮助开发者提升性能、避免内存泄漏,本文给大家介绍Java内存分配与JVM参数详解,... 目录引言JVM内存结构JVM参数概述堆内存分配年轻代与老年代调整堆内存大小调整年轻代与老年代比例元空

深度解析Java DTO(最新推荐)

《深度解析JavaDTO(最新推荐)》DTO(DataTransferObject)是一种用于在不同层(如Controller层、Service层)之间传输数据的对象设计模式,其核心目的是封装数据,... 目录一、什么是DTO?DTO的核心特点:二、为什么需要DTO?(对比Entity)三、实际应用场景解析

Java 线程安全与 volatile与单例模式问题及解决方案

《Java线程安全与volatile与单例模式问题及解决方案》文章主要讲解线程安全问题的五个成因(调度随机、变量修改、非原子操作、内存可见性、指令重排序)及解决方案,强调使用volatile关键字... 目录什么是线程安全线程安全问题的产生与解决方案线程的调度是随机的多个线程对同一个变量进行修改线程的修改操

从原理到实战深入理解Java 断言assert

《从原理到实战深入理解Java断言assert》本文深入解析Java断言机制,涵盖语法、工作原理、启用方式及与异常的区别,推荐用于开发阶段的条件检查与状态验证,并强调生产环境应使用参数验证工具类替代... 目录深入理解 Java 断言(assert):从原理到实战引言:为什么需要断言?一、断言基础1.1 语

深度解析Java项目中包和包之间的联系

《深度解析Java项目中包和包之间的联系》文章浏览阅读850次,点赞13次,收藏8次。本文详细介绍了Java分层架构中的几个关键包:DTO、Controller、Service和Mapper。_jav... 目录前言一、各大包1.DTO1.1、DTO的核心用途1.2. DTO与实体类(Entity)的区别1

Java中的雪花算法Snowflake解析与实践技巧

《Java中的雪花算法Snowflake解析与实践技巧》本文解析了雪花算法的原理、Java实现及生产实践,涵盖ID结构、位运算技巧、时钟回拨处理、WorkerId分配等关键点,并探讨了百度UidGen... 目录一、雪花算法核心原理1.1 算法起源1.2 ID结构详解1.3 核心特性二、Java实现解析2.

MySQL中的锁机制详解之全局锁,表级锁,行级锁

《MySQL中的锁机制详解之全局锁,表级锁,行级锁》MySQL锁机制通过全局、表级、行级锁控制并发,保障数据一致性与隔离性,全局锁适用于全库备份,表级锁适合读多写少场景,行级锁(InnoDB)实现高并... 目录一、锁机制基础:从并发问题到锁分类1.1 并发访问的三大问题1.2 锁的核心作用1.3 锁粒度分

SpringBoot整合liteflow的详细过程

《SpringBoot整合liteflow的详细过程》:本文主要介绍SpringBoot整合liteflow的详细过程,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋...  liteflow 是什么? 能做什么?总之一句话:能帮你规范写代码逻辑 ,编排并解耦业务逻辑,代码

JavaSE正则表达式用法总结大全

《JavaSE正则表达式用法总结大全》正则表达式就是由一些特定的字符组成,代表的是一个规则,:本文主要介绍JavaSE正则表达式用法的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录常用的正则表达式匹配符正则表China编程达式常用的类Pattern类Matcher类PatternSynta