java SPI 07-自動生成 SPI 配置文件實現方式

系列目錄

spi 01-spi 是什麼?入門使用java

spi 02-spi 的實戰解決 slf4j 包衝突問題git

spi 03-spi jdk 實現源碼解析github

spi 04-spi dubbo 實現源碼解析spring

spi 05-dubbo adaptive extension 自適應拓展app

spi 06-本身從零手寫實現 SPI 框架框架

spi 07-自動生成 SPI 配置文件實現方式ide

回顧

上一節咱們本身動手實現了一個簡單版本的 SPI。工具

這一節咱們一塊兒來實現一個相似於 google auto 的工具。this

使用演示

類實現

  • Say.java

定義接口google

@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

相關文章
相關標籤/搜索