從maven的debug compile到java的編譯時註解(與springboot專案整合)
事情的開始要從週一說起,那天晚上我正常編譯打包準備更換部件,這時突然發現maven有個選項是debug maven compile,遂感到奇怪,這玩意有啥用??,唯一能想到的是編譯時進行debug,但具體的應用場景不清楚,這時旁邊的同事吐槽了一句自從架構升級到中臺之後,我們負責的模組再也沒有控制器了,統一放到了閘道器部件,閘道器只依賴各個部件的"能力層",即只依賴介面,需要執行業務邏輯時需要解析各個部件的實現層提供的api檔案,這個檔案就是一個json陣列,但是這玩意很煩,需要手動去配置,我通常是拷貝一個再去修改,但經常翻車,忘記把service或者method改掉,這時我突然想到編譯時生成配置檔案然後打包不就完事了嗎?
1 {
2 "uri": "/api/test/ok",
3 "operation": "hello",
4 "description": "hello",
5 "service": "xxxx.service",
6 "method": "sayHello"
7 }
第一個想到的實現方式就是註解,但之前用的最多的註解型別是RUNTIME型別的,可以結合spring,反射+point實現註解邏輯,但編譯時註解怎麼操作?原理是javac編譯的過程是一個獨立的虛擬機器程序,jdk提供了一個AbstractProcessor留給我們去繼承重寫相關方法(熟悉設計模式的估計已經想到模板模式了),達到編譯時執行我們的註解處理器邏輯的目的.怎麼做的問題解決了,可如何讓虛擬機器去載入我們的註解處理器呢?答案是SPI(service provider interfacse),如果你熟悉dubbo,或者spring,你會經常發現他們的jar包中的META-INF下面總是別有洞天,來兩張圖感受下
那到底啥是SPI,大白話就是第三方框架或者jsr相關規範定義好了介面,你直接來實現,並且告訴別人這個介面的實現類必須是我寫的實現類,怎麼操作呢?原始的java SPI網上一搜一堆,META-INF下新建services資料夾,新建檔名稱為介面的全限定名,內容為實現類的全限定名,之後使用ServiceLoader進行載入,不過這玩意很煩,只要你寫到這個檔案類的實現都會被載入,所以dubbo做了優化,改成了鍵值對的方式,具體的可以看dubbo.internal下的實現
ok,把話說回來,SPI的作用就是XXInterface service = your impl,當你作為一個第三方框架的服務提供者,你就感受到這樣做的好處了,後面會再寫一篇文章詳細介紹,等不及的可以先去看effective java,靜態工廠方法那一塊也有相關SPI講解
照貓畫虎,只要我們的註解處理器也這樣操作就可以了,剛剛說到我們繼承了AbstractProcessor,那麼我們的spi檔名就是他實現的介面全限定名了,內容是我們的註解處理器的全限定名了,so
如果你感覺這樣麻煩採用guava提供的註解@AutoService即可,這個註解幫我們生成services下的檔案,編碼會貼在下面,不過在此之前要先說說怎麼整合到我們的專案中?
首先給待整合的專案加入我們註解處理器專案的依賴,然後我們需要配置下maven的打包外掛在編譯時執行我們的註解處理器,在整合的過程中發現,由於使用了lombok,其自身也是藉助編譯處理器生成getter,setter程式碼所以我們也要讓maven執行lombok的註解處理器,否則編譯時各種找不到符號,可是lombok的註解處理器叫啥名?彆著急,找到lombok的jar包然後去找META-INF下的services
看到了熟悉的檔名,檢視內容就有了下面的註解處理器配置,問題解決,最後貼下配置和程式碼
1 <dependency>
2 <groupId>xxx</groupId>
3 <artifactId>api-build</artifactId>
4 <version>1.0-SNAPSHOT</version>
5 <scope>provided</scope>
6 </dependency>
7
8
9 <plugin>
10 <artifactId>maven-compiler-plugin</artifactId>
11 <version>3.3</version>
12 <configuration>
13 <source>1.8</source>
14 <target>1.8</target>
15 <encoding>UTF-8</encoding>
16 <annotationProcessors>
17 <annotationProcessor>lombok.launch.AnnotationProcessorHider$AnnotationProcessor</annotationProcessor>
18 <annotationProcessor>lombok.launch.AnnotationProcessorHider$ClaimingProcessor</annotationProcessor>
19 <annotationProcessor>你的註解處理器限定名稱</annotationProcessor>
20 </annotationProcessors>
21 </configuration>
22 </plugin>
註解定義,注意是編譯時註解RetenionPolicy.CLASS
1 @Documented
2 @Target(ElementType.METHOD)
3 @Retention(RetentionPolicy.CLASS)
4 public @interface ApiFunction
5 {
6
7 String uri() default "";
8
9 String operation() default "";
10
11 String description() default "";
12
13 /* String service() default "";
14
15 String method() default "";*/
16
17 String group() default "";
18
19 String version() default "";
20
21 String[] authorities() default {};
22
23 boolean needAuth() default true;
24
25 int priority() default 0;
26
27 }
註解處理器相關程式碼與依賴
1 <!--@AutoService-->
2 <dependency>
3 <groupId>com.google.auto.service</groupId>
4 <artifactId>auto-service</artifactId>
5 <version>1.0-rc6</version>
6 </dependency>
7
8 <!--註解處理器自身是個processor,所以編譯時不可再去指定processor,否則迴圈呼叫自己,xx not found exception,gg-->
9 <plugin>
10 <artifactId>maven-compiler-plugin</artifactId>
11 <version>3.8.0</version>
12 <configuration>
13 <encoding>UTF-8</encoding>
14 <source>1.8</source>
15 <target>1.8</target>
16 <proc>none</proc>
17 </configuration>
18 </plugin>
1 /**
2 * @author tele
3 * @Description
4 * @create 2020-09-07
5 */
6 @AutoService(Processor.class)
7 public class ApiInfoGenerateProcessor extends AbstractProcessor
8 {
9 /**
10 * application.properties中的key,是否允許使用註解生成api-define.json
11 */
12 private static final String SWITCH = "api.auto-generate.enable";
13
14 /**
15 * 檔案路徑
16 */
17 private static final String API_PATH = "config/api-define.json";
18
19 /**
20 * 配置項路徑
21 */
22 private static final String APPLICATION_PATH = "application.properties";
23
24 private static boolean enable;
25
26 private FileObject apiInfoFile;
27
28 @Override
29 public synchronized void init(ProcessingEnvironment processingEnv)
30 {
31 try
32 {
33 // 檔案寫入到編譯後路徑
34 apiInfoFile = processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT,"",API_PATH);
35 // 從原始碼位置讀取配置開關
36 InputStream inputStream = processingEnv.getFiler().getResource(StandardLocation.CLASS_PATH, "", APPLICATION_PATH).openInputStream();
37 Properties properties = new Properties();
38 properties.load(inputStream);
39 enable = Boolean.valueOf(properties.getProperty(SWITCH));
// TODO close and check
40 }
41 catch (IOException e)
42 {
43 e.printStackTrace();
44 }
45 }
46
47 /**
48 * 指定支援的jdk版本
49 * @return
50 */
51 @Override
52 public SourceVersion getSupportedSourceVersion()
53 {
54 return SourceVersion.latestSupported();
55 }
56
57 /**
58 * 處理哪種型別的註解, * 表示處理所有型別的註解
59 * @return
60 */
61 @Override
62 public Set<String> getSupportedAnnotationTypes()
63 {
64 return Sets.newHashSet(ApiFunction.class.getCanonicalName());
65 }
66
67 /**
68 *
69 * @param annotations getSupportedAnnotationTypes返回的註解處理器集合
70 * @param roundEnv 獲取掃描到的註解節點
71 * @return 當你有多個註解處理器時,返回true表示其他註解處理器不再對該型別的註解進行處理
72 */
73 @Override
74 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
75 {
76 if(!annotations.isEmpty() && enable) {
77 Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(ApiFunction.class);
78 List<String> apiInfoList = elementsAnnotatedWith.stream().filter(e -> e.getEnclosingElement().getKind().equals(ElementKind.CLASS)).map(e -> {
79 // service 獲得父元素,註解加在方法上,父元素就是類或者介面了
80 TypeElement element = (TypeElement)e.getEnclosingElement();
81 // method
82 String methodName = String.valueOf(e.getSimpleName());
83 ApiFunction annotation = e.getAnnotation(ApiFunction.class);
84 Map<String, Object> map = new LinkedHashMap<>(8);
85 map.put("uri", annotation.uri());
86 map.put("operation", annotation.operation());
87 map.put("description", annotation.description());
88 map.put("service", String.valueOf(element.getQualifiedName()));
89 map.put("method", methodName);
90 if(annotation.group() != null && !"".equals(annotation.group())) {
91 map.put("group", annotation.group());
92 }
93 if(annotation.version() != null && !"".equals(annotation.version())) {
94 map.put("version", annotation.version());
95 }
96 if(annotation.authorities() != null && annotation.authorities().length != 0) {
97 map.put("authorities",Arrays.asList(annotation.authorities()));
98 }
99 // access 預設解析為true
100 if(!annotation.needAuth()) {
101 map.put("needAuth", annotation.needAuth());
102 }
103 if(annotation.priority() != 0) {
104 map.put("priority", annotation.priority());
105 }
106 return JSONUtil.toJson(map,true);
107 }).collect(Collectors.toList());
108 OutputStream outputStream = null;
109 try
110 {
111 File file = new File(apiInfoFile.toUri());
112 if(!file.exists()) {
113 file.getParentFile().mkdirs();
114 file.createNewFile();
115 }
116 System.out.println(String.format("find api:%d", apiInfoList.size()));
117 outputStream = new FileOutputStream(file);
118 IOUtils.write(String.valueOf(apiInfoList), outputStream, StandardCharsets.UTF_8);
119 outputStream.flush();
120 }
121 catch (IOException e)
122 {
123 e.printStackTrace();
124 }finally
125 {
126 try
127 {
128 IOUtils.close(outputStream);
129 }
130 catch (IOException e)
131 {
132 e.printStackTrace();
133 }
134 }
135 }
136 return true;
137 }
138 }
說回開頭,maven的debug問題,當你在依賴註解處理器的專案上執行debug maven compile時,只要給註解處理器程式碼打斷點就ok了更復雜的註解處理器應用可以參考https://juejin.im/post/6844903879524483086,也可以參考<<深入理解java虛擬機器>>第十章關的插入式註解處理器相關內容