java SPI 07-自动生成 SPI 配置文件实现方式

系列目录

spi 01-spi 是什么?入门使用

spi 02-spi 的实战解决 slf4j 包冲突问题

spi 03-spi jdk 实现源码解析

spi 04-spi dubbo 实现源码解析

spi 05-dubbo adaptive extension 自适应拓展

spi 06-自己从零手写实现 SPI 框架

spi 07-自动生成 SPI 配置文件实现方式

回顾

上一节我们自己动手实现了一个简单版本的 SPI。

这一节我们一起来实现一个类似于 google auto 的工具。

使用演示

类实现

  • Say.java

定义接口

@SPI
public interface Say {

    void say();

}
  • SayBad.java
@SPIAuto("bad")
public class SayBad implements Say {

    @Override
    public void say() {
        System.out.println("bad");
    }

}
  • SayGood.java
@SPIAuto("good")
public class SayGood implements Say {

    @Override
    public void say() {
        System.out.println("good");
    }
    
}

执行效果

执行 mvn clean install 之后。

META-INF/services/ 文件夹下自动生成文件 com.github.houbb.spi.bs.spi.Say

内容如下:

good=com.github.houbb.spi.bs.spi.impl.SayGood
bad=com.github.houbb.spi.bs.spi.impl.SayBad

代码实现

本部分主要用到编译时注解,难度相对较高。

所有源码均已开源在 lombok-ex

注解定义

@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE})
@Documented
public @interface SPIAuto {

    /**
     * 别称
     * @return 别称
     * @since 0.1.0
     */
    String value() default "";

    /**
     * 目标文件夹
     * @return 文件夹
     * @since 0.1.0
     */
    String dir() default "META-INF/services/";

}

其实这里 dir() 可以不做暴露,这里后期想做更加灵活的拓展,所以暂定为这样。

核心实现

@SupportedAnnotationTypes("com.github.houbb.lombok.ex.annotation.SPIAuto")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class SPIAutoProcessor extends BaseClassProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        java.util.List<LClass> classList = super.getClassList(roundEnv, getAnnotationClass());
        Map<String, Set<String>> spiClassMap = new HashMap<>();

        for (LClass lClass : classList) {
            String spiClassName = getSpiClassName(lClass);

            String fullName = lClass.classSymbol().fullname.toString();
            if(StringUtil.isEmpty(spiClassName)) {
                throw new LombokExException("@SPI class not found for class: "
                        + fullName);
            }
            Pair<String, String> aliasAndDirPair = getAliasAndDir(lClass);
            String newLine = aliasAndDirPair.getValueOne()+"="+fullName;

            // 完整的路径:文件夹+接口名
            String filePath = aliasAndDirPair.getValueTwo()+spiClassName;

            Set<String> lineSet = spiClassMap.get(filePath);
            if(lineSet == null) {
                lineSet = new HashSet<>();
            }
            lineSet.add(newLine);
            spiClassMap.put(filePath, lineSet);
        }

        // 生成文件
        generateNewFiles(spiClassMap);

        return true;
    }
}

整体流程:

(1)遍历所有类,找到带有 SPIAuto 注解的类

(2)根据类信息,注解信息,将所有类按照 SPI 接口分组,存储在 map 中

(3)根据 map 中的信息,生成对应的配置文件信息。

获取 SPI 接口方法名称

获取当前类的所有接口,并且找到第一个使用 @SPI 标注的接口返回。

/**
 * 获取对应的 spi 类
 * @param lClass 类信息
 * @return 结果
 * @since 0.1.0
 */
private String getSpiClassName(final LClass lClass) {
    List<Type> typeList =  lClass.classSymbol().getInterfaces();
    if(null == typeList || typeList.isEmpty()) {
        return "";
    }
    // 获取注解对应的值
    SPIAuto auto = lClass.classSymbol().getAnnotation(SPIAuto.class);
    for(Type type : typeList) {
        Symbol.ClassSymbol tsym = (Symbol.ClassSymbol) type.tsym;
        //TOOD: 后期这里添加一下拓展。
        if(tsym.getAnnotation(SPI.class) != null) {
            return tsym.fullname.toString();
        }
    }
    return "";
}

获取注解信息

注解主要是为了更加灵活指定,相对比较简单,实现如下:

针对类的别名默认是类名首字母小写,类似于 spring。

private Pair<String, String> getAliasAndDir(LClass lClass) {
    // 获取注解对应的值
    SPIAuto auto = lClass.classSymbol().getAnnotation(SPIAuto.class);
    //1. 别称
    String fullClassName = lClass.classSymbol().fullname.toString();
    String simpleClassName = fullClassName.substring(fullClassName.lastIndexOf("."));
    String alias = auto.value();
    if(StringUtil.isEmpty(alias)) {
        alias = StringUtil.firstToLowerCase(simpleClassName);
    }
    return Pair.of(alias, auto.dir());
}

