JDK SPI机制以及自定义SPI类加载问题

 更新时间:2022年11月19日 11:15:14   作者:两米以下皆凡人  
这篇文章主要介绍了JDK SPI机制以及自定义SPI类加载问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

概述

介绍SPI之前,我们先了解一下为什么要用SPI

JDBC相信已经不陌生了,JDBC 是一个标准。

不同的数据库厂商(如,mysql、oracle等)会根据这个标准,有它们自己的实现。

既然,JDBC 是一个标准,那么 JDBC 的接口,应该就已经存在于JDK 中了,以前我们在使用JDBC的时候,都是需要加载Driver驱动的,如:

Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql:///test";
Connection connection = = DriverManager.getConnection(url,"root","123456");

但是我们如果没有写的这行代码,也是可以让com.mysql.jdbc.Driver正确加载的,即:

String url = "jdbc:///test";
Connection connection = = DriverManager.getConnection(url,"root","123456");

那么这是为什么呢?要知道DriverManager类是由启动类加载器加载,而且根据全盘负责委托机制,每个类都有自己的类加载器,那么负责加载当前类的类加载器也会去加载当前类中引用的其他类,前提是引用的类没有被加载过

例如ClassA中有个变量 ClassB,那么加载ClassA的类加载器会去加载ClassB,如果找不到ClassB,则异常。

根据以上特性,那么JDK中的DriverManager启动类加载器会尝试去加载MySqljar包,但明显是找不到的,因为它根本不在JDK

那我们不妨看一下DriverManager的源码:

继续查看一下其中的 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;
    }
	// 1
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
            // Do nothing
            }
            return null;
        }
    });    println("DriverManager.initialize: jdbc.drivers = " + drivers);
    // 2
    if (drivers == null || drivers.equals("")) {
        return;
    }
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
        try {
            println("DriverManager.Initialize: loading " + aDriver);
            // 3
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}

分析其中两个地方:

1、这里使用了ServiceLoader机制来加载驱动,它是Java提供的一套 SPI(Service Provider Interface) 框架,用于实现服务提供方与服务使用方解耦

2、使用 jdbc.drivers 定义的驱动名加载驱动

3、ClassLoader.getSystemClassLoader() 就是应用程序类加载器

规则

SPI机制是JDK提供接口,第三方Jar包实现,接口由启动类加载器加载,实现类不在JDK中,需要反向委派,由线程上下文加载器加载。它约定:在 jar 包的 META-INF/services 包下,以接口全限定名为文件名,文件内容是实现类名称

这样便可以使用刚才loadInitialDrivers这个方法

ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
while(iter.hasNext()) {
    iter.next();
}

来得到具体的Driver实现类,那我们再追一下ServiceLoader是如何通过Driver.class接口来加载它具体的实现类的,现在进入 load() 方法:

public static <S> ServiceLoader<S> load(Class<S> service) {
    //获取到了线程上下文类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,那么这个方法中的load方法就会使用刚才拿到的线程上下文类加载器去加载目标实现类,不过这个方法比较深,真正加载的具体代码在 ServiceLoader 的内部类 LazyIteratornextService方法中:

自定义实现

注解

package com.phz.prpc.extension;import java.lang.annotation.*;/**
 * <p>
 * {@code SPI}注解,可运行其他第三方实现的抽象接口需使用此注解
 * </p>
 * </br>
 * <p>
 * {@code JDK}的{@code SPI}机制是{@code JDK}提供接口,第三方{@code jar}包实现,接口由启动类加载器加载,实现类不在{@code JDK}中,需要反向委派,由线程上下文加载器加载。
 * </p>
 * </br>
 * <p>
 * 它约定:在 {@code jar} 包的 {@code META-INF/services} 包下,以接口全限定名为文件名,文件内容是实现类名称
 * </p>
 * </br>
 * <p>
 * 那么我们完全可以参照它的思想取仿写一个
 * </p>
 *
 * @author PengHuanZhi
 * @date 2022年01月16日 17:50
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Spi {
}

基于SPI的伪类加载器

package com.phz.prpc.extension;import lombok.Data;
import lombok.extern.slf4j.Slf4j;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.Enumeration;import static java.nio.charset.StandardCharsets.UTF_8;/**
 * <p>
 * 自己实现一个扩展类加载器辅助类
 * ,区别于{@code JDK}的{@code SPI}机制,我们预定好在 {@code jar} 包的 {@code META-INF/extensions} 目录下方存放扩展类文件,文件内容就为第三方实现的全路径
 * </p>
 *
 * @author PengHuanZhi
 * @date 2022年01月16日 17:56
 */
@Slf4j
@Data
public final class ExtensionLoader<T> {
    /**
     * 约定第三方实现配置文件目录
     **/
    private static final String SERVICE_DIRECTORY = "META-INF/extensions/";    /**
     * 接口的类型,用于获取此接口下的第三方实现
     **/
    private final Class<?> type;    /**
     * 通过接口的{@link Class}对象获取其第三方实现类的加载器
     *
     * @param type 接口的类型
     * @return ExtensionLoader<T> 返回一个指定接口类型的类加载器辅助类
     **/
    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
        if (type == null) {
            throw new IllegalArgumentException("Spi需要知道你想要找到哪个功能的第三方实现!");
        }
        if (!type.isInterface()) {
            throw new IllegalArgumentException("只支持寻找接口类型的第三方实现!");
        }
        if (type.getAnnotation(Spi.class) == null) {
            throw new IllegalArgumentException("目标接口必须被@Spi注解标注!");
        }
        return new ExtensionLoader<>(type);
    }    /**
     * 获取这个接口指定名称的第三方实现对象
     *
     * @return T 返回目标实现
     **/
    public T getExtension() {
        // 加载到一个第三方实现
        Class<T> clazz = loadExtensionFile();
        if (clazz == null) {
            return null;
        }
        try {
            return clazz.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            throw new RuntimeException("实例化失败 : " + clazz);
        }
    }    /**
     * 加载约定好的目录下方的名称为接口全路径的扩展文件
     *
     * @return Class<T> 返回目标第三方实现的{@link Class}对象
     **/
    private Class<T> loadExtensionFile() {
        //想要获取谁的实现类
        String fileName = ExtensionLoader.SERVICE_DIRECTORY + type.getName();
        try {
            Enumeration<URL> urls;
            ClassLoader classLoader = ExtensionLoader.class.getClassLoader();
            urls = classLoader.getResources(fileName);
            if (urls != null) {
                URL resourceUrl = urls.nextElement();
                return loadResource(classLoader, resourceUrl);
            }
            return null;
        } catch (IOException e) {
            log.error(e.getMessage());
            return null;
        }
    }    /**
     * 读取扩展文件的内容,找到第三方实现的全路径,并获得其{@link Class}对象
     *
     * @param classLoader 扩展类加载器辅助类的类加载器
     * @param resourceUrl 文件在资源{@code URL}
     * @return Class<T> 返回目标{@link Class}对象
     **/
    @SuppressWarnings("unchecked")
    private Class<T> loadResource(ClassLoader classLoader, URL resourceUrl) {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceUrl.openStream(), UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                // 可能是注释
                final int ci = line.indexOf('#');
                //如果是第一个位置,则这一行都可以不用解析了
                if (ci == 0) {
                    continue;
                } else if (ci > 0) {
                    //如果非第一个位置,需要将注释前面的内容取出来,也就是将注释后面的内容截取
                    line = line.substring(0, ci);
                }
                return (Class<T>) classLoader.loadClass(line.trim());
            }
        } catch (IOException | ClassNotFoundException e) {
            log.error(e.getMessage());
            return null;
        }
        return null;
    }
}

