spi 02-spi 的實戰解決 slf4j 包衝突問題git
spi 03-spi jdk 實現源碼解析github
spi 04-spi dubbo 實現源碼解析spring
spi 05-dubbo adaptive extension 自適應拓展app
上一節咱們本身動手實現了一個簡單版本的 SPI。工具
這一節咱們一塊兒來實現一個相似於 google auto 的工具。this
定義接口google
@SPI public interface Say { void say(); }
@SPIAuto("bad") public class SayBad implements Say { @Override public void say() { System.out.println("bad"); } }
@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 類 * @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 配置文件。