Java中的SPI机制与扫描class原理

文章目录

  • 前言
  • ClassLoader
  • JAVA SPI机制
  • Spring SPI机制
    • 示例
    • 原理
  • 如何加载jar包里的class

前言

Java的SPI机制与Spring中的SPI机制是如何实现的?

ClassLoader

这里涉及到了class Loader的机制,有些复杂,jdk中提供默认3个class Loader:

  • Bootstrap ClassLoader:加载jdk核心类库;加载%JAVA_HOME\lib%下的jar;
  • ExtClassLoader:加载jdk扩展类库;加载%JAVA_HOME\lib\ext%下的jar;
  • AppClassLoader:加载classpath下的class,以及关联到maven仓库里的jar;

AppClassLoaderExtClassLoader父类都是URLClassLoader,我们自定义也是继承URLClassLoader进行扩展的;

所以,当我们使用类加载器加载资源时,它会找上面这些路径,而AppClassLoader是找当前执行程序的classpath,也就是我们target/classes目录,如果有是maven引用了其他依赖包,那么也会将maven地址下的依赖包的路径加到AppClassLoaderURL里,如果是多模块的项目,还会把引用的其他模块下target/classes的目录也加进来。

image-20230804144522488

image-20230804144419885

JAVA SPI机制

Java中提供的SPI机制是通过读取META-INF/services/目录下的接口文件,从而加载到实现类。

其规则如下:

  1. 规定号开放api
  2. 实现提供方需要依赖开发接口完成实现,例如msyql
  3. 实现提供方,resource下提供META-INF/services/接口全名文件,内容为实现类

例如下面这个:

image-20230713104226856

