Java SPI 一 之SPI(Service Provider Interface)进阶 AutoService
一、SPI(Service Provider Interface)
1.1 介绍
SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制(为某个接口寻找服务实现的机制),可以用来启用框架扩展和替换组件,其核心思想就是 解耦。
模块之间基于接口编程,模块之间不对实现类进行硬编码,当代码里涉及具体的实现类,就违反了可拔插的原则,为了实现在模块装配的时候能不在程序里动态指明,就需要spi了。
这里我们要跟API区分开来,简单介绍一下API
API(Application Programming Interface)是一种应用程序编程接口,它定义了一组用于与特定软件组件或服务进行交互的函数、方法和数据结构。
其目的主要用于提供一种与特定软件组件或服务进行交互的抽象层。
比如我们常见的系统API,接入的各种三方API,这些API的特点是实现方式以及做好了,开发者调用这些API来做一些有预期的事情。
1.2 使用场景
举个简单的例子,例如芯片公司定义了一个规范,需要第三方厂商去实现,那么对于芯片公司方来说,只需要集成对应厂商的插件,就可以完成对应规范的实现机制。 形成一种插拔式的扩展手段。
如JDBC、日志框架等都用到。
Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要(通常由服务提供者(如库或框架的开发者)实现,以提供特定功能的多种实现),我们看个图
1.3 原理
将接口的实现类放在配置文件中,在程序运行过程中读取配置文件,通过反射加载实现类。
具体流程:
–> 读取META-INF/services/下的配置文件
–> 获得所有能被实例化的类的名称
–> 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化
–> 把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型),然后返回实例对象。
JDK中查找服务的实现的工具类是:java.util.ServiceLoader。JDK标准的SPI会一次性加载实例化扩展点的所有实现。
1.4 使用demo
我们举例,先看下完整的项目目录
我们现在需要进行股票交易,有多个券商可用。
1、先定义好接口,新建module-spi,
package com.test;public interface ITrade {void trade();
}
2、两个接口的实现
新建module-effecta module-effectb,表示不同的实现方。
这两个module分别要引用 接口的module
<dependencies><dependency><groupId>org.example</groupId><artifactId>module-spi</artifactId><version>1.0-SNAPSHOT</version><scope>compile</scope></dependency></dependencies>
添加实现
package com.effectA;import com.test.ITrade;public class TradeEffectA implements ITrade {@Overridepublic void trade() {System.out.println("券商 A");}
}
最后进行注册
java目录下 增加 resources/META-INF/services/ 目录,在该目录下创建文件 ,如下图所示:
module-effectb重复操作
再写一个测试方法,新建一个module-main,充当调用者,首先添加引用
<dependencies><dependency><groupId>org.example</groupId><artifactId>module-spi</artifactId><version>1.0-SNAPSHOT</version><scope>compile</scope></dependency><dependency><groupId>org.example</groupId><artifactId>module-effectA</artifactId><version>1.0-SNAPSHOT</version><scope>compile</scope></dependency><dependency><groupId>org.example</groupId><artifactId>module-effectB</artifactId><version>1.0-SNAPSHOT</version><scope>compile</scope></dependency></dependencies>
package com.test;import java.util.ServiceLoader;public class Test {public static void main(String[] args) {ServiceLoader<ITrade> loader = ServiceLoader.load(ITrade.class);for (ITrade itrade: loader) {itrade.trade();}}
}
那为什么配置文件为什么要放在META-INF/services下面?
可以打开ServiceLoader的代码,里面定义了文件的PREFIX如下:
private static final String PREFIX = “META-INF/services/”
我们看下源码
public final class ServiceLoader<S> implements Iterable<S>{//路径前缀(就是我们放置配置文件的目录)
private static final String PREFIX = "META-INF/services/";// 代表被加载的类或者接口private final Class<S> service;// 用于定位,加载和实例化providers的类加载器private final ClassLoader loader;// 创建ServiceLoader时采用的访问控制上下文private final AccessControlContext acc;// 缓存providers,按实例化的顺序排列private LinkedHashMap<String,S> providers = new LinkedHashMap<>();// 懒查找迭代器private LazyIterator lookupIterator;......
}
在这个例子中,每次都要手动去新建META-INF/services/的文件,是不是很麻烦,我们可以用Autoservice来简化代码,先上demo
新建module-effectc-autoservice,表示不同使用autoservice自动写入配置的的实现方。
引用 接口的module-spi 及 autoservice
<dependencies><dependency><groupId>org.example</groupId><artifactId>module-spi</artifactId><version>1.0-SNAPSHOT</version><scope>compile</scope></dependency><dependency><groupId>com.google.auto.service</groupId><artifactId>auto-service</artifactId><version>1.0-rc4</version><scope>compile</scope></dependency></dependencies>
添加实现
@AutoService(ITrade.class)
@SupportedAnnotationTypes({"com.test.ITrade"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class TredeEffectC implements ITrade {@Overridepublic void trade() {System.out.println("券商 C");}
}
到这里就结束了,是不是要简化了很多。这个机制同样适用Android,如安卓组件化,demo比较简单,就不贴代码了。
二、Autoservice
AutoService是Google开发一个自动生成SPI清单文件的框架。
自动往 resources/META-INF/services/ 写入文件。
https://github.com/google/auto
不用它也行,如果不使用它就需要手动去创建这个文件、手动往这个文件里添加服务(接口实现)。
其原理步骤:
- 遍历找到所有带有AutoService注解的类
- 验证AutoService注解的值是否正确
- 遍历所有的下沉接口
- 在META-INF/services/路径下创建文件,文件名以类的接口类全路径命名
- 在文件里写入内容,实现类(当前注解类)的全路径
我们看下autoservice的注解处理
private void processAnnotations(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {//获取所有加了AutoService注解的类Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(AutoService.class);for (Element e : elements) {//将Element转成TypeElementTypeElement providerImplementer = MoreElements.asType(e);//获取AutoServce注解指定的valueAnnotationMirror annotationMirror = getAnnotationMirror(e, AutoService.class).get();//获取value集合Set<DeclaredType> providerInterfaces = getValueFieldOfClasses(annotationMirror);//如果没有指定value,报错if (providerInterfaces.isEmpty()) {error(MISSING_SERVICES_ERROR, e, annotationMirror);continue;}//遍历所有的value,获取value的完整类名(例如javax.annotation.processing.Processor)for (DeclaredType providerInterface : providerInterfaces) {TypeElement providerType = MoreTypes.asTypeElement(providerInterface);//判断是否是继承关系,是则放入providers缓存起来,否则报错if (checkImplementer(providerImplementer, providerType, annotationMirror)) {providers.put(getBinaryName(providerType), getBinaryName(providerImplementer));} else {//报错代码,略}}}}
注解处理完毕,就会生成SPI注册文件。如果SPI路径上文件已经存在,先要把已存在的SPI清单读进内存,再把新的provider加进去,然后全部写出,覆盖原来的文件。这部分逻辑如下:
private void generateConfigFiles() {//获取文件工具类,processingEnv是AbstractProcessor的成员变量,直接拿来用。Filer filer = processingEnv.getFiler();//遍历之前解析的providers缓存for (String providerInterface : providers.keySet()) {//providerInterface就是value字段指定的接口,例如javax.annotation.processing.ProcessorString resourceFile = "META-INF/services/" + providerInterface;log("Working on resource file: " + resourceFile);try {SortedSet<String> allServices = Sets.newTreeSet();try {//已经存在的SPI文件FileObject existingFile =filer.getResource(StandardLocation.CLASS_OUTPUT, "", resourceFile);//SPI文件中的service条目清单Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream());log("Existing service entries: " + oldServices);allServices.addAll(oldServices);} catch (IOException e) {log("Resource file did not already exist.");}//新的service条目清单Set<String> newServices = new HashSet<>(providers.get(providerInterface));//如果已经存在,则不处理if (!allServices.addAll(newServices)) {log("No new service entries being added.");continue;}//以下是将缓存的services写入文件中。log("New service file contents: " + allServices);FileObject fileObject =filer.createResource(StandardLocation.CLASS_OUTPUT, "", resourceFile);try (OutputStream out = fileObject.openOutputStream()) {ServicesFiles.writeServiceFile(allServices, out);}log("Wrote to: " + fileObject.toUri());} catch (IOException e) {fatalError("Unable to create " + resourceFile + ", " + e);return;}}}
所以我们将AutoService加到java项目中,其实就是引入了AutoServiceProcessor这个注解处理器,帮助我们处理@AutoService注解,将我们的服务(一般是APT类,也可以是其它的类,通过value指定)自动注册进SPI文件中。
三、Javapoet
javapoet是square推出的开源java代码生成框架,提供Java Api生成.java源文件 这个框架功能非常实用。
相关文章:

Java SPI 一 之SPI(Service Provider Interface)进阶 AutoService
一、SPI(Service Provider Interface) 1.1 介绍 SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制(为某个接口寻找服务实现的机制),可以用来启用框架扩展和替换组件,其…...

C++ list类成员函数介绍
目录 🤔list模板介绍: 🤔特点: 🤔list内存结构图解: 🤔 list的成员函数: 😊list构造函数: 🔍代码示例: 🔍运行结果&…...

【服务器】本地搭建PHP简单Imagewheel私人云图床
文章目录 1.前言2. Imagewheel网站搭建2.1. Imagewheel下载和安装2.2. Imagewheel网页测试2.3.cpolar的安装和注册 3.本地网页发布3.1.Cpolar临时数据隧道3.2.Cpolar稳定隧道(云端设置)3.3.Cpolar稳定隧道(本地设置) 4.公网访问测…...
第四十二回:DateRangePickerDialog Widget
文章目录 概念介绍使用方法示例代码 我们在上一章回中介绍了DatePickerDialog Widget相关的内容,本章回中将介绍 DateRangePickerDialog Widget.闲话休提,让我们一起Talk Flutter吧。 概念介绍 我们在这里说的DateRangePickerDialog是一种弹出窗口,只不…...

【C++系列P3】‘类与对象‘-三部曲——[基础知识](1/3)
前言 大家好吖,欢迎来到 YY 滴 C系列 ,热烈欢迎! 【 类与对象-三部曲】的大纲主要内容如下: 如标题所示,本章是【 类与对象-三部曲】三章中的第一章节——基础知识章节,主要内容如下: 目录 一.…...
Android UEvent事件分析之Kernel上报电量
kernel-4.4\drivers\power\power_supply_core.c 当电量信息需要更新的时候,kernel会调用power_supply_changed_work这个工作队列,使用kobject_uevent函数往上发送uevent事件,action是KOBJ_CHANGE; static void power_supply_changed_work(struct work_struct *work) {uns…...
C++ vector模板和deque的简单应用
目录 🤔vector模板和deque的简单介绍: 🤔vector和deque的主要不同之处: 🤔今天我们用vector模板和deque模板实现以下简单的功能: 代码实现: 🤔讲解: 🤔vector模板和d…...

声明式事务控制
声明式事务控制 编程式事务控制相关对象 PlatformTransactionManager PlatformTransactionManager接口是spring的事务管理器,它里面提供了常用的操作事务的方法 方法说明TransactionStatus getTransaction(TransactionDefaultion defination)获取事务的状态信息…...

cisp pte模拟题
1.信息搜集 本题共三个key 端口 1433 27689 存活ip 192.168.85.137 2.访问网站27689进行信息搜集 一个登录框,sql注入失败,暴力破解失败 扫描目录 发现三个文件robots.txt ,web.config 除了robots.txt,其他都访问不了 访问robots.txt,发现一个file参数…...

Docker容器 和 Kubernetes容器集群管理系统
一、快速了解Docker 1. 什么是Docker的定义 Docker 是一个开源的应用容器引擎,基于Go语言并遵从 Apache2.0 协议开源。Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以…...

港联证券|资金疯狂涌入AI,这一板块涨幅超90%!万亿巨头继续狂飙
国内外资金继续加码AI概念股。 当前,国内政策层面对于通用人工智能和算力中心的创新发展扶持政策不断推出,资本市场对于AI(人工智能)热情不断升温。在AI下游应用中,游戏板块成为最被看好的投资标的,资金流入…...

短视频矩阵系统源码-开源开发php语言搭建
短视频矩阵系统源码---------- php源码是什么? PHP源码指的就是PHP源代码,源代码是用特定编程语言编写的人类可读文本,源代码的目标是为可以转换为机器语言的计算机设置准确的规则和规范。因此,源代码是程序和网站的基础。 PHP…...

PFEA112-20 3BSE050091R20 张力控制器
您的张力测量系统包括: 张力电子PFEA111或PFEA112–PFEA111是一款经济高效的紧凑型用户友好型张力电子产品,提供 来自两个称重传感器的精确可靠的快速模拟SUM信号,用于控制 和/或监测。显示器可以显示SUM、单个A&B和差异信号小尺寸和DIN导…...
Java springboot+vue生成报纸排版页面的新闻官网
实现方案,可以作为您开始开发的参考: 后端: 使用Spring Boot框架构建Java Web应用;使用MyBatis Plus进行ORM映射,管理MySQL数据库;按照需求定义新闻数据表、类别数据表和用户数据表,使用默认的…...

Terra-Luna归零一年后:信任重建、加密未来路在何方?
本月既是Terra-Luna归零的一周年,也是FTX崩溃的第六个月,而这两个事件分别代表着2022年加密市场连环爆的开始与高潮,引发了加密行业15年历史上最可怕的生存危机。 尽管今年市场行情有所回暖,比特币开年至今涨幅70%,以太…...
Android 12.0 手动安装Persistent app失败的解决方案
1.概述 在12.0的系统产品开发中,对于一些安装app的失败问题,需要看日志 和抛出异常来判断问题所在,在最近的一些app安装失败抛出了关于Presistent app安装失败的问题,就需要从PMS安装的过程中看异常抛出的原因解决问题所在 2.手动安装Persistent app失败的解决方案的核心类…...

Unity3D安装:从命令行安装 Unity
推荐:将 NSDT场景编辑器 加入你的3D工具链 3D工具集: NSDT简石数字孪生 从命令行安装 Unity 如果要在组织中自动部署 Unity,可以从命令行安装 Editor 和其他组件。这些组件是普通的安装程序可执行程序和软件包,可以给用来自动部署…...

C++模板(详解)
非类型模板参数 模板参数可分为类型形参和非类型形参。类型形参: 出现在模板参数列表中,跟在class或typename关键字之后的参数类型名称。非类型形参: 用一个常量作为类(函数)模板的一个参数,在类ÿ…...
WuThreat身份安全云-TVD每日漏洞情报-2023-05-25
漏洞名称:Mitsubishi Electric MELSEC iQ-F 数据包缓冲区溢出 漏洞级别:严重 漏洞编号:CVE-2023-1424 相关涉及:Mitsubishi Electric Corporation MELSEC iQ-F Series CPU 漏洞状态:未定义 参考链接:https://tvd.wuthreat.com/#/listDetail?TVD_IDTVD-2023-12805 漏洞名称:Ap…...
android 12.0去掉recovery模式UI页面的选项
1.概述 在12.0进行定制化开发,会根据需要去掉recovery模式的一些选项 就是在device.cpp去掉一些选项就可以了 2.去掉recovery模式UI页面的选项核心代码 bootable/recovery/recovery_ui/device.cpp bootable/recovery/recovery_main.cpp 3.去掉recovery模式UI页面的选项的核…...
Golang dig框架与GraphQL的完美结合
将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用,可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器,能够帮助开发者更好地管理复杂的依赖关系,而 GraphQL 则是一种用于 API 的查询语言,能够提…...
第25节 Node.js 断言测试
Node.js的assert模块主要用于编写程序的单元测试时使用,通过断言可以提早发现和排查出错误。 稳定性: 5 - 锁定 这个模块可用于应用的单元测试,通过 require(assert) 可以使用这个模块。 assert.fail(actual, expected, message, operator) 使用参数…...

ETLCloud可能遇到的问题有哪些?常见坑位解析
数据集成平台ETLCloud,主要用于支持数据的抽取(Extract)、转换(Transform)和加载(Load)过程。提供了一个简洁直观的界面,以便用户可以在不同的数据源之间轻松地进行数据迁移和转换。…...

Python爬虫(一):爬虫伪装
一、网站防爬机制概述 在当今互联网环境中,具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类: 身份验证机制:直接将未经授权的爬虫阻挡在外反爬技术体系:通过各种技术手段增加爬虫获取数据的难度…...

Mysql中select查询语句的执行过程
目录 1、介绍 1.1、组件介绍 1.2、Sql执行顺序 2、执行流程 2.1. 连接与认证 2.2. 查询缓存 2.3. 语法解析(Parser) 2.4、执行sql 1. 预处理(Preprocessor) 2. 查询优化器(Optimizer) 3. 执行器…...

LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf
FTP 客服管理系统 实现kefu123登录,不允许匿名访问,kefu只能访问/data/kefu目录,不能查看其他目录 创建账号密码 useradd kefu echo 123|passwd -stdin kefu [rootcode caozx26420]# echo 123|passwd --stdin kefu 更改用户 kefu 的密码…...
JavaScript基础-API 和 Web API
在学习JavaScript的过程中,理解API(应用程序接口)和Web API的概念及其应用是非常重要的。这些工具极大地扩展了JavaScript的功能,使得开发者能够创建出功能丰富、交互性强的Web应用程序。本文将深入探讨JavaScript中的API与Web AP…...
GitHub 趋势日报 (2025年06月06日)
📊 由 TrendForge 系统生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日获星趋势图 今日获星趋势图 590 cognee 551 onlook 399 project-based-learning 348 build-your-own-x 320 ne…...

c++第七天 继承与派生2
这一篇文章主要内容是 派生类构造函数与析构函数 在派生类中重写基类成员 以及多继承 第一部分:派生类构造函数与析构函数 当创建一个派生类对象时,基类成员是如何初始化的? 1.当派生类对象创建的时候,基类成员的初始化顺序 …...
uniapp 实现腾讯云IM群文件上传下载功能
UniApp 集成腾讯云IM实现群文件上传下载功能全攻略 一、功能背景与技术选型 在团队协作场景中,群文件共享是核心需求之一。本文将介绍如何基于腾讯云IMCOS,在uniapp中实现: 群内文件上传/下载文件元数据管理下载进度追踪跨平台文件预览 二…...