使用java语言中的注解生成器生成代码
2017-05-29 21:47
399 查看
该文翻译自https://deors.wordpress.com/2011/09/26/annotation-types/
在这个系列中会:
介绍java中什么是注解
了解注解的公共使用和它的范围
了解什么是注解处理器以及它的定义角色
学习如何创建注解处理器
学习如何通过命令行,Eclipse,Maven运行注解处理器
学习如何使用注解处理器生成代码
学习如何利用一个外部的模板引擎Apache Velocity使用注解处理器生成代码
java的注解是在java语言规范第三版中介绍到的,并且在java 5中第一次实现。
通过使用注解,我们能够添加metadata元数据到我们的源代码中,如构建或发布信息,配置属性,编译行为或者质量检测。
与javadocs不同,注解是强类型,任何一个注解在classpath中都有一个对应的定义好的注解类型。此外,注解可以被定义为运行时可用,但是javadocs绝对不行。
注解可用于包,类型(classes、interfaces、enums、annotation types),变量(class、instance and local变量–包括定义在for或者while循环中的变量),构造函数,方法,参数。
没有任何元素的注解
在这种情况,括号可以省略
注解也可以包含元素,只是name-value对,被逗号隔开。支持基本类型,字符串,枚举和数组
当注解只有一个元素并且它的name是value的话,name可以省略
注释可以为某些或全部元素定义默认值。具有默认值的元素可以从注释声明中省略。
例如,假设注解类型的作者定义revision的默认值是1和reviewers的默认值是空的字符串数组,下面两个注解申明是等价的。
@Deprecated: 表明它标记的元素不应该被使用。当该标记的元素被使用的任何时候编译器会生成一个警告⚠️。它应该被javadoc沿用 @deprecated,保留在javadoc来解释
deprecated的动机。
@Override: 表明该元素是override了一个在父类中生命的元素。当编译器发现标记的元素并没有override任何元素,会生成一个警告⚠️。虽然它不是必须的,但是可以检查错误。
@SuppressWarnings 表明编译器应该Suppress一些指定的警告⚠️。
以下都可以看到很棒的例子:
Java Enterprise Edition and its main components – Enterprise JavaBeans, Java Persistence API or Web Services API’s
Spring Framework 被充分用于配置,依赖注入,以及控制反转在该框架的核心以及spring的其他项目中
Seam, Weld, Guice
Apache Struts 2
一个注解类型定义是使用@interface来替换interface
注解类型与常规的接口有一些不同:
只允许是基本数据类型、字符串、枚举、class类和数组。注意Object一般是不允许的,数组的数组是不允许的(因为每一个数组是个Object)。
注释元素的定义非常像一个method,但是记住修饰符和参数是不允许的。
定义默认值时使用
在任何类和接口中,一个枚举类型可以嵌套在注解类型定义中
@Documented: 表示标记的注解类型每次找到的被注解元素通过javadoc文档化
@Inherited(继承): 表示标记的注解类型会被子类也继承。就是,如果被标记的注解并没有出现,那它一定在父类中。该注解只适用类的继承不适用接口的实现。
JDK5.0新特性Annotation之@Inherited注解
@Retention(保留): 表示被标记注解类型要保留到什么时候。有以下几种保留策略:
CLASS 默认——包含在class文件中,但是允许时不能访问
SOURCE 当创建class文件时被编译器丢弃
RUNTIME 运行时可用
@Target: 表示该注解类型可以标注哪种元素。适用以下几种元素类型:
ANNOTATION_TYPE
CONSTRUCTOR
FIELD
LOCAL_VARIABLE
METHOD
PACKAGE
PARAMETER
TYPE
13179
以及如何运行它们
注解真的很赞。它具有定义良好的语法以及使用不同的类型来设置任何元数据或者配置。
从目前来看,与javadoc相比有优势,但是还不足以证明它的作用。那么有没有可能与注解交互并获取更多呢?当然是可以的。
在运行时,运行时保留策略的注解可以通过反射访问。 在Class类中通过方法
在编译时,注解处理器(Annotation Processors),一个专业的类,可以在编译时代码里找到的各种注解
com.sun.mirror发布了一个独立工具叫做apt(Annotation Processor Tool),可以用来写自定义处理器来处理注解以及镜像API(Mirror API)
从java 6开始,注解处理器已经通过JSR 269 (2)标注化,合并到了标准库中并且无缝集成到了javac(Java Compiler Tool)中。
虽然我们只描述java 6中新的注解处理器API,但是你可以在这里和这里以及这篇文章的一个很好的例子,都是关于之前提到的apt以及java 5中的镜像api。
一个注解处理器只不过是一个实现了
自定义处理器可以使用三个注解来配置自己:
最后,我们提供自己对
这是个不完整的类,当调用时不会做任何操作,被注册支持注解类型是
为了与注解交互,
一个包含
一个
处理这两个参数,在
使用
注册注解处理器的最简单的方法是利用标准java服务链(standard Java services mechanism):
将注解处理器打入jar包中
包含在jar文件目录
在目录下包含一个叫
在文件中写入处理器的全限定名,每行一个
java编译器以及其他工具会在提供claspath中寻找这个文件并使用注册的处理器
在我们的例子中,文件夹结构如下
打包之后,我们开始使用它
如果你把注解处理器添加到javac classpath中并使用上面java服务链方式注册,那它们可以被javac执行。
以下例子,命令会编译并使用Complexity注解处理java源文件
用来被测试的java类内容是:
当执行javac命令后,输入如下:
虽然默认的javac通常表现很好,不过还有一些选项能帮助我们运行注解处理器在更多情况。
-Akey[=value]: 用来向处理器传递选项。只有通过
-proc:{none|only}:默认情况下,javac会运行注解处理器并编译所有资源。
proc:none 没有注解处理完成,当你构建注解处理器本身时很有用
proc:only 只有注解处理完成,当你运行校验,如质量工具或标准检查时很有用
-processorpath path:用来指定注解处理器在哪以及去哪里找依赖。清楚区分项目依赖和处理器依赖(在运行时并不需要的依赖)是很有用的。
-s dir: 用来指定生产的代码放在哪里。
-processor class1[,class2,class3…]: 用来指定哪些处理器被执行。如果没有会去寻找jva服务链提供的。当我们执行限制运行注册处理器的一小部分时很有用。
在Eclipse IDE中,当你访问属性配置时,你可以发现在Java Compiler group下,有一个叫Annotation Processing的选项。
激活Annotation Processing(默认是关闭的),就可以通过选项页的表格出入处理器了。
此外,可以通过Factory Path选项执行选择的处理器。
只要配置一次,每次构建行为被触发,就会执行注解处理了
这种自动化水平,可以帮助我们无缝集成所以处理类型。标准验证或者代码生成在整个项目生命周期中作为一个单独的过程。它还可以通过连续集成引擎(Continuous Integration engines)无缝集成。
虽然把注解处理器集成到Maven构建中有很多方式,我们建议使用这里描述的方式。描述基于Mojo(maven plug-in),Mojo是用来关注编译任务。
集成Maven需要我们的注解和注解处理器作为Maven工件可用。
我们建议将注解和注解处理器分成不同的工件。因为注解处理器在其他客户端项目不需要访问,可以减少跨项目依赖的数量。
通过这种方法,我们设置三个不同的项目,对应每一个不同的Maven工件。
注解工件(Annotation artifact): 只包含注解类型
注解处理器工件(Annotation Processor artifact):包含注解处理器。他将依赖注解工件,编译器插件需要被设置成
客户端工程:包含了客户端代码,它依赖上面两个工件。
下面是注解工件的文件夹结构以及pom文件
注意: Maven compiler plug-in的版本用来构建该工件
下面是注解处理器工件的文件夹结构以及pom文件
注意:
打包处理器服务文件夹
Maven compiler plug-in的版本
proc:none选项
依赖注解工件
最后,是客户端工程的文件夹结构和pom文件
一旦做好,每次在Maven构建执行时,处理器也会按照预期执行。
生成代码容易,生成对的代码就难了。并且用一个优雅又有效的方式是一件很麻烦的任务。
幸运的是,在过去几年Model-Driven Engineering(MDE,模型驱动工程,有的引用是Model-Driven Development模型驱动开发或者Model-Driven Architecture,模型驱动架构)已经帮助发展成了一种成熟的技术。
Model-Drive Engineering(MDE)远不止是生成代码,虽然我们认为它是MDE方法的自然切入点。
注解处理器就是我们可以用来生成源代码工具之一。
MDE的支柱之一是构造抽象(construction of abstractions)。我们给软件系统建模(model),希望通过不同的方法来建立各个层次的细节。当一个抽象层被建模,我们进行下一层的建模,下一层再下一层,最后以一个完成的可部署的产品结束。
在这个背景下,无论我们使用多少层细节,一个模型只不过用来代表我们系统的抽象。
而meta-model则是我们用来编写模型的规则。你可以把它理解为模型的scheme或者语义
我们可以利用这个模型来生成配置文件或者编写被现有代码驱动的新的源文件。比如:从一个被注解的对象创建一个远程代理或者一个数据访问对象。
这种方式的核心就是注解处理器。一个处理器可以读在源代码中找到的注解,提取这个模型,然后通过模型做任何我们想做的事(如:打开一个文件,传入内容)。java编译器会小心验证模型。
如果我们要写生成器,
下文例子展示了在注解生成器中如何创建java源文件。生成的class名字是被注解的class名字加上“BeanInfo”。
我们将从注解获取需要的信息(model模块)和写生成文件的逻辑(view逻辑)混合在了一起。
用这种方式写一个体面的生成器是很困难的。如果我们需要一些更复杂的任务是很麻烦,容易出错且很难维护的。
因此,我们需要一个更优雅的方式:
将model和view明确分离
使用模板来减轻写生成文件的任务
我们来看看如何使用Apache Velocity来像我们想要的方式生成。
Velocity在MVC模式下提供view或者作为将数据转换成XML的XSLT的替代。
Velocity有它自己的语言,叫做Velocity Template Language (VTL)。这是生产丰富且易于阅读的模板的关键。在VTL中,我们可以通过简单且直观的方式定义变量,控制流和迭代以及访问java对象中包含的信息。
以下是一个Velocity模板的片段:
正如你所见,VTL是很简单且容易理解的。粗体高亮的部分
编写用于生成代码的模板
注解处理器会从round environment读取被注解的元素,并且存储在易于访问的java对象中。一个map用于字段(fields),一个map用于方法(methods),类和包名等等。
注解处理器将初始化Velocity上下文(context)
注解处理器将加载Velocity模板
注解处理器将创造源文件(使用过滤器filter)并通过context向Velocity模板传递一个writer
Velocity引擎将生成源代码
通过这些步骤你会发现处理器/生成器代码很清楚,结构良好并且易于理解和维护。
我们来一步一步操作
让我们创建一个文件,名字叫
注意:要让这个模板起作用,我们需要给Velocity传入以下信息:
packageName 生成类的全包名
className 生成类的名字
fields字段 包含源class中字段的集合
simpleName 字段的名字
type 类型(例子中没有用到)
description 自己的解释(例子中没有用到)
…
methods 包含源class中方法的集合
simpleName 方法的名字
arguments 方法的参数(例子中没有用到)
returnType 返回类型(例子中没有用到)
description 自己的解释(例子中没有用到)
…
所有这些信息(model)会在源class中找到的注解提取出来,并存储在javabean中传递给Velocity
process方法需要从注解和源class本身中提取建立model所需的信息。你可以使用JavaBean存储尽可能多的需要的信息,但是在我们的例子中我们会用
velocity配置文件,在例子中叫
这里为Velocity设置了一个log的属性以及一个基于classpath的资源loader来寻找模板
假设下面是客户端类:
当我们javac命令出问题时,我们可以在终端中看到注解元素没有找到和BeanInfo类被生成
如果我们检查源目录会找到期望的BeanInfo类,任务完成。
我们学习了什么是注解和注解类型以及他们的基本使用
我们学习了什么是注解处理器,如何编写他们以及使用不同的工具执行它,如java编译器,Eclipse和Maven
我们讨论一点Model-Drive Engineering和代码生成的事
我们展示了如何通过java编译器,如何创建代码生成器并集成的
我们展示了如何利用现有的生成器框架如(Apache Velocity)来创建优雅,强大易于维护的代码生成器
现在是时候运用到你的项目中了,考虑生成。
Code Generation using Annotation Processors in the Java language – part 1: Annotation Types 注解类型
这篇帖子我会开始关于使用java语言中注解处理器来代码生成系列文章,它是多么强大,并在最后展示如何在编译时使用它们生成代码。在这个系列中会:
介绍java中什么是注解
了解注解的公共使用和它的范围
了解什么是注解处理器以及它的定义角色
学习如何创建注解处理器
学习如何通过命令行,Eclipse,Maven运行注解处理器
学习如何使用注解处理器生成代码
学习如何利用一个外部的模板引擎Apache Velocity使用注解处理器生成代码
java的注解是在java语言规范第三版中介绍到的,并且在java 5中第一次实现。
通过使用注解,我们能够添加metadata元数据到我们的源代码中,如构建或发布信息,配置属性,编译行为或者质量检测。
与javadocs不同,注解是强类型,任何一个注解在classpath中都有一个对应的定义好的注解类型。此外,注解可以被定义为运行时可用,但是javadocs绝对不行。
注解语法
注解通常会出现在被注解代码段的前面,通常在自己的行,缩进到同一位置。注解可用于包,类型(classes、interfaces、enums、annotation types),变量(class、instance and local变量–包括定义在for或者while循环中的变量),构造函数,方法,参数。
没有任何元素的注解
@Override() public void theMethod() {…}
在这种情况,括号可以省略
@Override public void theMethod() {…}
注解也可以包含元素,只是name-value对,被逗号隔开。支持基本类型,字符串,枚举和数组
@Author(name = "Albert", created = "17/09/2010", revision = 3, reviewers = {"George", "Fred"}) public class SimpleAnnotationsTest {…}
当注解只有一个元素并且它的name是value的话,name可以省略
@WorkProduct("WP00000182") @Complexity(ComplexityLevel.VERY_SIMPLE) public class SimpleAnnotationsTest {…}
注释可以为某些或全部元素定义默认值。具有默认值的元素可以从注释声明中省略。
例如,假设注解类型的作者定义revision的默认值是1和reviewers的默认值是空的字符串数组,下面两个注解申明是等价的。
@Author(name = "Albert", created = "17/09/2010", revision = 1, reviewers = {}) public class SimpleAnnotationsTest() {…} @Author(name = "Albert", // defaults are revision 1 created = "17/09/2010") // and no reviewers public class SimpleAnnotationsTest() {…}
注解的典型用途
在java语言规范(JLS)中定义了三个注解类型,它们被java编译器使用:@Deprecated: 表明它标记的元素不应该被使用。当该标记的元素被使用的任何时候编译器会生成一个警告⚠️。它应该被javadoc沿用 @deprecated,保留在javadoc来解释
deprecated的动机。
@Override: 表明该元素是override了一个在父类中生命的元素。当编译器发现标记的元素并没有override任何元素,会生成一个警告⚠️。虽然它不是必须的,但是可以检查错误。
@SuppressWarnings 表明编译器应该Suppress一些指定的警告⚠️。
以下都可以看到很棒的例子:
Java Enterprise Edition and its main components – Enterprise JavaBeans, Java Persistence API or Web Services API’s
Spring Framework 被充分用于配置,依赖注入,以及控制反转在该框架的核心以及spring的其他项目中
Seam, Weld, Guice
Apache Struts 2
注解类型
注解类型是用来定义自定义注解的特定接口一个注解类型定义是使用@interface来替换interface
public @interface Author { String name(); String created(); int revision() default 1; String[] reviewers() default {}; } public @interface Complexity { ComplexityLevel value() default ComplexityLevel.MEDIUM; } public enum ComplexityLevel { VERY_SIMPLE, SIMPLE, MEDIUM, COMPLEX, VERY_COMPLEX; }
注解类型与常规的接口有一些不同:
只允许是基本数据类型、字符串、枚举、class类和数组。注意Object一般是不允许的,数组的数组是不允许的(因为每一个数组是个Object)。
注释元素的定义非常像一个method,但是记住修饰符和参数是不允许的。
定义默认值时使用
default关键字跟在value后面。
在任何类和接口中,一个枚举类型可以嵌套在注解类型定义中
public @interface Complexity { public enum Level { VERY_SIMPLE, SIMPLE, MEDIUM, COMPLEX, VERY_COMPLEX; } …
用来定义注解的注解
JDK自带的一些注解用来修改注解类型的行为:@Documented: 表示标记的注解类型每次找到的被注解元素通过javadoc文档化
@Inherited(继承): 表示标记的注解类型会被子类也继承。就是,如果被标记的注解并没有出现,那它一定在父类中。该注解只适用类的继承不适用接口的实现。
JDK5.0新特性Annotation之@Inherited注解
@Retention(保留): 表示被标记注解类型要保留到什么时候。有以下几种保留策略:
CLASS 默认——包含在class文件中,但是允许时不能访问
SOURCE 当创建class文件时被编译器丢弃
RUNTIME 运行时可用
@Target: 表示该注解类型可以标注哪种元素。适用以下几种元素类型:
ANNOTATION_TYPE
CONSTRUCTOR
FIELD
LOCAL_VARIABLE
METHOD
PACKAGE
PARAMETER
TYPE
Code Generation using Annotation Processors in the Java language – part 2: Annotation Processors 注解处理器
在第一部分中我们介绍了java语言中什么是注解以及它们的基本使用。在第二部分中我们介绍注解处理器,如何构建它们13179
以及如何运行它们
注解真的很赞。它具有定义良好的语法以及使用不同的类型来设置任何元数据或者配置。
从目前来看,与javadoc相比有优势,但是还不足以证明它的作用。那么有没有可能与注解交互并获取更多呢?当然是可以的。
在运行时,运行时保留策略的注解可以通过反射访问。 在Class类中通过方法
getAnnotation()和
getAnnotations()来完成。
在编译时,注解处理器(Annotation Processors),一个专业的类,可以在编译时代码里找到的各种注解
注解处理器 API
当注解在java 5中被介绍的时候,注解处理器的API并没有成熟或者标准化。在com.sun.mirror发布了一个独立工具叫做apt(Annotation Processor Tool),可以用来写自定义处理器来处理注解以及镜像API(Mirror API)
从java 6开始,注解处理器已经通过JSR 269 (2)标注化,合并到了标准库中并且无缝集成到了javac(Java Compiler Tool)中。
虽然我们只描述java 6中新的注解处理器API,但是你可以在这里和这里以及这篇文章的一个很好的例子,都是关于之前提到的apt以及java 5中的镜像api。
一个注解处理器只不过是一个实现了
javax.annotation.processing.Processor接口的类并遵守一些规定。
javax.annotation.processing.AbstractProcessor提供的类抽象实现了公共方法,更方便自定义处理器。
自定义处理器可以使用三个注解来配置自己:
javax.annotation.processing.SupportedAnnotationTypes:该注解用来注册处理器支持的注解。有效值是注解类型的完全限定名称——支持通配符
javax.annotation.processing.SupportedSourceVersion:该注解用来注册注解处理器支持的source版本
javax.annotation.processing.SupportedOptions:该注解用来注册支持的自定义选项,可以通过命令行传入
最后,我们提供自己对
process()方法的实现。
写自己的第一个注解处理器
根据第一部分的介绍,我们创建一个类来处理Complexity annotation。package sdc.assets.annotations.processors; import … @SupportedAnnotationTypes("sdc.assets.annotations.Complexity") @SupportedSourceVersion(SourceVersion.RELEASE_6) public class ComplexityProcessor extends AbstractProcessor { public ComplexityProcessor() { super(); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { return true; } }
这是个不完整的类,当调用时不会做任何操作,被注册支持注解类型是
sdc.assets.annotations.Complexity。因此,每次当编译器找到一个类被该类型注解就会执行该处理器,提供的process在classpath中可以用了。
为了与注解交互,
process()方法接收两个参数:
一个包含
java.lang.model.TypeElement对象的set集合:注解处理是在一轮或多轮后完成。在每一轮中,处理器被调用并接收在本轮需要被处理的注解类型。
一个
javax.annotation.processing.RoundEnvironment对象:通过该对象可以访问本轮或前一轮被处理的被注解的资源元素。
处理这两个参数,在
processingEnv实力变量中有一个
ProcessingEnvironment可以使用。该对象可以访问log以及一些工具类。后面会讨论一些。
使用
RoundEnvironment对象以及
Element接口的反射方法,我们写一个简单的处理器的实现,可以打印complexity每一个注解的元素。
for (Element elem : roundEnv.getElementsAnnotatedWith(Complexity.class)) { Complexity complexity = elem.getAnnotation(Complexity.class); String message = "annotation found in " + elem.getSimpleName() + " with complexity " + complexity.value(); processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, message); } return true; // no further processing of this annotation type
打包并注册注解处理器
完成注解处理器的最后一步是打包并注册它,让java编译器或者其他工具可以找到它。注册注解处理器的最简单的方法是利用标准java服务链(standard Java services mechanism):
将注解处理器打入jar包中
包含在jar文件目录
META-INF/services下
在目录下包含一个叫
javax.annotation.processing.Processor的文件
在文件中写入处理器的全限定名,每行一个
java编译器以及其他工具会在提供claspath中寻找这个文件并使用注册的处理器
在我们的例子中,文件夹结构如下
打包之后,我们开始使用它
通过javac运行处理器
想象一下你有一个java项目使用了一些自定义注解并且有相关的注解处理器。在java 5时,编译和处理时两个不同的步骤(使用两个不同的工具),但是在java 6下所有任务都集成到java编译器中(javac)。如果你把注解处理器添加到javac classpath中并使用上面java服务链方式注册,那它们可以被javac执行。
以下例子,命令会编译并使用Complexity注解处理java源文件
>javac -cp sdc.assets.annotations-1.0-SNAPSHOT.jar; sdc.assets.annotations.processors-1.0-SNAPSHOT.jar SimpleAnnotationsTest.java
用来被测试的java类内容是:
package sdc.startupassets.annotations.base.client; import ... @Complexity(ComplexityLevel.VERY_SIMPLE) public class SimpleAnnotationsTest { public SimpleAnnotationsTest() { super(); } @Complexity() // this annotation type applies also to methods // the default value 'ComplexityLevel.MEDIUM' is assumed public void theMethod() { System.out.println("consoleut"); } }
当执行javac命令后,输入如下:
虽然默认的javac通常表现很好,不过还有一些选项能帮助我们运行注解处理器在更多情况。
-Akey[=value]: 用来向处理器传递选项。只有通过
SupportedOptions注册的选项才有用
-proc:{none|only}:默认情况下,javac会运行注解处理器并编译所有资源。
proc:none 没有注解处理完成,当你构建注解处理器本身时很有用
proc:only 只有注解处理完成,当你运行校验,如质量工具或标准检查时很有用
-processorpath path:用来指定注解处理器在哪以及去哪里找依赖。清楚区分项目依赖和处理器依赖(在运行时并不需要的依赖)是很有用的。
-s dir: 用来指定生产的代码放在哪里。
-processor class1[,class2,class3…]: 用来指定哪些处理器被执行。如果没有会去寻找jva服务链提供的。当我们执行限制运行注册处理器的一小部分时很有用。
通过Eclipse运行处理器
Eclipse IDE以及其他主流IDE都支持注解处理器并集成进了他们的常规构建过程中。在Eclipse IDE中,当你访问属性配置时,你可以发现在Java Compiler group下,有一个叫Annotation Processing的选项。
激活Annotation Processing(默认是关闭的),就可以通过选项页的表格出入处理器了。
此外,可以通过Factory Path选项执行选择的处理器。
只要配置一次,每次构建行为被触发,就会执行注解处理了
通过Maven运行处理器
注解处理器也可以集成在Apache Maven构建中执行。这种自动化水平,可以帮助我们无缝集成所以处理类型。标准验证或者代码生成在整个项目生命周期中作为一个单独的过程。它还可以通过连续集成引擎(Continuous Integration engines)无缝集成。
虽然把注解处理器集成到Maven构建中有很多方式,我们建议使用这里描述的方式。描述基于Mojo(maven plug-in),Mojo是用来关注编译任务。
集成Maven需要我们的注解和注解处理器作为Maven工件可用。
我们建议将注解和注解处理器分成不同的工件。因为注解处理器在其他客户端项目不需要访问,可以减少跨项目依赖的数量。
通过这种方法,我们设置三个不同的项目,对应每一个不同的Maven工件。
注解工件(Annotation artifact): 只包含注解类型
注解处理器工件(Annotation Processor artifact):包含注解处理器。他将依赖注解工件,编译器插件需要被设置成
proc:none所以在构建该工件时注解处理器不执行。
客户端工程:包含了客户端代码,它依赖上面两个工件。
下面是注解工件的文件夹结构以及pom文件
注意: Maven compiler plug-in的版本用来构建该工件
下面是注解处理器工件的文件夹结构以及pom文件
注意:
打包处理器服务文件夹
Maven compiler plug-in的版本
proc:none选项
依赖注解工件
最后,是客户端工程的文件夹结构和pom文件
一旦做好,每次在Maven构建执行时,处理器也会按照预期执行。
Code Generation using Annotation Processors in the Java language – part 3: Generating Source Code 生成源代码
第三部分我们将介绍如何谁用注解处理器生成源代码生成代码容易,生成对的代码就难了。并且用一个优雅又有效的方式是一件很麻烦的任务。
幸运的是,在过去几年Model-Driven Engineering(MDE,模型驱动工程,有的引用是Model-Driven Development模型驱动开发或者Model-Driven Architecture,模型驱动架构)已经帮助发展成了一种成熟的技术。
Model-Drive Engineering(MDE)远不止是生成代码,虽然我们认为它是MDE方法的自然切入点。
注解处理器就是我们可以用来生成源代码工具之一。
在MDE中的模型和元模型
在进入如何使用注解处理器生成代码之前,我们想提出几个概念:模型(model)和元模型(meta-model)MDE的支柱之一是构造抽象(construction of abstractions)。我们给软件系统建模(model),希望通过不同的方法来建立各个层次的细节。当一个抽象层被建模,我们进行下一层的建模,下一层再下一层,最后以一个完成的可部署的产品结束。
在这个背景下,无论我们使用多少层细节,一个模型只不过用来代表我们系统的抽象。
而meta-model则是我们用来编写模型的规则。你可以把它理解为模型的scheme或者语义
使用注解处理器生成源代码
如我们所见,注解是一个很棒的方式来定义一个meta-model并创建一个model。注解类型扮演meta-model的角色,一系列注解的代码扮演model的角色。我们可以利用这个模型来生成配置文件或者编写被现有代码驱动的新的源文件。比如:从一个被注解的对象创建一个远程代理或者一个数据访问对象。
这种方式的核心就是注解处理器。一个处理器可以读在源代码中找到的注解,提取这个模型,然后通过模型做任何我们想做的事(如:打开一个文件,传入内容)。java编译器会小心验证模型。
过滤器(Filter)
在第二部分讨论过,每个处理器可以访问一些有用的工具。其中一个就是过滤器。javax.annotation.processing.Filer接口定义了一些方法,可以创建源文件,class文件,或者通用资源。通过过滤器可以确保使用正确的目录并且在我们的文件系统中不会丢失生成的有用项目。
如果我们要写生成器,
-d和
-s选项在javac或者在Maven pom文件中配置是很重要的。
下文例子展示了在注解生成器中如何创建java源文件。生成的class名字是被注解的class名字加上“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
不要像我兄弟一样创建
上一个例子简单有趣,但是很混乱。我们将从注解获取需要的信息(model模块)和写生成文件的逻辑(view逻辑)混合在了一起。
用这种方式写一个体面的生成器是很困难的。如果我们需要一些更复杂的任务是很麻烦,容易出错且很难维护的。
因此,我们需要一个更优雅的方式:
将model和view明确分离
使用模板来减轻写生成文件的任务
我们来看看如何使用Apache Velocity来像我们想要的方式生成。
VELOCITY历史简介
Velocity,来自Apache Software Foundation的一个项目,是一个java的模板引擎,可以通过混合模板和java对象的数据生成所有类型的文本文件。Velocity在MVC模式下提供view或者作为将数据转换成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;
正如你所见,VTL是很简单且容易理解的。粗体高亮的部分
#foreach($field in $fields),你可以看到两个典型的VTL构造:迭代一个对象集合和打印集合中找到的元素的一些属性。
VELOCITY生成器的方法
现在我们决定使用VELOCITY来加强我们的生成器,我们需要按照以下的计划重新设计。编写用于生成代码的模板
注解处理器会从round environment读取被注解的元素,并且存储在易于访问的java对象中。一个map用于字段(fields),一个map用于方法(methods),类和包名等等。
注解处理器将初始化Velocity上下文(context)
注解处理器将加载Velocity模板
注解处理器将创造源文件(使用过滤器filter)并通过context向Velocity模板传递一个writer
Velocity引擎将生成源代码
通过这些步骤你会发现处理器/生成器代码很清楚,结构良好并且易于理解和维护。
我们来一步一步操作
Step1 :写模板
为了简单起见,我们不打算展示全部的BeanInfo生成器,只有建立我们处理器需要的字段和方法。让我们创建一个文件,名字叫
beaninfo.vm,并将它放在Maven处理器工件的
src/main/resources位置。以下是模板内容:
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, <code>null</code> 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 }
注意:要让这个模板起作用,我们需要给Velocity传入以下信息:
packageName 生成类的全包名
className 生成类的名字
fields字段 包含源class中字段的集合
simpleName 字段的名字
type 类型(例子中没有用到)
description 自己的解释(例子中没有用到)
…
methods 包含源class中方法的集合
simpleName 方法的名字
arguments 方法的参数(例子中没有用到)
returnType 返回类型(例子中没有用到)
description 自己的解释(例子中没有用到)
…
所有这些信息(model)会在源class中找到的注解提取出来,并存储在javabean中传递给Velocity
Step 2: 读取model的处理器
让我们创建一个处理器而且不要忘记注解它来处理一个BeanInfo注解类型@SupportedAnnotationTypes("example.annotations.beaninfo.BeanInfo") @SupportedSourceVersion(SourceVersion.RELEASE_6) public class BeanInfoProcessor extends AbstractProcessor {
process方法需要从注解和源class本身中提取建立model所需的信息。你可以使用JavaBean存储尽可能多的需要的信息,但是在我们的例子中我们会用
javax.lang.model.element类型,因为我们没有机会向Velocity传递太多的细节(但是如果有必要,我们会创建一个完整的beaninfo生成器)。
String fqClassName = null; String className = null; String packageName = null; Map<String, VariableElement> fields = new HashMap<String, VariableElement>(); Map<String, ExecutableElement> methods = new HashMap<String, ExecutableElement>(); 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); } }
Step 3: 初始化Velocity context并加载模板
以下代码片段展示了如何初始化Volecity context以及加载模板: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");
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
这里为Velocity设置了一个log的属性以及一个基于classpath的资源loader来寻找模板
Step 4:创建新的source并生成内容
最后,让我们创建一个源文件并使用这个新文件作为目标运行模板。以下片段展示了如何操作: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();
Step 5: 打包并运行
最后,注册处理器(像在第二部分中service配置文件那样),打包并从客户端的命令行、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"); } }
当我们javac命令出问题时,我们可以在终端中看到注解元素没有找到和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.
如果我们检查源目录会找到期望的BeanInfo类,任务完成。
总结
通过这个系列,我们学习到了如何使用java6中的注解生成器框架来生成源代码我们学习了什么是注解和注解类型以及他们的基本使用
我们学习了什么是注解处理器,如何编写他们以及使用不同的工具执行它,如java编译器,Eclipse和Maven
我们讨论一点Model-Drive Engineering和代码生成的事
我们展示了如何通过java编译器,如何创建代码生成器并集成的
我们展示了如何利用现有的生成器框架如(Apache Velocity)来创建优雅,强大易于维护的代码生成器
现在是时候运用到你的项目中了,考虑生成。
相关文章推荐
- Java语言使用注解处理器生成代码——第二部分:注解处理器
- Java语言使用注解处理器生成代码 —— 第一部分:注解类型
- Java语言使用注解处理器生成代码——第三部分:生成源代码
- 使用java的wsimport.exe工具生成wsdl的客户端代码
- 使用idea和wsdl生成java代码
- 使用FreeMarker模板生成java代码的例子
- [代码生成] 使用Java与XSLT的10条技巧
- 使用java的wsimport.exe工具生成wsdl的客户端代码
- 使用java代码生成随机验证码
- Java使用Flying Saucer实现HTML代码生成PDF文档
- Java使用wkhtmltox实现HTML代码生成PDF文档或者图片
- 使用注解简化Java开发中的样板代码——Lombok框架
- [编写高质量代码:改善java程序的151个建议]建议91 枚举和注解结合使用威力更大
- 使用xsl来动态生成java代码
- java使用JNI调用C++代码(vs2010生成dll文件)
- 使用Freemarker模板生成JAVA代码
- 使用XJC生成Java类添加注解
- 使用cxf生成webservice java代码
- [译]使用注解处理器生成代码-3 生成源代码
- CXF学习之旅(三) - 使用Maven根据WSDL生成生成Java代码