Annotations是一种元数据,其作用在于提供程序本身以外的一些数据信息,也就是说Annotation他不会属于程序代码本身,不参与逻辑运算,故而不会对原程序代码的操作产生直接的影响。
为编译器提供辅助信息 — Annotations可以为编译器提供而外信息,以便于检测错误,抑制警告等. 编译源代码时进行而外操作 — 软件工具可以通过处理Annotation信息来生成原代码,xml文件等等. 运行时处理 — 有一些annotation甚至可以在程序运行时被检测,使用. 元注解 元注解的作用就是负责注解其他注解。它们被用来提供对其它 annotation类型作说明。Java定义的元注解:
@Target
@Retention
@Documented
@Inherited
@Native
@Repeatable
@Target
@Target
指明了注解修饰的对象范围。@Target
接受参数类型为 java.lang.annotation.ElementType。ElementType 包含以下Enum 值:
ANNOTATION_TYPE: 用于描述注解类型 CONSTRUCTOR: 用于描述构造器 FIELD: 用于描述域,包括Enum常量 LOCAL_VARIABLE: 用于描述局部变量 METHOD: 用于描述方法 PACKAGE: 用于描述包 PARAMETER: 用于描述参数 TYPE: 用于描述类,接口(包括注解类型)或Enum类型 TYPE_PARAMETER: 用于描述泛型 TYPE_USE: 用于描述类型检查 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Target(ElementType.TYPE) public @interface Table { public String tableName () default "className" ; } @Target(ElementType.FIELD) public @interface NoDBColumn {}
@Retention
@Retention
定义了该Annotation被保留的时间长短:某些Annotation仅出现在源代码中,而被编译器丢弃;而另一些却被编译在class文件中;编译在class文件中的Annotation可能会被虚拟机忽略,而另一些在class被装载时将被读取(请注意并不影响class的执行,因为Annotation与class在使用上是被分离的)。使用这个meta-Annotation可以对 Annotation的“生命周期”限制。取值(java.lang.annotation.RetentionPolicy)有:
SOURCE:在源文件中有效(即源文件保留) CLASS:在class文件中有效(即class保留) RUNTIME:在运行时有效(即运行时保留) 1 2 3 4 5 6 7 8 9 10 11 @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Column { public String name () default "fieldName" ; public String setFuncName () default "setField" ; public String getFuncName () default "getField" ; public boolean defaultDBValue () default false ; }
@Documented
@Documented
注解表示每当使用指定的注解时,使用Javadoc工具记录这些元素。默认情况下,注解不包括在Javadoc中。 @Documented
是一个标记注解,没有成员。
1 2 3 4 5 6 7 8 9 @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Column { public String name () default "fieldName" ; public String setFuncName () default "setField" ; public String getFuncName () default "getField" ; public boolean defaultDBValue () default false ; }
@Inherited
@Inherited
元注解是一个标记注解,@Inherited
表示被标注的注解是可被继承的。如果一个使用@Inherited
修饰的注解被用于一个class,则这个annotation将被用于该class的子类。
1 2 3 4 5 6 7 @Target({ElementType.METHOD, ElementType.TYPE}) @Inherited public @interface Greeting { public enum FontColor { BULE,RED,GREEN}; String name () ; FontColor fontColor () default FontColor.GREEN; }
注意,@Inherited
只能让子类获取父类标记在Type上的注解。如果注解标记在父类方法上,子类覆盖的方法无法获取到注解。如果子类实现一个或多个接口,也不会从它实现的接口继承任何注释。
想实现方法上注解的继承,你可以通过反射在继承链上找到方法上的注解,例如Spring提供了AnnotationUtils用于处理Annotation。
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 public static <A extends Annotation > A getInheritedAnnotation ( Class<A> annotationClass, AnnotatedElement element) { A annotation = element.getAnnotation(annotationClass); if (annotation == null && element instanceof Method) annotation = getOverriddenAnnotation(annotationClass, (Method) element); return annotation; } private static <A extends Annotation > A getOverriddenAnnotation ( Class<A> annotationClass, Method method) { final Class<?> methodClass = method.getDeclaringClass(); final String name = method.getName(); final Class<?>[] params = method.getParameterTypes(); final Class<?> superclass = methodClass.getSuperclass(); if (superclass != null ) { final A annotation = getOverriddenAnnotationFrom(annotationClass, superclass, name, params); if (annotation != null ) return annotation; } for (final Class<?> intf : methodClass.getInterfaces()) { final A annotation = getOverriddenAnnotationFrom(annotationClass, intf, name, params); if (annotation != null ) return annotation; } return null ; } private static <A extends Annotation > A getOverriddenAnnotationFrom ( Class<A> annotationClass, Class<?> searchClass, String name, Class<?>[] params) { try { final Method method = searchClass.getMethod(name, params); final A annotation = method.getAnnotation(annotationClass); if (annotation != null ) return annotation; return getOverriddenAnnotation(annotationClass, method); } catch (final NoSuchMethodException e) { return null ; } }
@Native
用于表示可从Native方法获取的常量属性。
@Repeatable
表示标记的注释可以多次应用于同一声明或类型使用,即重复注解。
重复注解示例 1 2 3 4 @Schedule(dayOfMonth="last") @Schedule(dayOfWeek="Fri", hour="23") public void doPeriodicCleanup () { ... }
为了实现重复注解,需要下面两步:
声明一个可重复注解类型 1 2 3 4 5 6 7 8 import java.lang.annotation.Repeatable;@Repeatable(Schedules.class) public @interface Schedule { String dayOfMonth () default "first" ; String dayOfWeek () default "Mon" ; int hour () default 12 ; }
@Repeatable
注解的值是注解容器的类型,java 编译器用它来存储重复注解。
声明该类型注解容器 1 2 3 public @interface Schedules { Schedule[] value(); }
注解容器必须拥有数组类型的变量属性,数组元素类型必须是重复注解的类型。
Java 自身使用的注解 在 java.lang 中使用了下面5个注解:
@Deprecated
@Deprecated
注解表示标记的元素已被弃用,不应再使用。
使用例子:
1 2 3 4 5 6 / ** * @deprecated * 解释为什么它被弃用 * / @Deprecated static void deprecatedMethod(){}
@Override
@Override
注解表示意图覆盖超类中声明的元素。
1 2 3 @Override int overriddenMethod(){}
@SuppressWarnings
@SuppressWarnings
注释告诉编译器压制它将会生成的特定警告。
1 2 3 4 5 6 @SuppressWarnings (“deprecation”)void useDeprecatedMethod(){ objectOne.deprecatedMethod(); }
@SafeVarargs
@SafeVarargs
注解在应用于方法或构造函数时,声明该代码对其参数不执行潜在的不安全操作。
@FunctionalInterface
@FunctionalInterface
注解,在Java SE 8引入,表示该类型声明意在成为功能的接口,由Java语言规范所定义的。
自定义运行时注解 java 自定义注解分为两种, 运行时注解和编译期注解。
@Retention(RetentionPolicy.RUNTIME)
表示运行时保留的注解。我们可以在运行时,通过java反射机制,获取到注解信息。
接口AnnotatedElement表示程序中的注解元素,可以使用反射读取。接口中返回的所有注解都是不可变和可序列化的,接口方法返回的数组可以被修改,但不会影响原有的注解。接口的getAnnotationsByType(Class)和getDeclaredAnnotationsByType(Class)方法支持重复注解。
目标为class的注解 1 2 3 4 5 6 7 8 9 10 11 12 13 import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface ClassInfo { String name () default "" ; String description () default "" ; }
目标为方法的注解 1 2 3 4 5 6 7 8 9 10 11 12 13 import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MethodInfo { String name () default "" ; String description () default "" ; }
目标为属性的注解 1 2 3 4 5 6 7 8 9 10 11 12 13 import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface FieldInfo { String name () default "" ; String description () default "" ; }
注解处理工具 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 import java.lang.reflect.Field;import java.lang.reflect.Method;public class InfoUtil { public static void HandleInfo (Class<?> clazz) { if (clazz.isAnnotationPresent(ClassInfo.class)) { ClassInfo classInfo = clazz.getAnnotation(ClassInfo.class); System.out.println("classInfo:\n" +classInfo.name()+"::" +classInfo.description()); } Method[] methods = clazz.getDeclaredMethods(); for (Method item: methods){ if (item.isAnnotationPresent(MethodInfo.class)){ MethodInfo methodInfo = item.getAnnotation(MethodInfo.class); System.out.println("methodInfo:\n" +methodInfo.name()+"::" +methodInfo.description()); } } Field[] fields = clazz.getDeclaredFields(); for (Field item: fields){ if (item.isAnnotationPresent(FieldInfo.class)){ FieldInfo fieldInfo = item.getAnnotation(FieldInfo.class); System.out.println("methodInfo:\n" +fieldInfo.name()+"::" +fieldInfo.description()); } } } }
实体类demo 1 2 3 4 5 6 7 8 9 10 11 @ClassInfo(name = "DemoClass",description = "A demo class for annotation") public class Demo { @FieldInfo(name = "DemoField",description = "A demo field for annotation") private String name; @MethodInfo(name = "DemoMethod", description = "A demo method for annotation") public String getName () { return name; } }
测试类 1 2 3 4 5 public class Test { public static void main (String[] args) { InfoUtil.HandleInfo(Demo.class); } }
1 2 3 4 5 6 classInfo: DemoClass::A demo class for annotation methodInfo: DemoMethod::A demo method for annotation methodInfo: DemoField::A demo field for annotation
自定义注解之编译时注解 在介绍编译期注解前,先介绍下注解处理原理,JVM编译处理流程如下:
在命令行上指定的所有源文件被读取,解析为语法树,然后将所有外部可见定义输入到编译器的符号表中。 调用所有适当的注释处理器。如果任何注释处理器生成任何新的源或类文件,则重新启动编译,直到不创建新文件。 最后,解析器创建的语法树被分析并转换成类文件。在分析过程中,可能会发现对其他类的引用。编译器将检查这些类的源和类路径; 如果在源路径上找到这些文件,那么这些文件也将被编译,尽管它们不会被注释处理。 处理流程详细介绍 Parse and Enter 阶段 源文件被scanner处理成unicode字符流,parser 读取字符流,使用TreeMaker构建语法树,语法树的节点都是JCTree的子类(这些子类都实现了com.sun.source.Tree的子接口)构建的。我们先看下包com.sun.source.tree
中的java程序的语法节点类。
每个生成的语法树都被传递给Enter(Enter 继承了JCTree及其子类的访问者类 visitor)。
graph TD
state(Enter.main)-->state0(Enter.classEnter)
state0(Enter.classEnter)-->|Enter.uncompleted|state1(MemberEnter)
state1(MemberEnter)-->|MemberEnter.halfcompleted|state2(MemberEnter)
state2(MemberEnter)-->|To Do|state3(Attribute and Generate) 在第一阶段,对所有类递归下降遍历tree及其成员类的tree。每个类都有一个 MemberEnter 对象作为处理结果返回。 将所有类中出现的符号输入到类自身的符号表中,所有类符号、类的参数类型符号(泛型参数类型)、超类符号和继承的接口类型符号等都存储到一个未处理的列表中。 将这个未处理的列表中所有的类都解析到各自的类符号列表中,这个操作是在MemberEnter.complete()中完成(默认构造器也是在这里完成的)。 代码流程如下(com.sun.source.Tree 包位于 JAVA_HOME/lib/tools.jar 中,如需使用,要把tools.jar 加到项目的classpath中。com.sun.tools.javac.Main.compile()
是 javac的主入口。):
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 public class App { public static void main ( String[] args ) { String[] optionsAndSources = { "/test/Apple.java" }; com.sun.tools.javac.Main.compile(optionsAndSources,new PrintWriter (System.out)); } } public void compile (List<JavaFileObject> var1, List<String> var2, Iterable<? extends Processor> var3) { this .initProcessAnnotations(var3); this .delegateCompiler = this .processAnnotations( this .enterTrees( this .stopIfError(CompileState.PARSE, this .parseFiles(var1))) , var2); this .delegateCompiler.compile2(); } private void compile2 () { case BY_TODO: while (true ) { if (this .todo.isEmpty()) { break label44; } this .generate( this .desugar( this .flow( this .attribute( (Env)this .todo.remove())))); } }
词法分析主要借助com.sun.tools.javac.parser
包,语法分析主要借助了com.sun.tools.javac.tree
包。代码调用栈如下:
1 2 3 4 5 JavaCompiler.parseFiles -> JavaCompiler.parse(JavaFileObject var1) -> JavaCompiler.parse(JavaFileObject var1, CharSequence var2) -> JavacParser.parseCompilationUnit() -> com.sun.tools.javac.tree.TreeMaker
注解处理 这一部分主要由com.sun.tools.javac.processing.JavacProcessingEnvironment
处理。注释处理是编译之前的一个初步步骤。该步骤包括一系列轮次,每个轮次用于解析和输入源文件,然后确定并调用任何适当的注释处理器。在初始回合之后,如果任何被调用的注释处理器生成任何需要作为最终编译的一部分的新源文件或类文件,则将执行后续轮次。最后,当完成所有必要的轮次后,将执行实际编译。
实际上在解析了要编译的文件且确定其包含的注解后,才知道是否需要执行注解处理。因此,为了避免在没有执行注释处理的情况下不必要地解析和输入源文件,JavacProcessingEnvironment与概念模型有细微的差异,同时仍然满足在实际编译之前整个注释处理发生的概念要求。
分析生成 一旦指定的所有文件都已被解析并输入到编译器的符号表中,并且在结束了所有的注释处理,JavaCompiler继续分析语法树以生成相应的类文件。此阶段包括类型检查、控制流分析、泛型的类型擦除、去除语法糖、字节码生成等操作。
com.sun.tools.javac.comp.Attr
:检查语义的合理性并进行逻辑判断,类型是否匹配,是否初始化,泛型是否可推导,字符串常量合并com.sun.tools.javac.comp.Check
:协助attr,变量类型是否正确com.sun.tools.javac.comp.Resolve
:协助attr,变量方法类的访问是否合法,是否是静态变量com.sun.tools.javac.comp.ConstFold
:协助attr,常量折叠com.sun.tools.javac.comp.Infer
:协助attr,推导泛型com.sun.tools.javac.comp.Flow
: 数据流分析和替换等价源代码的分析(即上面的进一步语义分析)com.sun.tools.javac.comp.TransTypes
: 涉及泛型类型的代码将转换为没有泛型类型的代码com.sun.tools.javac.comp.Lower
: 处理语法糖”,Lower通过替换等效的,更简单的树来重写语法树以消除特定类型的子树com.sun.tools.javac.jvm.Gen
:遍历语法树生成最终的java字节码com.sun.tools.javac.jvm.Items
:辅助gen,这个类表示任何可寻址的操作项,这些操作项都可以作为一个单位出现在操作栈上com.sun.tools.javac.jvm.Code
:辅助gen,存储生成的字节码,并提供一些能够影射操作码的方法一旦类被写为类文件,其大部分语法树和生成的字节码将不再需要。为了节省内存,将清除对树的这些部分和符号的引用,以允许垃圾收集器恢复内存。
例子 首先是定义一个注解:
1 2 3 4 @Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) public @interface ClassInfoPrinter {}
@Retention(RetentionPolicy.CLASS)
是必须的,这样才能在编译期获取注解信息。
接下来我们实现自定义的注解处理器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @SupportedAnnotationTypes( "info.victorchu.demos.annotation.ClassInfoPrinter") @SupportedSourceVersion(SourceVersion.RELEASE_8) public class ClassInfoPrinterProcessor extends AbstractProcessor { @Override public boolean process (Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { for (TypeElement annotation: annotations) { for ( Element element : roundEnv.getElementsAnnotatedWith(annotation) ) { processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "found @ClassInfoPrinter at " + element); } } return false ; } }
到处,我们完成了注解处理的所有工作。下面是如何使用,annotation 可以和maven 结合使用,
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 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <groupId > info.victorchu.demos</groupId > <version > 1.0-SNAPSHOT</version > <artifactId > processed</artifactId > <name > processed</name > <dependencies > <dependency > <groupId > info.victorchu.demos</groupId > <artifactId > processor</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > </dependencies > <build > <plugins > <plugin > <artifactId > maven-compiler-plugin</artifactId > <configuration > <annotationProcessors > <annotationProcessor > info.victorchu.demos.processor.ClassInfoPrinterProcessor</annotationProcessor > </annotationProcessors > <showWarnings > true</showWarnings > </configuration > </plugin > </plugins > </build > </project >
如果不使用maven,也可以通过java 提供的方式使用注解处理器。
处理器本身和注解必须已经单独编译,并出现在类路径中,因此,应该做的第一件事是: 1 2 javac -proc:none info/victorchu/demos/processor/ClassInfoPrinterProcessor.java javac -proc:none info/victorchu/demos/processor/ClassInfoPrinter.java
-proc:none
选项的意思是停用所有注解处理器(可选)
然后,使用-processor 指定刚编译的注解处理器类,对源文件进行编译: 1 javac -processor info.victorchu.demos.processor.ClassInfoPrinterProcessor Person.java
要一次性指定多个注解处理器,可以用逗号分隔它们的类名,如下所示: 1 javac -processor package1.Processor1,package2.Processor2 SourceFile.java
如果不想在编译时指定,可以结合java 的SPI 机制,在META-INF/services/javax.annotation.processing.Processor
文件中加上想要使用的注解处理器类名。
深入代码 抽象注解处理器 JDK 提供了易于扩展的抽象注解处理器:
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 public abstract class AbstractProcessor implements Processor { protected ProcessingEnvironment processingEnv; private boolean initialized = false ; protected AbstractProcessor () {} public Set<String> getSupportedOptions () { SupportedOptions so = this .getClass().getAnnotation(SupportedOptions.class); if (so == null ) return Collections.emptySet(); else return arrayToSet(so.value()); } public Set<String> getSupportedAnnotationTypes () { SupportedAnnotationTypes sat = this .getClass().getAnnotation(SupportedAnnotationTypes.class); if (sat == null ) { if (isInitialized()) processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "No SupportedAnnotationTypes annotation " + "found on " + this .getClass().getName() + ", returning an empty set." ); return Collections.emptySet(); } else return arrayToSet(sat.value()); } public SourceVersion getSupportedSourceVersion () { SupportedSourceVersion ssv = this .getClass().getAnnotation(SupportedSourceVersion.class); SourceVersion sv = null ; if (ssv == null ) { sv = SourceVersion.RELEASE_6; if (isInitialized()) processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "No SupportedSourceVersion annotation " + "found on " + this .getClass().getName() + ", returning " + sv + "." ); } else sv = ssv.value(); return sv; } public synchronized void init (ProcessingEnvironment processingEnv) { if (initialized) throw new IllegalStateException ("Cannot call init more than once." ); Objects.requireNonNull(processingEnv, "Tool provided null ProcessingEnvironment" ); this .processingEnv = processingEnv; initialized = true ; } public abstract boolean process (Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) ; }
要实现我们自己的注解处理器,需要继承AbstractProcessor,并实现process方法。
注解处理上下文 ProcessingEnvironment 在实现process方法前,我们先了解下JDK 提供的工具类。ProcessingEnvironment 提供了编写新文件,报告错误信息和提供一些实用工具。Hotspot的实现类是com.sun.tools.javac.processing
包中的JavacProcessingEnvironment。
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 public interface ProcessingEnvironment { Map<String,String> getOptions () ; Messager getMessager () ; Filer getFiler () ; Elements getElementUtils () ; Types getTypeUtils () ; SourceVersion getSourceVersion () ; Locale getLocale () ; }
filer Filer是个接口,hotspot的实现类是com.sun.tools.javac.processing
包中的JavacFiler。该接口支持注解处理器创建新文件。通过这种方式创建的文件会被实现类感知,并且能够更好地管理这些文件。一共有3种文件类型:源文件,类文件和辅助资源文件。注意。这样创建的文件(源文件,类文件)在写入流(write或outputStream) 被关闭后,才会被后面的注解处理器处理。辅助资源文件不会参加之后的注解处理。
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 public interface Filer { JavaFileObject createSourceFile (CharSequence name, Element... originatingElements) throws IOException; JavaFileObject createClassFile (CharSequence name, Element... originatingElements) throws IOException; FileObject createResource (JavaFileManager.Location location, CharSequence pkg, CharSequence relativeName, Element... originatingElements) throws IOException; FileObject getResource (JavaFileManager.Location location, CharSequence pkg, CharSequence relativeName) throws IOException;}
拿到JavaFileObject或FileObject,就可以对文件进一步修改。
应用场景 依赖 JSR-269 开发的典型的第三方库有,代码自动生成的 Lombok 和 Google Auto ,代码检查的 Checker 和 Google Error Prone ,编译阶段完成依赖注入的 Google Dagger 2 等。实际应用中,由于字节码的版本问题,创建新文件会比修改java语法树的方式更安全一些。
参考资料