Java SPI 一 之SPI(Service Provider Interface)进阶 AutoService

news2024/12/28 21:00:10

一、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 {
    @Override
    public 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 {
    @Override
    public void trade() {
        System.out.println("券商 C");
    }
}

到这里就结束了,是不是要简化了很多。这个机制同样适用Android,如安卓组件化,demo比较简单,就不贴代码了。
在这里插入图片描述

二、Autoservice

AutoService是Google开发一个自动生成SPI清单文件的框架。

自动往 resources/META-INF/services/ 写入文件。

https://github.com/google/auto

不用它也行,如果不使用它就需要手动去创建这个文件、手动往这个文件里添加服务(接口实现)。

其原理步骤:

  1. 遍历找到所有带有AutoService注解的类
  2. 验证AutoService注解的值是否正确
  3. 遍历所有的下沉接口
  4. 在META-INF/services/路径下创建文件,文件名以类的接口类全路径命名
  5. 在文件里写入内容,实现类(当前注解类)的全路径

我们看下autoservice的注解处理


    private void processAnnotations(
            Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        //获取所有加了AutoService注解的类
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(AutoService.class);

        for (Element e : elements) {
            //将Element转成TypeElement
            TypeElement providerImplementer = MoreElements.asType(e);
            //获取AutoServce注解指定的value
            AnnotationMirror 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.Processor
            String 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源文件 这个框架功能非常实用。

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

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

相关文章

Admin.NET管理系统(c#+vue3)前后端学习笔记

我的学习笔记 - 9iAdmin.NET 欢迎学习交流&#xff08;一&#xff09;前端笔记1.1 关于.env的设置1.2 关于路由模式问题1.3 关于 vue.config.ts1.4 关于 打包&#xff08;pnpm run build&#xff09;溢出问题1.5 关于 打包&#xff08;pnpm run build&#xff09;后部署到IIS重…

article-六轴码垛机器人admas正逆运动学仿真

基座自由度、大臂摆动自由度、小臂摆动自由度、腕部Y轴摆动自由度、腕部Z轴摆动自由度及其腕部末端X轴旋转自由度 其导入过程为&#xff1a; 机器人三维模型总体有6个部分。打开机器人的SolidWork三维模型&#xff0c;依次另存为6个“Parasolid(x_t)”类型的文件。打开ADAMS/…

【MATLAB第36期】基于MATLAB的QOWOA-LSTM鲸鱼优化算法准反向策略的WOA优化LSTM时间序列预测模型 优势明显,注释详细,绘图丰富

【MATLAB第36期】基于MATLAB的QOWOA-LSTM鲸鱼优化算法准反向策略的QOWOA优化LSTM时间序列预测模型&#xff0c;优势明显&#xff0c;注释详细&#xff0c;绘图丰富 一、代码优势 1.使用优化后的QOWOA算法优化LSTM超参数&#xff08;学习率&#xff0c;隐藏层节点&#xff0c;…

2020下半年上午题

2020下半年 d a b 小阶向大阶对齐 b b 平均cpi: MIPS: d c 公加验&#xff0c;私解签 加密防止被动攻击&#xff0c;认证防止主动攻击 a 访问控制包括&#xff1a;授权&#xff0c;确定存取权限&#xff0c;实施存取权限 c a c a 先申请先得 b b 著作权包括&…

OpenCV使用SURF和SIFT算法报错解决记录

OpenCV使用SURF和SIFT算法报错解决记录 1.报错代码&#xff0c;使用以下两种写法都会报错 # 创建SIFT和SURF特征提取器 # 写法1 sift cv2.xfeatures2d.SIFT_create() surf cv2.xfeatures2d.SURF_create() # 写法2 sift cv2.SIFT_create() surf cv2.SURF_create()第一种报…

架构整洁之道下篇(实现细节)

目录 1.实现细节 1.1.数据库只是实现细节 1.2.Web是实现细节 1.3.应用程序框架是实现细节 1.4.案例分析&#xff1a;视频销售网站 1.5.拾遗 1.5.1.按层封装 1.5.2.按功能封装 1.5.3.端口和适配器 1.5.4.按组件封装 1.5.5.组织形式和封装的区别 2.总结 1.实现细节 …

13_Uboot移植

目录 查找NXP官方的开发板默认配置文件 编译NXP官方开发板对应的uboot 烧写验证与驱动测试 SD卡和EMMC驱动检查 LCD驱动检查 网络驱动 在U-Boot中添加自己的开发板 添加开发板默认配置文件 添加开发板对应的头文件 添加开发板对应的板级文件夹 修改mx6ull_alientek_…

Vue——状态管理库Pinia

写在前面&#xff1a;本文参考小满大牛的pinia专栏 一、Vuex与Pinia Vuex 和 Pinia 均是 Vue.js 的状态管理库&#xff0c;它们为 Vue 应用程序提供了一种集中式的、可预测的状态管理解决方案。 Vuex 是 Vue.js 官方推荐的状态管理库之一。它的核心概念包括 state、mutation…

【C++初阶】类与对象(中)之取地址及const取地址操作符重载

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前学习C和算法 ✈️专栏&#xff1a;C航路 &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章对你有帮助的话 欢迎 评论&#x1f4ac; 点赞&#x1…

架构整洁之道中篇(组件构建原则软件架构)

目录 1.组件构建原则 1.1.组件 1.2.组件聚合 1.3.组件耦合 2.软件架构 2.1.什么是软件架构&#xff1f; 2.2.独立性 2.3.划分边界 2.4.策略与层次 2.5.业务逻辑 2.6.尖叫的软件架构 2.7.整洁架构 2.8.层次与边界 2.9.Main组件 2.10.测试边界 2.11.整洁的嵌入式…

Edgedetect2

边缘检测&#xff0c;检查数据变化&#xff0c;用异或实现 对于 8 位矢量中的每个位&#xff0c;检测输入信号何时从一个时钟周期变为下一个时钟周期&#xff08;检测任何边沿&#xff09;。输出位应在发生 0 到 1 转换后设置周期。 以下是一些示例。为清楚起见&#xff0c;in…

HNU-电路与电子学-小班4

第四次小班讨论 一、题目 1、书 3-41、3-62 2、书 4-23、4-26 3、设计一个时序电路。该电路仅在连续三个或三个以上时钟期间&#xff0c;且两个输入信号 X1 和 X2 相同时&#xff0c;输出信号 Z 为 1&#xff0c;其余情况 Z 为 0。试做出该电路的 Mealy 机和 Moore 机状态…

Windows:设置右键用RStudio打开文件和文件夹

0. 前言 在使用RStudio写R脚本的时候总是要先打开它&#xff0c;再通过它打开脚本和文件夹&#xff0c;感觉不是很方便。由于VSCode以及其他软件都可以整合到右键菜单中打开文件或文件夹&#xff0c;因此就折腾了一下怎么在右键中使用RStudio打开文件&#xff0c;下面是效果展…

简析java JNI技术

前言 认识JNI(Java Native Interface)技术&#xff0c;了解Java调用本地C/C库的简单原理以及一些基本的知识点&#xff1b;自己编写一个自定义的JNI接口。 一、简介 JNI是Java Native Interface的缩写&#xff0c;通过使用 Java本地接口书写程序&#xff0c;可以确保代…

在vue3中如何使用百度地图API(详细步骤+demo示例)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、注册账号、申请成为开发者二、申请密钥AK三、在vue3.0中使用百度地图API 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、注册账号…

htb Mailroom里容器(Debian 11)图形界面显示在本机kali上,socat,unix转发,容器里不安装xrdp

在攻击机kali(ip:10.10.14.18)上运行chisel服务端: chisel server -v -p 60080 --socks5 在靶机的虚拟机(ssh root10.10.11.209)上,执行docker exec containers_sites_1 /bin/bash,进入容器里 进入容器后,先下载kali上的socat和chisel: curl -o /bin/chisel http://10.10.14…

使用JMeter+Grafana+Influxdb搭建可视化性能测试监控平台

【背景说明】 使用jmeter进行性能测试时&#xff0c;工具自带的查看结果方式往往不够直观和明了&#xff0c;所以我们需要搭建一个可视化监控平台来完成结果监控&#xff0c;这里我们采用三种JMeterGrafanaInfluxdb的方法来完成平台搭建 【实现原理】 通过influxdb数据库存储…

初学用于华为鸿蒙系统(HarmonyOS)的编程开发工具HUAWEI DevEco Studio:你好,鴻蒙~

本文是6月6日博文“初学用于华为鸿蒙系统(HarmonyOS)的编程开发工具HUAWEI DevEco Studio”的续篇。 成功通过华为开发者联盟的实名认证审核后&#xff0c;使用远程模拟器(Remote Emulator)运行程序。 步骤如下&#xff1a; 菜单Tools - Device Manager&#xff1a; 点击设备…

Vue列表渲染

1&#xff0c;回顾HTML列表&#xff1f; 答&#xff1a;列表分为顺序列表ol&#xff0c;无序列表ul&#xff0c;用于在网页上以表格的形式进行数据展示&#xff0c;数据放在单元格之中&#xff0c;可以用于布局或者展示某个具体对象的信息。li表示列表的每一项。自定义列表为dl…

C++多态详解(虚函数重写、接口继承、虚函数表详解)

目录 1. 多态概念 2. 多态的定义及实现 2.1 多态的构成条件 2.2 虚函数重写 2.3 C11 override和final 2.4 重载、覆盖&#xff08;重写&#xff09;、隐藏&#xff08;重定义&#xff09;的对比 3. 抽象类 3.1 概念 3.2 接口继承和实现继承 4. 多态的原理 4.1 虚函数表 4.2…