[譯]使用注解處理器生成代碼-3 生成源代碼
本博文原文地址摸我
本篇博文是關于使用注解處理器生成java代碼系列的第三篇也是最后一篇文章。在第一篇(在這里)中,我們介紹了注解和其一般用法。在第二篇(在這里)中,我們介紹了注解處理器,如何構造并且使用它。?在本篇博文中,我們將想你展示如何使用注解處理器來生成源代碼。
簡介
生成源代碼很簡單。生成正確的源代碼卻很難。優雅高效的去生成正確的代碼是很麻煩的任務。
幸運的是,Model-Driver Engineering(1)為我們提供了基于已經證明有效的過程和工具的成熟的方法理論。
MDE 的 Model 和 Meta-model
在討論如何使用注解處理器生成源代碼之前,有幾個相關的概念我們要實現講明,那就是models 和 meta-model
MDE的理論基礎之一為抽象的構造(construction of abstractions)。我們將軟件系統在不同的層次和細節上使用不同的方法進行建模。當軟件在一個抽象層次上被建模完成之后,我們就開始對下一個抽象層次進行建模,知道建立一個完備的,可部署的產品。
在這種理論環境下,一個model 就是我們用來在某一抽象層級上表示軟件系統的抽象。
meta-model就是我們用來寫model的規則,你可以理解為model的綱要或者語法。
使用注解處理器生成源代碼
由上述描述可見,注解是定義model和meta-model的好方法,注解類型(Annotation Type)充當meta-model的角色,標注在一段代碼上的注解來提供model。
我們可以使用這個model來生成配置文件或者從現有代碼中生成新代碼。比如,通過注解bean來生成遠程代理或者數據訪問對象。
這個方法的核心就是使用注解處理器。注解處理器可以讀取在源代碼中發現的注解,并且對注解做任何想做的事情-比如,打開文件,寫文件,等等。
Filter
我們在第二篇博文中曾經說過,每個處理器都可以通過處理環境(processing environment)對象獲得一些有用的工具,Filter就是其中之一。
javax.annotation.processing.Filer接口定義了一些關于創建源文件,類文件和一般資源的方法。通過使用Filter我們可以使用正確的文件目錄,并且確保不會丟失文件系統中的生成的文件或者資源。
下面這個例子可以顯示如何在注解處理器中生成代碼。生成的類名就是被注解的類名加上BeanInfo的后綴:
if (e.getKind() == ElementKind.CLASS) { TypeElement classElement = (TypeElement) e; PackageElement packageElement = (PackageElement) classElement.getEnclosingElement(); JavaFileObject jfo = processingEnv.getFiler(). createSourceFile(classElement.getQualifiedName() + "BeanInfo"); BufferedWriter bw = new BufferedWriter(jfo.openWriter()); bw.append("package "); bw.append(packageElement.getQualifiedName()); bw.append(";"); bw.newLine(); bw.newLine(); // rest of generated class contents
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
不要這樣生成代碼
上邊這個例子十分簡單,有趣但是很混亂。
我們把從注解中讀取信息的邏輯和寫生成的源文件的邏輯混一起拉。
按照上述那種方式很難寫出簡潔的代碼,如果當我們遇到一些更加復雜的邏輯時,就更難啦。
我們需要一個更加優雅的實現方式:- 將不同邏輯分離- 使用模版來讓代碼生成更加簡單
讓我們看看使用Apache的Velocity構造代碼生成器的例子吧。
Velocity簡介
Velocity 是通過混合模版和java類的數據來生成各類文本文件的模版引擎。?Velocity可以在MVC框架中渲染視圖或者在xml傳輸數據時替代XSLT
Velocity有它自己的語言叫做Velocity Template Language(VTL)。在VTL中,我們可以定義變量,控制流,迭代和獲取java對象中的數據。
下面就是Velocity模版的一個片段:
**#foreach($field in $fields)** /** * Returns the ${field.simpleName} property descriptor. * * @return the property descriptor */ public PropertyDescriptor ${field.simpleName}PropertyDescriptor() { PropertyDescriptor theDescriptor = null; return theDescriptor; } #end #foreach($method in $methods) /** * Returns the * *${method.simpleName}**() method descriptor. * * @return the method descriptor */ public MethodDescriptor ${method.simpleName}MethodDescriptor() { MethodDescriptor descriptor = null;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
正如你所看到的,VTL十分簡單并且易于理解。#foreach($field in $fields)代表對對象集合的迭代;${method.simpleName}則是打印數據信息。
Velocity代碼生成器
既然我們決定使用Veloctiy來增強我們的代碼生成器,那么我們就要重新進行設計:
- 設計用來生成代碼的模版
- 注解處理器會從round environment中讀取被注解元素,并且將其保存到對象中,比如保存成員變量,方法,或者類,包的表.
- 注解處理器需要初始化Velocity相關上下文- 注解處理器需要加載Velocity模版- 注解處理器會創建源文件(使用Filer)并且傳遞一個Writer給Velocity模版
- Veloctiy引擎生成源代碼
使用這個方案,我們會發現處理器和生成器相關的代碼是清晰,良好組織并且易于理解和維護。
讓我們一步一步的來實現這個方案吧。
步驟一:實現一個模版
為了簡單,我們并不會展示BeanInfo生成器的全部代碼,而是只展示我們注解處理器需要使用的一部分成員變量和方法。
我們先創建一個名為beaninfovm的文件,并且放置在Maven的src/main/resource下。文件內容如下:
package ${packageName}; import java.beans.MethodDescriptor; import java.beans.ParameterDescriptor; import java.beans.PropertyDescriptor; import java.lang.reflect.Method; public class ${className}BeanInfo extends java.beans.SimpleBeanInfo { /** * Gets the bean class object. * * @return the bean class */ public static Class getBeanClass() { return ${packageName}.${className}.class; } /** * Gets the bean class name. * * @return the bean class name */ public static String getBeanClassName() { return "${packageName}.${className}"; } /** * Finds the right method by comparing name & number of parameters in the class * method list. * * @param classObject the class object * @param methodName the method name * @param parameterCount the number of parameters * * @return the method if found, null
otherwise */ public static Method findMethod(Class classObject, String methodName, int parameterCount) { try { // since this method attempts to find a method by getting all // methods from the class, this method should only be called if // getMethod cannot find the method Method[] methods = classObject.getMethods(); for (Method method : methods) { if (method.getParameterTypes().length == parameterCount &&method.getName(). equals(methodName)) { return method; } } } catch (Throwable t) { return null; } return null; } #foreach($field in $fields) /** * Returns the ${field.simpleName} property descriptor. * * @return the property descriptor */ public PropertyDescriptor ${field.simpleName}PropertyDescriptor() { PropertyDescriptor theDescriptor = null; return theDescriptor; } #end#foreach($method in $methods) /** * Returns the ${method.simpleName}() method descriptor. * * @return the method descriptor */ public MethodDescriptor ${method.simpleName}MethodDescriptor() { MethodDescriptor descriptor = null; Method method = null; try { // finds the method using getMethod with parameter types // TODO parameterize parameter types Class[] parameterTypes = {java.beans.PropertyChangeListener.class}; method=getBeanClass(). getMethod("${method.simpleName}", parameterTypes); } catch (Throwable t) { // alternative: use findMethod // TODO parameterize number of parameters method = findMethod(getBeanClass(), "${method.simpleName}", 1); } try { // creates the method descriptor with parameter descriptors // TODO parameterize parameter descriptors ParameterDescriptor parameterDescriptor1 = new ParameterDescriptor(); parameterDescriptor1.setName("listener"); parameterDescriptor1.setDisplayName("listener"); ParameterDescriptor[] parameterDescriptors = {parameterDescriptor1}; descriptor = new MethodDescriptor(method, parameterDescriptors); } catch (Throwable t) { // alternative: create a plain method descriptor descriptor = new MethodDescriptor(method); } // TODO parameterize descriptor properties descriptor.setDisplayName("${method.simpleName} (java.beans.PropertyChangeListener)"); descriptor.setShortDescription("Adds a property change listener."); descriptor.setExpert(false); descriptor.setHidden(false); descriptor.setValue("preferred", false); return descriptor; } #end }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
為了使用上述的模版,我們需要向Velocity傳遞下邊這些信息:
packageName:生成類的全限定包名
className:生成類名
field:生成類中的成員變量集合;每個成員變量我們需要以下信息:
simpleName:成員變量名 - type:成員變量的類型(在本例中并未使用)
description:自我解釋型信息(在本例中并未使用)
…
method:生成類中函數的集合;每個函數我們需要一下信息:
simpleName:函數名
arguments:函數的參數(在本例中并未使用)
returnType: 函數返回值類型(在本例中并未使用)
description:自我解釋性信息(在本例中并未使用)
…
所有的這些信息都會從源文件中的注解中獲得,并保存到JavaBean中,再傳遞給Velocity。
步驟二:注解處理器讀取信息
讓我們來實現一個注解處理器,并且注解它支持處理BeanInfo注解類型,相關原理請查看第二篇博文。
@SupportedAnnotationTypes("example.annotations.beaninfo.BeanInfo") @SupportedSourceVersion(SourceVersion.RELEASE_6) public class BeanInfoProcessor extends AbstractProcessor {
1
2
3
4
注解處理器需要從注解和源文件中提取。你可以使用JavaBean來保存你需要的信息。但是在這個例子中,我們將使用javax.lang.model.element,因為我們不計劃傳遞給Velocity過多信息:
String packageName = null; Map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
步驟三:初始化Velocity并且加載模版
下邊的代碼片段展示了如何初始化Velocity并且加載模版
if (fqClassName != null) { Properties props = new Properties(); URL url = this.getClass().getClassLoader(). getResource("velocity.properties"); props.load(url.openStream()); VelocityEngine ve = new VelocityEngine(props); ve.init(); VelocityContext vc = new VelocityContext(); vc.put("classNameassName); vc.put("packageNameckageName); vc.put("fieldselds); vc.put("methodsthods); Template vt = ve.getTemplate("beaninfo.vm");
1
2
3
4
5
6
7
8
9
10
11
12
13
Velocity的配置文件,應該命名為Velocity.properties,并放置在src/main/resources文件夾下。配置文件的內容如下:
runtime.log.logsystem.class = org.apache.velocity.runtime.log.SystemLogChute resource.loader = classpath classpath.resource.loader.class = org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
1
2
3
這些屬性配置了Velocity的日志和尋找模版的類路徑。
步驟四:創建新的文件并且生成代碼
最后,我們建立新的代碼文件并以這個文件為目標運行模版。下邊的代碼片段展示了如何如何去做上述操作:
JavaFileObject jfo = processingEnv.getFiler().createSourceFile ( fqClassName + "BeanInfo"); processingEnv.getMessager().printMessage( Diagnostic.Kind.NOTE, "creating source file: " + jfo.toUri()); Writer writer = jfo.openWriter(); processingEnv.getMessager().printMessage( Diagnostic.Kind.NOTE, "applying velocity template: " + vt.getName()); vt.merge(vc, writer); writer.close();
1
2
3
4
5
6
7
8
9
10
步驟五:打包并運行
最終,注冊注解處理器(可以回想一下在第二篇博文中的服務配置相關內容),打包處理器并且在命令行,eclipse和Maven構建項目時使用它。
假設下邊就是需要處理的類:
package example.velocity.client; import example.annotations.beaninfo.BeanInfo; @BeanInfo public class Article { @BeanInfo private String id; @BeanInfo private int department; @BeanInfo private String status; public Article() { super(); } public String getId() { return id; } public void setId(String id) { this.id = id; } public int getDepartment() { return department; } public void setDepartment(int department) { this.department = department; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } @BeanInfo public void activate() { setStatus("active"); } @BeanInfo public void deactivate() { setStatus("inactive"); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
當我們使用命令行執行編譯任務時,我們在終端上看到被注解標注的元素被找到并且BeanInfo類被生成。
Article.java:6: Note: annotated class: example.annotations.velocity.client.Article public class Article { ^ Article.java:9: Note: annotated field: id private String id; ^ Article.java:12: Note: annotated field: department private int department; ^ Article.java:15: Note: annotated field: status private String status; ^ Article.java:53: Note: annotated method: activate public void activate() { ^ Article.java:59: Note: annotated method: deactivate public void deactivate() { ^ Note: creating source file: file:/c:/projects/example.annotations.velocity.client/src/main/java/example/annotations/velocity/client/ArticleBeanInfo.java Note: applying velocity template: beaninfo.vm Note: example\annotations\velocity\client\ArticleBeanInfo.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
檢查相應的文件夾我們會發現BeanInfo類文件被創建。任務完成!
總結
在這個系列文章中,我們學習了如何使用Java6中的注解處理器框架生成源代碼:
- 我們學習了注解和注解類型的概念和他們的基本用法
- 我們學習了注解處理器的概念,還有如何編寫,以及從不同工具運行它。
- 我們大致討論了一下Model-Drive Engineer和代碼生成。
- 我們展示了如何使用注解處理器生成代碼
- 我們學習了如何使用Velocity來創建優雅的,強大的,可維護的基于注解處理器的代碼生成器。
-
(1) 如何你想詳細了解MDE,請查看這篇文件
(2) Filter的API文檔可以在這里進行查看
Java
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。