生成文件

生成文件是实现最核心饿部分,主要参考 google 的 auto 实现:

其实主要难点在于文件的路径获取,这一点在编译时注解中比较麻烦,所以导致代码写的比较冗余。

/**
 * 创建新的文件
 * key: 文件路径
 * value: 对应的内容信息
 * @param spiClassMap 目标文件路径
 * @since 0.1.0
 */
private void generateNewFiles(Map<String, Set<String>> spiClassMap) {
    Filer filer = processingEnv.getFiler();
    for(Map.Entry<String, Set<String>> entry : spiClassMap.entrySet()) {
        String fullFilePath = entry.getKey();
        Set<String> newLines = entry.getValue();
        try {
            // would like to be able to print the full path
            // before we attempt to get the resource in case the behavior
            // of filer.getResource does change to match the spec, but there's
            // no good way to resolve CLASS_OUTPUT without first getting a resource.
            FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "",fullFilePath);
            System.out.println("Looking for existing resource file at " + existingFile.toUri());
            Set<String> oldLines = readServiceFile(existingFile.openInputStream());
            System.out.println("Looking for existing resource file set " + oldLines);
            // 写入
            newLines.addAll(oldLines);
            writeServiceFile(newLines, existingFile.openOutputStream());
            return;
        } catch (IOException e) {
            // According to the javadoc, Filer.getResource throws an exception
            // if the file doesn't already exist.  In practice this doesn't
            // appear to be the case.  Filer.getResource will happily return a
            // FileObject that refers to a non-existent file but will throw
            // IOException if you try to open an input stream for it.
            // 文件不存在的情况下
            System.out.println("Resources file not exists.");
        }
        try {
            FileObject newFile = filer.createResource(StandardLocation.CLASS_OUTPUT, "",
                    fullFilePath);
            try(OutputStream outputStream = newFile.openOutputStream();) {
                writeServiceFile(newLines, outputStream);
                System.out.println("Write into file "+newFile.toUri());
            } catch (IOException e) {
                throw new LombokExException(e);
            }
        } catch (IOException e) {
            throw new LombokExException(e);
        }
    }
}

其他

整体思路就是这样,还有一些细节此处就不再展开了。

欢迎移步 github lombok-ex

如果对你有帮助,给个 star 鼓励一下作者~

进步一思考

生态作为框架的一部分,主要是为了给使用者提供便利。

实际上这个工具可以做的更加灵活,比如可以为 dubbo spi 自动生成 spi 配置文件。

参考资料

AutoServiceProcessor

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JavaAgent、Javassist、SPI机制和Java反射是Java开发中的四个概念,它们在功能和应用场景上有一些区别。 1. JavaAgent: - JavaAgent是Java虚拟机(JVM)提供的一个机制,允许在程序运行时对字节码进行修改和增强。 - 主要应用于性能监控、代码热替换、AOP(面向切面编程)等方面。 - 通过JavaAgent,开发者可以在应用程序运行期间动态地修改已加载的类或增加新的类,从而实现对程序行为的改变。 2. Javassist: - Javassist是一个开源的Java字节码操作库,提供了一组简单易用的API,用于在运行时修改字节码。 - 可以通过Javassist来实现类似于JavaAgent的功能,包括动态生成类、修改现有类的方法、字段等。 - Javassist提供了更高层次的抽象和更加易用的API,使得字节码操作更加简单和灵活。 3. SPI(Service Provider Interface)机制: - SPI是一种Java的扩展机制,用于在运行时动态地发现和加载实现某个接口或抽象类的服务提供者。 - 主要应用于插件化开发,允许开发者通过配置文件或其他方式定义服务提供者,使得程序在运行时可以动态地加载和使用这些服务。 - SPI机制的核心是通过Java的反射机制来实现动态加载和调用。 4. Java反射: - Java反射是Java语言提供的一种机制,允许在运行时动态地检查类、实例化对象和调用对象的方法或字段。 - 反射可以使得开发者在运行时获取类的信息并进行操作,而不需要在编译时明确知道类的具体细节。 - Java反射可以用于动态加载类、实现框架和工具、进行代码生成等。 综上所述,JavaAgent是JVM提供的机制,Javassist是一个方便进行字节码操作的库,SPI机制是一种动态发现和加载服务提供者的机制,Java反射是Java语言提供的一种机制。它们各自在功能和应用场景上有所不同。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值