[译]使用注解处理器生成代码-3 生成源代码

网友投稿 665 2022-05-30

本博文原文地址摸我

本篇博文是关于使用注解处理器生成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。

步骤二:注解处理器读取信息

[译]使用注解处理器生成代码-3 生成源代码

让我们来实现一个注解处理器,并且注解它支持处理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 fields = new HashMap(); Map methods = new HashMap(); for (Element e : roundEnv. getElementsAnnotatedWith(BeanInfo.class)) { if (e.getKind() == ElementKind.CLASS) { TypeElement classElement = (TypeElement) e; PackageElement packageElement = (PackageElement) classElement.getEnclosingElement(); processingEnv.getMessager().printMessage( Diagnostic.Kind.NOTE, "annotated class: " + classElement.getQualifiedName(), e); fqClassName = classElement.getQualifiedName(). toString(); className = classElement.getSimpleName().toString(); packageName = packageElement.getQualifiedName(). toString(); } else if (e.getKind() == ElementKind.FIELD) { VariableElement varElement = (VariableElement) e; processingEnv.getMessager().printMessage( Diagnostic.Kind.NOTE, "annotated field: " + varElement.getSimpleName(), e); fields.put(varElement.getSimpleName().toString(), varElement); } else if (e.getKind() == ElementKind.METHOD) { ExecutableElement exeElement = (ExecutableElement) e; processingEnv.getMessager().printMessage( Diagnostic.Kind.NOTE, "annotated method: " + exeElement.getSimpleName(), e); methods.put(exeElement.getSimpleName().toString(), exeElement); }

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小时内删除侵权内容。

上一篇:Kubernetes手记(4)- 命令入门
下一篇:2021最新版SpringCloud高频面试题分享
相关文章