重现建一个项目app用来测试

  1. 定义接口plugin-api打成jar

    /*** @author ALI* @since 2023/6/30*/
    public interface Plugin {Object run(Object data);
    }
  2. 定义实现,然后打成jar

    /*** @author ALI* @since 2023/6/30*/
    public class PluginImpl implements Plugin {@Overridepublic Object run(Object data) {Motest motest = new Motest();System.out.println(motest.getName());System.out.println(data);return null;}
    }/*** @author ALI* @since 2023/6/30*/
    public class Motest {private String name;public Motest() {name = "sss";}public String getName() {return name;}
    }

    这里我还定义了一个其他的类,用来测试再load class时是否会加载。

  3. 在新项目中加载jar中的资源,引入plugin-api

       /*** 使用jar的classLoader*/private static void load2() throws Exception{String jarPath = "E:/workspace/git/test-plugin/app/target/classes/plugin-impl-1.0-SNAPSHOT.jar";URLClassLoader jarUrlClassLoader = new URLClassLoader(new URL[]{new URL("file:" + jarPath)});// ServerLoader搜索ServiceLoader<Plugin> load = ServiceLoader.load(Plugin.class, jarUrlClassLoader);Iterator<Plugin> iterator = load.iterator();while (iterator.hasNext()) {// 实例化对象:这里会进行加载(Class.forName),然后反射实例化Plugin next = iterator.next();next.run("sdsdsdsds");}}
    

    这里使用ServiceLoader时传入了jarClassLoader,开篇已经解释过了:因为类加载器的原因,不会加载我们自定义的jar包,所以手动创建类加载器。

    image-20230713144008469

    结果已经很显而易见,已经成功加载了,这种方式的划,会加载jar包里实现了接口的所有实现类,这个方式使用也是很方便的。

  4. 使用URLClassLoader加载class

Spring SPI机制

在Spring中,它的SPI机制,和JAVA 中的类似,需要这样的条件:

  1. 定义接口模块包,用于开发给第三方实现;

  2. 第三方要有resources\META-INF\spring.factories文件,其内容是键值对方式,key为接口类,value就是我们的实现类;

而Spring执行就是获取到文件里的value,然后反射实例化。

示例

  1. 定义接口模块

image-20230804152730928

  1. 定义第三方实现组件,并配置spring.factoryies

    image-20230804152135550

  2. 项目中引入接口模块组件,和实现组件

    image-20230804152923183

    结果:

    image-20230804153102826

原理

loadFactories两个参数

Class factoryType:用于反射实例化;

ClassLoader classLoader:用于加载资源,所有这里可以直接使用URLClassLoader指定jar的类加载,如果不指定,就是它自己本身的类加载;

	public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {Assert.notNull(factoryType, "'factoryType' must not be null");ClassLoader classLoaderToUse = classLoader;if (classLoaderToUse == null) {// 如果为空,它用自己的加载器classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();}// 这里就是加载spring.factories文件里的value值// 找出所有的实现类的类路径List<String> factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse);if (logger.isTraceEnabled()) {logger.trace("Loaded [" + factoryType.getName() + "] names: " + factoryImplementationNames);}List<T> result = new ArrayList<>(factoryImplementationNames.size());// 遍历找出来的类,然后通过反射实例化for (String factoryImplementationName : factoryImplementationNames) {result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse));}// 排序AnnotationAwareOrderComparator.sort(result);return result;}

这里看一下

	public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {// 将接口类转化成类路径,如com.liry.pluginapi.PluginString factoryTypeName = factoryType.getName();// 先获取到spring.factories里的键值对(map),然后再getreturn loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {// 缓存;程序运行中需要多次获取MultiValueMap<String, String> result = cache.get(classLoader);if (result != null) {return result;}try {// 通过类加载获取所有资源地址urlEnumeration<URL> urls = (classLoader != null ?classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));result = new LinkedMultiValueMap<>();// 遍历while (urls.hasMoreElements()) {URL url = urls.nextElement();UrlResource resource = new UrlResource(url);// 通过PropertiesLoaderUtils工具获取spring.factories里的键值对Properties properties = PropertiesLoaderUtils.loadProperties(resource);for (Map.Entry<?, ?> entry : properties.entrySet()) {String factoryTypeName = ((String) entry.getKey()).trim();// 将value通过逗号分隔成数组,然后再全部添加到结果集中for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {result.add(factoryTypeName, factoryImplementationName.trim());}}}// 加入缓存cache.put(classLoader, result);return result;}catch (IOException ex) {throw new IllegalArgumentException("Unable to load factories from location [" +FACTORIES_RESOURCE_LOCATION + "]", ex);}}

注意:MultiValueMap这个map相同的key不会覆盖value,而是组成链表,如下,一个key可以有多个value,逗号分隔

	public void add(K key, @Nullable V value) {List<V> values = this.targetMap.computeIfAbsent(key, k -> new LinkedList<>());values.add(value);}

如何加载jar包里的class

假设需要获取一个jar包里的class该如何?

如下4个步骤即可:

    public static void main(String[] args) throws Exception {String packageName = "com.liry.springplugin";// 1. 转换为 com/liry/springpluginString packagePath = ClassUtils.convertClassNameToResourcePath(packageName);// 2. 通过类加载器加载jar包URL
//        ClassLoader classLoader = Test.class.getClassLoader();ClassLoader classLoader = new URLClassLoader(new URL[]{new URL("file:E:\\workspace\\git\\test-plugin\\spring-plugin\\target\\spring-plugin-1.0-SNAPSHOT.jar")});URL resources = classLoader.getResource(packagePath);// 3. 打开资源通道JarFile jarFile = null;URLConnection urlConnection = resources.openConnection();if (urlConnection instanceof java.net.JarURLConnection) {java.net.JarURLConnection jarURLConnection = (java.net.JarURLConnection) urlConnection;jarFile = jarURLConnection.getJarFile();}// 定义一个结果集List<String> resultClasses = new ArrayList<>();// 4. 遍历资源文件Enumeration<JarEntry> entries = jarFile.entries();while (entries.hasMoreElements()) {JarEntry entry = entries.nextElement();// 文件全路径String path = entry.getName();// 判断是否在指定包路径下,jar包里有多层目录、MF文件、class文件等多种文件信息if (path.startsWith(packagePath)) {// 使用spring的路径匹配器匹配class文件if (path.endsWith(".class")) {resultClasses.add(path);}}}resultClasses.forEach(System.out::println);}

image-20230803174544910

说明一下,加载jar包的问题;

上面给出了两种方式

第一种:使用类加载加载

ClassLoader classLoader = Test.class.getClassLoader();

第二种:使用URLClassLoader加载

ClassLoader classLoader = new URLClassLoader(new URL[]{new URL("file:E:\\workspace\\git\\test-plugin\\spring-plugin\\target\\spring-plugin-1.0-SNAPSHOT.jar")});

这两种方式不同之处在于,查找jar的路径,第一种方式因为我测试项目使用的maven,在pom.xml里引入了spring-plugin-1.0-SNAPSHOT的包,所以才能通过类加载器直接进行加载,这是因为使用maven,maven引用的依赖路径会被加入到AppClassLoader种,然后使用Test.class.getClassLoader()去加载class时,会委派给AppClassLoader进行加载,才会加载到。

所以,如果不是在maven种引入的包,使用第二种方式。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/22424.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Dockerfile构建MySQL镜像

创建工作目录 [rootlocalhost ~]# mkdir mysql [rootlocalhost ~]# cd mysql/ 编写Dockerfile文件 [rootlocalhost mysql]# vim Dockerfile FROM centos:7 MAINTAINER Crushlinux <crushlinux163.com> #安装mariadb数据库 RUN yum install -y mariadb mariadb-server mar…

java与javaw运行jar程序

运行jar程序 一、java.exe启动jar程序 (会显示console黑窗口) 1、一般用法&#xff1a; java -jar myJar.jar2、重命名进程名称启动&#xff1a; echo off copy "%JAVA_HOME%\bin\java.exe" "%JAVA_HOME%\bin\myProcess.exe" myProcess -jar myJar.jar e…

AP AUTOSAR在软件定义汽车生态系统中的角色

AP AUTOSAR在软件定义汽车生态系统中的角色 AP AUTOSAR是AUTOSAR(汽车开放系统架构)的最新版本之一,它是一种面向服务的软件平台,旨在满足未来汽车电子系统的需求,特别是高性能计算、高带宽通信、软件无线更新(OTA)等方面。 AP AUTOSAR在软件定义汽车生态系统中扮演着…

【练手】自定义注解+AOP

在SpringBoot中实现自定义注解&#xff1a;( 声明注解的作用级别以及保留域 ) Target({ElementType.METHOD,ElementType.PARAMETER}) //注解的作用级别 Retention(RetentionPolicy.RUNTIME) //注解的保留域 public interface Log {int value() default 99; }在…

数据结构----异或

数据结构----异或 一.何处用到了异或 1. 运算符 //判断是否相同 用到了异或&#xff0c;看异或结果如果是0就是相同&#xff0c;不是0就是不同//注意&#xff1a; 不能给小数用&#xff0c;小数没有相等的概念&#xff0c;所以小数判断是否相同都是进行相减判断2.找一堆数中…

docker 部署mysql 5.6集群

docker搭建mysql的集群&#xff08;一主双从&#xff09; 1.拉取镜像 docker pull mysql:5.6 2.启动master容器 docker run -it -d --name mysql_master -p 3306:3306 --ip 192.168.162.100 \ -v /data/mysql_master/mysql:/var/lib/mysql \ -v /data/mysql_master/conf.d…

【无标题】云原生在工业互联网的落地及好处!

什么是工业互联网&#xff1f; 工业互联网&#xff08;Industrial Internet&#xff09;是新一代信息通信技术与工业经济深度融合的新型基础设施、应用模式和工业生态&#xff0c;通过对人、机、物、系统等的全面连接&#xff0c;构建起覆盖全产业链、全价值链的全新制造和服务…

eclipse常用快捷键

Eclipse常用快捷键 补全代码的声明&#xff1a;alt /快速修复: ctrl 1批量导包&#xff1a;ctrl shift o使用单行注释&#xff1a;ctrl /使用多行注释&#xff1a; ctrl shift /取消多行注释&#xff1a;ctrl shift \复制指定行的代码&#xff1a;ctrl alt down 或…

Scrum是什么意思,Scrum敏捷项目管理工具有哪些?

一、什么是Scrum&#xff1f; Scrum是一种敏捷项目管理方法&#xff0c;旨在帮助团队高效地开展软件开发和项目管理工作。 Scrum强调迭代和增量开发&#xff0c;通过将项目分解为多个短期的开发周期&#xff08;称为Sprint&#xff09;&#xff0c;团队可以更好地应对需求变…

微信小程序 width 100% 加 margin 或 padding 溢出屏幕

微信小程序 width 100% 加 margin 或 padding 溢出屏幕 方案1&#xff1a;fill-available margin方案2&#xff1a;box-sizing padding 方案1&#xff1a;fill-available margin width: 100%;width: -moz-available;width: -webkit-fill-available;width: fill-available;ma…

C#设计模式之---工厂方法模式

工厂方法模式&#xff08;Factory Method&#xff09; 工厂方法模式&#xff08;Factory Method&#xff09;的出现解决简单工厂中的难以拓展的问题&#xff0c;解决了其一旦添加新的产品&#xff0c;则需要修改简单工厂方法&#xff0c;进而造成简单工厂的实现逻辑过于复杂。…

聊聊混合动力汽车和纯电骑车的优势和劣势

混合动力汽车和纯电动骑车是两种不同的交通工具&#xff0c;它们都有各自的优势和劣势。本文将分别探讨混合动力汽车和纯电动骑车的优势和劣势&#xff0c;并为文章提供三个备选的好听的标题。 混合动力汽车是一种结合了内燃机和电动机的汽车&#xff0c;它可以同时利用燃油和电…

使用PHP和Redis实现简单秒杀功能

安装Redis 首先&#xff0c;需要在服务器上安装Redis。如果使用Linux系统&#xff0c;可以使用命令行安装。如果使用Windows系统&#xff0c;可以下载并安装Redis二进制文件。 创建Redis连接 在PHP中&#xff0c;可以使用Redis扩展来连接Redis服务器。需要在PHP文件中包含Re…

大数据课程G1——Hbase的概述

文章作者邮箱&#xff1a;yugongshiyesina.cn 地址&#xff1a;广东惠州 ▲ 本章节目的 ⚪ 了解HIve的概念&#xff1b; ⚪ 了解HIve与数据库的区别&#xff1b; ⚪ 了解HIve的特点&#xff1b; 一、简介 1. 概述 1. HBase原本是由Yahoo!公司开发后来贡献给了…

自己实现 OpenAI 的 /v1/embeddings 接口

自己实现 OpenAI 的 /v1/embeddings 接口 0. 背景1. 修改 .env 文件2. 修改 get_embedding 方法 0. 背景 使用 OpenAI 的 API Key 是需要付费的&#xff0c;为了节省成本&#xff0c;自己尝试实现 OpenAI 的各种接口。 本文章主要是实现 /v1/embeddings 和 /v1/engines/{mode…

SQL注入 ❤ ~~~ 网络空间安全及计算机领域常见英语单词及短语——网络安全(二)

SQL注入 ❤ 学网安英语 大白话讲SQL注入SQL注入原理1. 用恶意拼接查询进行SQL注入攻击2. 利用注释执行非法命令进行SQL注入攻击3. 利用传入非法参数进行SQL注入攻击4. 添加额外条件进行SQL注入攻击 时间和布尔盲注时间盲注&#xff08;Time-Based Blind SQL Injection&#xf…

使用隧道HTTP时如何解决网站验证码的问题?

使用代理时&#xff0c;有时候会遇到网站验证码的问题。验证码是为了防止机器人访问或恶意行为而设置的一种验证机制。当使用代理时&#xff0c;由于请求的源IP地址被更改&#xff0c;可能会触发网站的验证码机制。以下是解决网站验证码问题的几种方法&#xff1a; 1. 使用高匿…

树和二叉树 --- 数据结构

目录 1.树的概念及结构 1.1树的概念 1.2树的表示 1.3树在实际生活中的运用 2.二叉树的概念及结构 2.1概念 2.2特殊的二叉树 2.3二叉树的性质 2.4二叉树的存储结构 1.树的概念及结构 1.1树的概念 树是一种非线性的数据结构&#xff0c;它是由n (n>0)个有限结点组成…

No primary or single unique constructor found for interface java.util.List

报错截图&#xff1a; 报错内容&#xff1a; 2023-08-04 15:46:32.884 ERROR 14260 --- [io-8080-exec-10] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing fa…

Android 版本 对应的 API版本

Android 14&#xff08;开发者预览版&#xff09; 如需详细了解平台变更&#xff0c;请参阅 Android 14 文档。 Android 13&#xff08;API 级别 33&#xff09; 如需详细了解平台变更&#xff0c;请参阅 Android 13 文档。 Android 12&#xff08;API 级别 31、32&#xf…