java annotation基础
简介
之前有一篇简短的文章讨论过annotation的相关概念以及它的应用。annotation是java里一个比较有意思的特性。它本身相当于对代码中一种元数据的标记。在程序编译的时候,它和没有添加annotation的代码看起来没有多少差别。那么,annotation到底是用在哪里的呢?我们一般是怎么来处理它的呢?这里针对这几个方面进一步的讨论。
几个常见的annotation
在讨论具体的annotation概念之前,我们在很多程序代码里已经接触过一些相关的annotation以及它的应用。一个典型的就是@Override。比如说当我们有如下的代码:
public class Employee { public void setSalary(double salary) { System.out.println("Employee.setSalary():" + salary); } }
如果我们要定义一个类Manager,继承类Employee:
public class Manager extends Employee { @Override public void setSalary(double salary) { System.out.println("Manager.setSalary():" + salary); } }
在子类里实现的方法setSalary前面多了一个@Override的标记。那么,这个东西有什么用呢?如果我们在代码里把这部分去掉,编译代码的时候没有任何问题。这么粗看起来好像标记的修饰没有什么用。但是我们再尝试一下将Manager里方法的签名稍微修改一下,比如将double类型参数改成int。然后再编译代码,将会发现出现如下的错误:
Manager.java:2: error: method does not override or implement a method from a supertype @Override ^
从代码逻辑上来看,既然Manager类继承了Employee类,它必须要实现一个和父类签名相同的方法,如果没有的话,编译的时候会报错。所以这里@Override标记相当于告诉编译器对声明的继承方法进行签名检查。这样可以在编译的时候发现问题。
annotation基本概念
annotation相当于是一个元数据层面的东西。它在编译的过程中会产生这些信息提供给后面的编译以及分析工具来使用。所以从这个角度来说,如果后续没有对相关annotation的分析和使用,有和没有它们编译之后的行为是没有任何变化的。
那么该如何定义一个annotation呢?以前面使用过的@Override为例,它的定义如下:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }
可见,我们定义一个annotation,它的基本形式如下:
[modifiers] @interface <annotation-type-name> { // Annotation type body }
前面是一个访问权限修饰,比如public, protected, private。后面就是我们定义的annotation名。比如Override。
所以,按照这个基本定义,我们可以定义一个类似的annotation Version:
public @interface Version { int major(); int minor(); }
在这个annotation里定义了两个元素,major和minor。这里的定义major, minor看起来好像一个典型的方法,但是它本身只是表示里面的数值。在使用这个annotation的时候,我们使用如下形式的代码:
@Version(major=1, minior=0)
可见,这里的两个属性只是元素值而不是某个特定的方法。通过这种方式来指定里面的元数据内容。
基于上面的定义,我们可以在一些如下的地方使用Version这个标记。一段示例代码如下:
@Version(major=1, minor=0) public class VersionTest { @Version(major=1, minor=0) private int xyz = 100; @Version(major=1, minor=0) public VersionTest() {} @Version(major=1, minor=0) public VersionTest(int xyz) { this.xyz = xyz; } @Version(major=1, minor=0) public void printData() {} @Version(major=1, minor=0) public void setXyz(int xyz) { @Version(major=1, minor=0) int newValue = xyz; this.xyz = xyz; } }
从这部分代码来看,我们可以将定义的annotation放到类、成员变量、构造函数、方法以及方法局部变量等地方。那么,对于annotation可以用到哪里的修饰限制,我们在后面会详细描述。
annotation类型定义限制
因为annotation属于元数据定义类型,它的定义和使用和我们普通使用的数据类型不一样。所以,它有很多特殊的地方。这些也是我们在定义和使用它们的时候需要避免的。
1. annotation类型不能继承其他annotation类型
这个类型特殊的地方在于,我们不能把它当成一个普通的java数据类型,所以如果我们采用如下形式的代码:
public @interface ExtendedVersion extends BasicVersion { int extend(); }
这部分代码在编译的时候是不能通过的。实际上,所有的annotation类型都是隐式的继承自java.lang.annotation.Annotation接口。这个接口的定义如下:
package java.lang.annotation; public interface Annotation { boolean equals(Object obj); int hashCode(); String toString(); Class<? extends Annotation> annotationType(); }
这个接口里定义的方法前3个是属于Object对象的。而后面的这个方法由java提供的proxy对象在运行时动态的生成。所以我们实际定义的annotation里没有显式的实现这个接口的定义。
2. annotation类型里定义的方法不能有任何参数
如果我们定义如下的代码:
public @interface WrongVersion { String concatenate(int major, int minor); }
这部分代码是不能编译的。为什么呢?因为annotation里定义的元素只是使得我们将一个数据值关联到某个具体的标记对象,而不是定义特殊的逻辑运算。我们可以将annotation里定义的所谓方法当成一个个的成员变量定义。只是它的定义形式有点不一样。
3. annotation里定义的方法不能有throws声明
因此,按照如下方式写的代码是不能编译通过的:
public @interface WrongVersion { int major() throws Exception; int minor(); }
4. annotation里定义的方法返回类型必须是如下几种:
1) 基本类型,像byte, short, int, long, float, double, boolean, char.
2) java.lang.String
3) java.lang.Class
4)枚举类型
5) annotation类型
6) 上述类型的数组
5. annotation类型不能声明方法
6. annotation类型不能是泛型。
meta annotation
在前面的代码示例里,还有几个比较特别的地方。就是我们看到定义@Override annotation的时候,里面居然还有@Target, @Retention等几个修饰的annotation。它们这种用来定义annotation的annotation就是元annotation。主要的几个我们都来过一下。
Target
Target主要用来描述这个annotation可以用到哪些地方。它本身的定义如下:
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Target { /** * Returns an array of the kinds of elements an annotation type * can be applied to. * @return an array of the kinds of elements an annotation type * can be applied to */ ElementType[] value(); }
这里指定的ElementType数组用来描述它应用的地方。ElementType是一个枚举类型,它的详细定义如下:
public enum ElementType { /** Class, interface (including annotation type), or enum declaration */ TYPE, /** Field declaration (includes enum constants) */ FIELD, /** Method declaration */ METHOD, /** Formal parameter declaration */ PARAMETER, /** Constructor declaration */ CONSTRUCTOR, /** Local variable declaration */ LOCAL_VARIABLE, /** Annotation type declaration */ ANNOTATION_TYPE, /** Package declaration */ PACKAGE, /** * Type parameter declaration * * @since 1.8 */ TYPE_PARAMETER, /** * Use of a type * * @since 1.8 */ TYPE_USE }
在java 9里还提供了对MODULE的支持。
假设我们定义如下的annotation:
@Target({ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD}) public @interface Version { int major(); int minor(); }
在前面的定义里,它可以被应用到类型的定义,构造函数和方法上。所以我们可以在如下的代码里使用:
@Version(major = 1, minor = 0) public class VersionTest { @Version(major = 1, minor = 0) public VersionTest() { } @Version(major = 1, minor = 1) public void doSomething() { } }
Retention
Retention主要用来描述这个annotation可以在哪个层面被访问。它本身的定义如下:
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Retention { /** * Returns the retention policy. * @return the retention policy */ RetentionPolicy value(); }
描述它的访问范围被定义在枚举类型里RetentionPolicy里:
public enum RetentionPolicy { /** * Annotations are to be discarded by the compiler. */ SOURCE, /** * Annotations are to be recorded in the class file by the compiler * but need not be retained by the VM at run time. This is the default * behavior. */ CLASS, /** * Annotations are to be recorded in the class file by the compiler and * retained by the VM at run time, so they may be read reflectively. * * @see java.lang.reflect.AnnotatedElement */ RUNTIME }
从里面自带的注释就可以看到这三种不同应用范围的差别。具体采用哪种需要根据具体应用的需要。而且不同应用范围的选取也决定了后续处理方式的不一样。我们会在后面的部分详细描述。
Inherited
Inherited表示定义annotation使用的继承关系。它也是只能用来修饰annotation。它的定义如下:
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Inherited { }
如果一个annotation的类型有Inherited修饰,那么这个annotation对象修饰的对象如果有子类的话,这个子类也会继承这个annotation实例。我们可以看一个具体的示例。
假如我们有如下的annotaion定义:
@Inherited public @interface Ann3 { int id(); }
假如我们声明两个类A和B:
@Ann3(id=707) public class A { }
public class B extends A { }
在这部分里,class B继承了@Ann3 annotation。
Documented
Documented这个annotation相对比较简单,它主要是用来支持javadoc生成文档的。当我们用它修饰某个annotation的时候,这个annotation的实例在应用到某个地方的时候,它会被javadoc生成对应的文档内容。
Repeatable
还有一个比较常用的就是Repeatable。通过它修饰的annotation可以多次修饰目的对象。它的定义如下:
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Repeatable { /** * Indicates the <em>containing annotation type</em> for the * repeatable annotation type. * @return the containing annotation type */ Class<? extends Annotation> value(); }
当然,它的用法比较特殊一点。创建一个可重复的annotation需要两个步骤:
1. 定义一个annotation类型,并且用Repeatable修饰。同时在Repeatable里设置value为另外一个annotation。这个额外的annotation里包含有需要声明为可重复的annotation.
2. 声明这个包含的annotation里一个元素是可重复annotation的数组。
一个详细的示例如下。比如我们定义一个ChangeLog的annotation:
@Retention(RetentionPolicy.RUNTIME) @Repeatable(ChangeLogs.class) public @interface ChangeLog { String date(); String comments(); }
这里声明了ChangeLog是可重复的。但是Repeatable里定义的value是ChangeLogs.class。这是包含annotation。
@Retention(RetentionPolicy.RUNTIME) public @interface ChangeLogs { ChangeLog[] value(); }
ChangeLogs里面的value方法返回的类型是一个ChangeLog的数组。这样,在程序里,我们可以来使用annotation ChangeLog:
@ChangeLog(date="08/28/2017", comments="Declared the class") @ChangeLog(date="09/21/2017", comments="Added the process() method") public class Test { public static void process() { } }
这样,我们就可以在程序里多次使用同一个annotation。
annotation processing
前面讨论完了annotation的定义之后,还有一个需要关注的问题就是如果我们程序中应用上了这些annotation之后。我们该怎么来处理它们呢?像之前我们提到过的@Override, @Test等。既然它们编译的时候产生的字节码和没有这些的没什么区别。那么为什么在程序编译或者运行的时候它们这些定义的特性又会起作用呢?
在之前的讨论里我们提到过,要定义annotation的有效范围,需要通过定义它的@Retention属性。这里定义了RetentionPolicy这个枚举类型。主要包括SOURCE, CLASS, RUNTIME这几种。其中SOURCE指的是将annotation编译的时候丢弃。但是在一些情况下它可以用来生成新的代码。而RUNTIME指的是在运行的时候
annotation将由编译器将它们加入到class文件里。在运行的时候可以通过反射的方式来访问。所以,我们再针对这两种情况的应用进一步讨论一下。
runtime level
在runtime level的情况下,我们就是需要通过在运行时用反射的方式来访问class信息,然后做各种处理。我们来看一个示例。假设我们定义有两个annotation,一个Test, 一个TestInfo:
package com.yunzero; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Retention; import java.lang.annotation.Target; @Retention(RUNTIME) @Target(METHOD) public @interface Test { public boolean enabled() default true; }
package com.yunzero; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Retention; import java.lang.annotation.Target; @Retention(RUNTIME) @Target({ TYPE }) public @interface TestInfo { public enum Priority { LOW, MEDIUM, HIGH } Priority priority() default Priority.MEDIUM; String[] tags() default ""; String createdBy() default "frank"; String lastModified() default "07/01/2018"; }
这两个annotation的定义比较直接。它们都是用在runtime上。其中Test annotation被用在method上。也就是说它只能被用来修饰定义的方法。而TestInfo annotation可以用在type上。那么它可以被用在class定义上。除了这些应用范围的定义以外,我们还定义了一些元素属性。这些不过是用来描述这些annotation对象的信息。
接着,我们再定义一个应用了这两个annotation的类:
package com.yunzero; import com.yunzero.TestInfo.Priority; @TestInfo( priority = Priority.HIGH, createdBy = "frank liu", tags = {"sales", "test"} ) public class TestExample { @Test void testA() { if (true) { throw new RuntimeException("This test always failed"); } } @Test(enabled = false) void testB() { if (false) { throw new RuntimeException("This test always passed"); } } @Test(enabled = true) void testC() { if (10 > 1) { } } }
在这个类TestExample里,在它的类定义里,我们添加了TestInfo annotation。里面定义了priority, createdBy, tags信息。而里面的三个方法testA, testB, testC都被Test annotation修饰。从这里定义的样式来看,我们这里像是模拟了一个单元测试框架的用法。
既然是runtime级别的分析处理,后面就是需要通过反射对运行时的对象进行分析了。对它们分析的代码过程如下:
package com.yunzero; import java.lang.annotation.Annotation; import java.lang.reflect.Method; public class App { public static void main(String[] args) { System.out.println("Testing..."); int passed = 0, failed = 0, count = 0, ignore = 0; Class<TestExample> obj = TestExample.class; if (obj.isAnnotationPresent(TestInfo.class)) { TestInfo testerInfo = obj.getAnnotation(TestInfo.class); System.out.printf("%nPriority :%s", testerInfo.priority()); System.out.printf("%nCreatedBy :%s", testerInfo.createdBy()); System.out.printf("%nTags :"); int tagLength = testerInfo.tags().length; for (String tag : testerInfo.tags()) { if (tagLength > 1) { System.out.print(tag + ". "); } else { System.out.print(tag); } tagLength--; } System.out.printf("%nLastModified :%s%n%n", testerInfo.lastModified()); for (Method method : obj.getDeclaredMethods()) { // if method is annotated with @Test if (method.isAnnotationPresent(Test.class)) { Annotation annotation = method.getAnnotation(Test.class); Test test = (Test) annotation; // if enabled = true (default) if (test.enabled()) { try { method.invoke(obj.newInstance()); System.out.printf("%s - Test '%s' - passed %n", ++count, method.getName()); passed++; } catch (Throwable ex) { System.out.printf("%s - Test '%s' - failed: %s %n", ++count, method.getName(), ex.getCause()); failed++; } } else { System.out.printf("%s - Test '%s' - ignored%n", ++count, method.getName()); ignore++; } } } System.out.printf("%nResult : Total : %d, Passed: %d, Failed %d, Ignore %d%n", count, passed, failed, ignore); } } }
从这里可以看到,既然TestExample是Test和TestInfo两个annotation应用上的实体,那么,对它们的分析就必须从这个实体上来。从这个角度来看,annotation果然就像是一个修饰的东西,它本身不能作为一个单独的实体来使用。
而这里分析使用情况的代码核心在于获取实体对象的class对象。也就是前面的Class<TestExample> obj = TestExample.class;这部分。其他的代码无非就是分析它是否有某些annotation以及通过反射的方式来调用某些方法并进行统计。
运行上述的代码将的到如下的输出:
Testing... Priority :HIGH CreatedBy :frank liu Tags :sales. test LastModified :07/01/2018 1 - Test 'testA' - failed: java.lang.RuntimeException: This test always failed 2 - Test 'testB' - ignored 3 - Test 'testC' - passed Result : Total : 3, Passed: 1, Failed 1, Ignore 1
所以,总的来说,基于运行时的annotation分析主要就是通过反射来分析类里面的各种成员,然后针对性的进行处理。通过这个思路,我们也可以看到一些常用的测试框架,向JUnit也是采取类似的方式来进行处理的。
source level
annotation的应用里,还有一个比较少见的用法,就是基于source level的处理。它的处理主要是通过它可以生成一些新的代码。在spring data等一些框架里就有用到。那么,这又是怎么实现的呢?
这是因为,在java里,我们可以通过在编译的过程中指定一些annotation的processor。这样在编译的过程中编译器每编译一轮之后发现有新的文件生成就会将新的文件引入而开始新一轮的编译。也就是这个特性的应用使得我们在新代码的生成中的到应用。当然,这里也有一个限制,我们只能新生成代码而不能修改原有的代码。
现在,我们来看一个示例。比如说我们看到定义的很多类里都需要定义一个toString的方法。但是给它们每个类专门去写这么一个toString的方法显得太繁琐。于是我们希望能够通过添加annotation的方式加到这些类上面去。然后再通过对annotation source级别的分析来生成它们对应的toString方法。那么,我们该怎么做呢?
首先,我们需要定义标记toString方法的annotation:
package sourceAnnotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.SOURCE) public @interface ToString { boolean includeName() default true; }
这部分定义里,我们专门标识了RetentionPolicy.SOURCE。表示它们是可以被应用到源代码级别的。它可以修饰到类和方法上。
然后,我们再将这个annotation应用到几个类上面:
package rect; import sourceAnnotations.ToString; @ToString(includeName=false) public class Point { private int x; private int y; public Point(int x, int y) { this.x = x; this.y = y; } @ToString(includeName=false) public int getX() { return x; } @ToString(includeName=false) public int getY() { return y; } }
package rect; import sourceAnnotations.ToString; @ToString public class Rectangle { private Point topLeft; private int width; private int height; public Rectangle(Point topLeft, int width, int height) { this.topLeft = topLeft; this.width = width; this.height = height; } @ToString(includeName=false) public Point getTopLeft() { return topLeft; } @ToString public int getWidth() { return width; } @ToString public int getHeight() { return height; } }
现在,如果我们采用如下的代码来运行程序的话,它是不能通过编译的:
package rect; import sourceAnnotations.ToStrings; public class SourceLevelAnnotationDemo { public static void main(String[] args) { Rectangle rect = new Rectangle(new Point(10, 10), 20, 30); System.out.println(ToStrings.toString(rect)); } }
在这部分代码里,引入了ToStrings类。但是在原有的包里是没有这个类的。因为这个类和对应的方法是需要通过我们的代码来生成。所以,现在唯一缺的就是我们生成上述ToStrings类的代码了。
在java的javax.annotation.processing.*包里有一个AbstractProcessor类,如果我们需要生成新的代码,就需要通过继承它。继承它需要实现如下的方法:
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment currentRound)
我们详细实现的代码如下:
package sourceAnnotations; import java.beans.*; import java.io.*; import java.util.*; import javax.annotation.processing.*; import javax.lang.model.*; import javax.lang.model.element.*; import javax.tools.*; import javax.tools.Diagnostic.Kind; @SupportedAnnotationTypes("sourceAnnotations.ToString") @SupportedSourceVersion(SourceVersion.RELEASE_8) public class ToStringAnnotationProcessor extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment currentRound) { if (annotations.size() == 0) return true; try { JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile("sourceAnnotations.ToStrings"); try (PrintWriter out = new PrintWriter(sourceFile.openWriter())) { out.println("// Automatically generated by sourceAnnotations.ToStringAnnotationProcessor"); out.println("package sourceAnnotations;"); out.println("public class ToStrings {"); for (Element e : currentRound.getElementsAnnotatedWith(ToString.class)) { if (e instanceof TypeElement) { TypeElement te = (TypeElement) e; writeToStringMethod(out, te); } } out.println(" public static String toString(Object obj) {"); out.println(" return java.util.Objects.toString(obj);"); out.println(" }"); out.println("}"); } } catch (IOException ex) { processingEnv.getMessager().printMessage(Kind.ERROR, ex.getMessage()); } return true; } private void writeToStringMethod(PrintWriter out, TypeElement te) { String className = te.getQualifiedName().toString(); out.println(" public static String toString(" + className + " obj) {"); ToString ann = te.getAnnotation(ToString.class); out.println(" StringBuilder result = new StringBuilder();"); if (ann.includeName()) out.println(" result.append(\"" + className + "\");"); out.println(" result.append(\"[\");"); boolean first = true; for (Element c : te.getEnclosedElements()) { String methodName = c.getSimpleName().toString(); ann = c.getAnnotation(ToString.class); if (ann != null) { if (first) first = false; else out.println(" result.append(\",\");"); if (ann.includeName()) { String fieldName = Introspector.decapitalize(methodName.replaceAll("^(get|is)", "")); // Turn getWidth into width, isDone into done, getURL into URL out.println(" result.append(\"" + fieldName + "=" + "\");"); } out.println(" result.append(toString(obj." + methodName + "()));"); } } out.println(" result.append(\"]\");"); out.println(" return result.toString();"); out.println(" }"); } }
在上述代码里,首先通过AbstractProcessor里的成员变量processingEnv来创建一个java源文件。然后再通过RoundEnvironment得到被标记的元素。如果是TypeElement,也就是我们前面定义的ToString修饰的类,那么调用writeToStringMethod.
这些代码其实很大一部分是用来生成源文件内容的,显得比较繁琐一点而已。现在既然是生成源代码的代码弄好了。我们该怎么来编译和运行程序呢?
肯定,我们首先应该编译这个生成ToStrings类的代码:
javac sourceAnnotations/ToStringAnnotationProcessor.java
在编译完这部分代码之后,我们再编译使用到ToStrings类的代码:
javac -processor sourceAnnotations.ToStringAnnotationProcessor rect/*.java
这里,我们需要在命令行里指定processor,表示用它来处理annotation相关代码的生成。在执行完这部分代码之后,我们会发现sourceAnnotations目录下生成了一个ToStrings.java的文件,它的内容如下:
// Automatically generated by sourceAnnotations.ToStringAnnotationProcessor package sourceAnnotations; public class ToStrings { public static String toString(rect.Point obj) { StringBuilder result = new StringBuilder(); result.append("["); result.append(toString(obj.getX())); result.append(","); result.append(toString(obj.getY())); result.append("]"); return result.toString(); } public static String toString(rect.Rectangle obj) { StringBuilder result = new StringBuilder(); result.append("rect.Rectangle"); result.append("["); result.append(toString(obj.getTopLeft())); result.append(","); result.append("width="); result.append(toString(obj.getWidth())); result.append(","); result.append("height="); result.append(toString(obj.getHeight())); result.append("]"); return result.toString(); } public static String toString(Object obj) { return java.util.Objects.toString(obj); } }
而且还有这个文件对应被编译后的class文件。这时候,我们再执行运行程序的命令:
java rect.SourceLevelAnnotationDemo
将看到如下的输出:
rect.Rectangle[[10,10],width=20,height=30]
这样,一个生成处理annotation代码的完整示例就完成了。
总结
annotation在我们程序的应用中比较广泛,但是又显得很不起眼。它主要是在程序里添加一些元数据信息方便我们后续的程序去处理它们。这种方式从使用者的角度来说带来了很大的便利性。很多需要生成的繁琐的代码可以通过程序来生成处理。对于annotation的定义和处理主要是runtime和source两个级别的。它们有的用于测试框架的实现,有的用于一些新代码生成和使用。这些技术特点和细节还是值得学习和深究的。
参考材料
http://www.baeldung.com/java-annotation-processing-builder
http://www.mkyong.com/java/java-custom-annotations-example/
https://www.amazon.com/Core-Java-II-Advanced-Features-10th/dp/0134177290/ref=sr_1_5?s=books&ie=UTF8&qid=1529300087&sr=1-5&keywords=core+java