测试

参考如下方式:

代码中体现(因为自定义的SPI机制用于笔者自己的项目下方,所以读者可以仅关注代码中的11行即可):

/**
 * 使用负载均衡算法从服务集合中选取一个服务
 *
 * @param serviceInstances 服务集合
 * @return InetSocketAddress 选取的服务
 **/
public InetSocketAddress doChoice(List<InetSocketAddress> serviceInstances) {
    String loadBalanceAlgorithm = prpcProperties.getLoadBalanceAlgorithm();
    LoadBalance loadBalance;
    try {
        loadBalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension();
        if (loadBalance == null) {
            loadBalance = LoadBalanceAlgorithm.valueOf(loadBalanceAlgorithm);
        }
    } catch (IllegalArgumentException e) {
        log.error("未知的负载均衡算法:{},异常信息为:{}", loadBalanceAlgorithm, e.getMessage());
        throw new PrpcException(ErrorMsg.UNKNOWN_LOAD_BALANCE_ALGORITHM);
    }
    return loadBalance.doChoice(serviceInstances);
}

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • java原生序列化和Kryo序列化性能实例对比分析

    java原生序列化和Kryo序列化性能实例对比分析

    这篇文章主要介绍了java原生序列化和Kryo序列化性能实例对比分析,涉及Java和kryo序列化和反序列化相关实例,小编觉得很不错,这里分享给大家,希望给大家一个参考。
    2017-10-10
  • 通过Spring Security魔幻山谷讲解获取认证机制核心原理

    通过Spring Security魔幻山谷讲解获取认证机制核心原理

    这篇文章主要介绍了通过Spring Security魔幻山谷讲解获取认证机制核心原理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-04-04
  • Java使用OTP动态口令(每分钟变一次)进行登录认证

    Java使用OTP动态口令(每分钟变一次)进行登录认证

    这篇文章主要介绍了Java使用OTP动态口令(每分钟变一次)进行登录认证,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-09-09
  • Pulsar源码彻底解决重复消费问题

    Pulsar源码彻底解决重复消费问题

    这篇文章主要为大家介绍了Pulsar源码彻底解决重复消费问题,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-05-05
  • Spring Boot(二)之web综合开发

    Spring Boot(二)之web综合开发

    本篇文章为大家介绍spring boot的其它特性(有些未必是spring boot体系桟的功能,但是是spring特别推荐的一些开源技术本文也会介绍),对了这里只是一个大概的介绍,特别详细的使用我们会在其它的文章中来展开说明
    2017-05-05
  • Java引用传递实现方式以及与值传递的区别

    Java引用传递实现方式以及与值传递的区别

    这篇文章主要给大家介绍了关于Java引用传递实现方式以及与值传递的区别的相关资料,引用传递指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数,需要的朋友可以参考下
    2023-09-09
  • java-制表符\t的使用说明

    java-制表符\t的使用说明

    这篇文章主要介绍了java-制表符\t的使用说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-02-02
  • java 后端生成pdf模板合并单元格表格的案例

    java 后端生成pdf模板合并单元格表格的案例

    这篇文章主要介绍了java 后端生成pdf模板合并单元格表格的案例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-01-01
  • 关于文件合并与修改md5值的问题

    关于文件合并与修改md5值的问题

    这篇文章主要介绍了关于文件合并与修改md5值的问题,使用本博客的方法,不仅仅可以修改md5值,还可以达到隐藏文件的目的,需要的朋友可以参考下
    2023-04-04
  • 一文解决System.in关闭后无法再继续使用流的问题

    一文解决System.in关闭后无法再继续使用流的问题

    这篇文章主要给大家介绍如何解决System.in关闭后无法再继续使用流的问题,文中有详细的解决方法和代码示例,具有一定的参考价值,需要的朋友可以参考下
    2023-07-07

最新评论