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

这篇具有很好参考价值的文章主要介绍了Java SPI 一 之SPI(Service Provider Interface)进阶& AutoService。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

文章来源地址https://www.toymoban.com/news/detail-462705.html

一、SPI(Service Provider Interface)

1.1 介绍

SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制(为某个接口寻找服务实现的机制),可以用来启用框架扩展和替换组件,其核心思想就是 解耦。

模块之间基于接口编程,模块之间不对实现类进行硬编码,当代码里涉及具体的实现类,就违反了可拔插的原则,为了实现在模块装配的时候能不在程序里动态指明,就需要spi了。

这里我们要跟API区分开来,简单介绍一下API

API(Application Programming Interface)是一种应用程序编程接口,它定义了一组用于与特定软件组件或服务进行交互的函数、方法和数据结构。
其目的主要用于提供一种与特定软件组件或服务进行交互的抽象层。
比如我们常见的系统API,接入的各种三方API,这些API的特点是实现方式以及做好了,开发者调用这些API来做一些有预期的事情。
Java SPI 一 之SPI(Service Provider Interface)进阶& AutoService

1.2 使用场景

举个简单的例子,例如芯片公司定义了一个规范,需要第三方厂商去实现,那么对于芯片公司方来说,只需要集成对应厂商的插件,就可以完成对应规范的实现机制。 形成一种插拔式的扩展手段。

如JDBC、日志框架等都用到。

Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要(通常由服务提供者(如库或框架的开发者)实现,以提供特定功能的多种实现),我们看个图
​​Java SPI 一 之SPI(Service Provider Interface)进阶& AutoService

1.3 原理

将接口的实现类放在配置文件中,在程序运行过程中读取配置文件,通过反射加载实现类。

具体流程:
–> 读取META-INF/services/下的配置文件
–> 获得所有能被实例化的类的名称
–> 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化
–> 把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型),然后返回实例对象。

JDK中查找服务的实现的工具类是:java.util.ServiceLoader。JDK标准的SPI会一次性加载实例化扩展点的所有实现。

1.4 使用demo

我们举例,先看下完整的项目目录
Java SPI 一 之SPI(Service Provider Interface)进阶& AutoService
Java SPI 一 之SPI(Service Provider Interface)进阶& AutoService
我们现在需要进行股票交易,有多个券商可用。

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/ 目录,在该目录下创建文件 ,如下图所示:
Java SPI 一 之SPI(Service Provider Interface)进阶& AutoService
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
Java SPI 一 之SPI(Service Provider Interface)进阶& AutoService

新建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比较简单,就不贴代码了。
Java SPI 一 之SPI(Service Provider Interface)进阶& AutoService

二、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源文件 这个框架功能非常实用。

到了这里,关于Java SPI 一 之SPI(Service Provider Interface)进阶& AutoService的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • linux进阶55——service文件

    创建ping@.service文件,内容可以如下: 将.service文件拷贝至/usr/lib/systemd/system/下 systemctl是一个systemd工具,负责控制systemd系统和管理系统服务。 主要包括服务描述、启动顺序和依赖关系。 Description:当前服务的简单描述; Documentation:文档位置; After:在什么服务之后启动;

    2024年02月16日
    浏览(33)
  • 【K8S系列】第十二讲:Service进阶

    目录 ​编辑 序言 1.Service介绍 1.1 什么是Service 1.2 Service 类型 1.2.1 NodePort 1.2.2 LoadBalancer 1.2.3 ExternalName 1.2.4 ClusterIP 2.yaml名词解释  3.投票  当发现自己的才华撑不起野心时,就安静下来学习吧 三言两语,不如细心探索 今天学习一下Service相关内容,希望此文,能帮助读者对

    2024年02月02日
    浏览(36)
  • java中interface的使用以及注意事项

    一、接口(interface)基本概念 接口(interface):是java中一种引用数据类型,可以看做方法的集合,其内部主要就是封装了方法,包含抽象方法(JDK 7及以前),默认方法和静态方法(JDK 8),私有方法(JDK 9). 二、使用格式   1.定义格式:   public interface 接口名称{     //抽象方法    

    2024年02月06日
    浏览(37)
  • java使用@interface和反射来动态生成excel

    1、对象类上搞注解 public class ReportExecuteDetailDto {     // 项目信息     private String regionCode;         // 大区编号     @ExcelColumn(order = 0, title = \\\"大区\\\")     private String regionName;         // 大区名称          @ExcelColumn(order = 14, title = \\\"行申请金额\\\", dataType = ExcelColumn.FieldType.

    2024年02月11日
    浏览(34)
  • 什么是Java中的JVMTI(JVM Tool Interface)?

    Java中的JNI(Java Native Interface)和JVMTI(JVM Tool Interface)都是与Java运行时环境(JVM)交互的工具,但它们有不同的目的和使用场景。下面我从新手的角度来幽默地解释一下它们的区别和用途。 JNI:Java Native Interface,就是Java Native库。当你需要在本地代码(C、C++等)中调用Java代

    2024年02月14日
    浏览(41)
  • 【GiraKoo】Java Native Interface(JNI)的空间(引用)管理

    Java是通过垃圾回收机制回收内存,C/C++是通过malloc,free,new,delete手动管理空间。那么在JNI层,同时存在Java和C/C++的空间时,该如何进行空间的管理呢?本文参考Oracle的官方文档,对JNI层中空间的管理进行说明。明确哪些内容需要手动调用Delete,哪些不需要手动调用。 全局

    2024年02月05日
    浏览(35)
  • JAVA加密解密异常之java.security.NoSuchAlgorithmException: Cannot find any provider supporting DES/CBC/PKCS7

    AVA加密解密异常之java.security.NoSuchAlgorithmException: Cannot find any provider supporting AES/CBC/PKCS7Padding 网上最接近的解决办法: 最后解决办法: 1.在jdk中的jrelibsecurity修改java.security文件,替换security.provider.7=org.bouncycastle.jce.provider.BouncyCastleProvider。 并将原有的#security.provider.7=com.sun.s

    2024年02月03日
    浏览(52)
  • JAVA-- 带你重温函数式接口、使用Functional Interface最佳实践

    函数式接口( Functional Interface )就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口 并且这类接口使用了 @FunctionalInterface 进行注解。在jdk8中,引入了一个新的包 java.util.function , 可以使java 8 的函数式编程变得更加简便。这个package中的接口大致分为了以下七类:

    2024年02月02日
    浏览(42)
  • Java | interface 和 implements关键字【接口,看这篇就够了】

    学完 继承 、学完 多态 ,但面对汹涌而来🌊的接口,相信很多同学都不知所措,因此我耗费几天几夜的时间,搜寻大量书籍资料,苦心闭关钻研,写出了一篇关于Java的接口从 入门小白到精通大佬 的学习之路,相信这篇文章一定对您有所帮助📖 Java接口是一系列方法的声明

    2024年02月19日
    浏览(48)
  • 分分钟搞定Java中的抽象类和接口!!!(abstract & interface)

    1.抽象类的定义 概述: 我们创建一个动物类,并且在这个类中创建动物对象,但是当你提到动物类,你并不知道我说的是什么动物,只有看到了具体的动物,你才知道这是什么动物,所以说动物本身并不是一个具体的事物,而是一个抽象的事物。只有真正的猫,狗才是具体的

    2024年02月01日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包