注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方式,使我们可以在稍后的某个时刻更容易的使用这些数据。
说白了 就是以形式化的方式允许我们在代码中添加一些信息,方便某个时刻来使用这些信息
注解是 Java 5 所引入的众多语言变化之一。它们提供了 Java 无法表达的但是你需要完整表述程序所需的信息。因此,注解使得我们可以以编译器验证的格式存储程序的额外信息。注解可以生成描述符文件,甚至是新的类定义,并且有助于减轻编写“样板”代码的负担,通过使用注解,你可以将元数据保存在 Java 源代码中。
注解的语法十分简单,主要是在现有语法中添加 @ 符号。Java 5 引入了前三种定义在 java.lang 包中的注解:
- @Override:表示当前的方法定义将覆盖基类的方法。如果你不小心拼写错误,或者方法签名被错误拼写的时候,编译器就会发出错误提示。
- @Deprecated:如果使用该注解的元素被调用,编译器就会发出警告信息。
- @SuppressWarnings:关闭不当的编译器警告信息。
- @SafeVarargs:在 Java 7 中加入用于禁止对具有泛型varargs参数的方法或构造函数的调用方发出警告。
- @FunctionalInterface:Java 8 中加入用于表示类型声明为函数式接口。
定义注解
如下是一个注解的定义。注解的定义看起来很像接口的定义。事实上,它们和其他 Java 接口一样,也会被编译成 class 文件。
// onjava/atunit/Test.java// The @Test tagpackage onjava.atunit;import java.lang.annotation.*;@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface Test {}
除了 @ 符号之外, @Test 的定义看起来更像一个空接口。注解的定义也需要一些元注解(meta-annotation),比如 @Target 和 @Retention。@Target 定义你的注解可以应用在哪里(例如是方法还是字段)。@Retention 定义了注解在哪里可用,在源代码中(SOURCE),class文件(CLASS)中或者是在运行时(RUNTIME)。
注解通常会包含一些表示特定值的元素。当分析处理注解的时候,程序或工具可以利用这些值。注解的元素看起来就像接口的方法,但是可以为其指定默认值。
不包含任何元素的注解叫标记注解 像@Test就是标记注解
下面是一个简单的注解,我们可以用它来追踪项目中的用例。程序员可以使用该注解来标注满足特定用例的一个方法或者一组方法。于是,项目经理可以通过统计已经实现的用例来掌控项目的进展,而开发者在维护项目时可以轻松的找到用例用于更新,或者他们可以调试系统中业务逻辑。
// annotations/UseCase.javaimport java.lang.annotation.*;@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface UseCase {int id();String description() default "no description";}
注意 id 和 description 与方法定义类似。由于编译器会对 id 进行类型检查,因此将跟踪数据库与用例文档和源代码相关联是可靠的方式。description 元素拥有一个 default 值,如果在注解某个方法时没有给出 description 的值。则该注解的处理器会使用此元素的默认值。
应用
// annotations/PasswordUtils.javaimport java.util.*;public class PasswordUtils {@UseCase(id = 47, description ="Passwords must contain at least one numeric")public boolean validatePassword(String passwd) {return (passwd.matches("\\w*\\d\\w*"));}@UseCase(id = 48)public String encryptPassword(String passwd) {return new StringBuilder(passwd).reverse().toString();}@UseCase(id = 49, description ="New passwords can't equal previously used ones")public boolean checkForNewPassword(List<String> prevPasswords, String passwd) {return !prevPasswords.contains(passwd);}}
注解的元素在使用时表现为 名-值 对的形式,并且需要放置在 @UseCase 声明之后的括号内。在 encryptPassword() 方法的注解中,并没有给出 description 的值,所以在 @interface UseCase 的注解处理器分析处理这个类的时候会使用该元素的默认值
元注解
Java 语言中目前有 5 种标准注解(前面介绍过),以及 5 种元注解。元注解用于注解其他的注解
| 注解 | 解释 |
|---|---|
| @Target | 表示注解可以用于哪些地方。可能的 ElementType 参数包括: CONSTRUCTOR:构造器的声明 FIELD:字段声明(包括 enum 实例) LOCAL_VARIABLE:局部变量声明 METHOD:方法声明 PACKAGE:包声明 PARAMETER:参数声明 TYPE:类、接口(包括注解类型)或者 enum 声明 |
| @Retention | 表示注解信息保存的时长。可选的 RetentionPolicy 参数包括: SOURCE:注解将被编译器丢弃 CLASS:注解在 class 文件中可用,但是会被 VM 丢弃。 RUNTIME:VM 将在运行期也保留注解,因此可以通过反射机制读取注解的信息。 |
| @Documented | 将此注解保存在 Javadoc 中 |
| @Inherited | 允许子类继承父类的注解 |
| @Repeatable | 允许一个注解可以被使用一次或者多次(Java 8)。 |
大多数时候,程序员定义自己的注解,并编写自己的处理器来处理他们。
编写注解处理器
如果没有用于读取注解的工具,那么注解不会比注释更有用。使用注解中一个很重要的部分就是,创建与使用注解处理器。Java 拓展了反射机制的 API 用于帮助你创造这类工具。同时他还提供了 javac 编译器钩子在编译时使用注解。
下面是一个非常简单的注解处理器,我们用它来读取被注解的 PasswordUtils 类,并且使用反射机制来寻找 @UseCase 标记。给定一组 id 值,然后列出在 PasswordUtils 中找到的用例,以及缺失的用例。
// annotations/UseCaseTracker.javaimport java.util.*;import java.util.stream.*;import java.lang.reflect.*;public class UseCaseTracker {public static voidtrackUseCases(List<Integer> useCases, Class<?> cl) {for(Method m : cl.getDeclaredMethods()) {UseCase uc = m.getAnnotation(UseCase.class);if(uc != null) {System.out.println("Found Use Case " +uc.id() + "\n " + uc.description());useCases.remove(Integer.valueOf(uc.id()));}}useCases.forEach(i ->System.out.println("Missing use case " + i));}public static void main(String[] args) {List<Integer> useCases = IntStream.range(47, 51).boxed().collect(Collectors.toList());trackUseCases(useCases, PasswordUtils.class);}}输出为:Found Use Case 48no descriptionFound Use Case 47Passwords must contain at least one numericFound Use Case 49New passwords can't equal previously used onesMissing use case 50
这个程序用了两个反射的方法:getDeclaredMethods() 和 getAnnotation(),它们都属于 AnnotatedElement 接口(Class,Method 与 Field 类都实现了该接口)。getAnnotation() 方法返回指定类型的注解对象,在本例中就是 “UseCase”。如果被注解的方法上没有该类型的注解,返回值就为 null。我们通过调用 id() 和 description() 方法来提取元素值。注意 encryptPassword() 方法在注解的时候没有指定 description 的值,因此处理器在处理它对应的注解时,通过 description() 取得的是默认值 “no description”
注解元素
在 UseCase.java 中定义的 @UseCase 的标签包含 int 元素 id 和 String 元素 description。注解元素可用的类型如下所示:
- 所有基本类型(int、float、boolean等)
- String
- Class
- enum
- Annotation
- 以上类型的数组
如果你使用了其他类型,编译器就会报错。注意,也不允许使用任何包装类型,但是由于自动装箱的存在,这不算是什么限制。注解也可以作为元素的类型。稍后你会看到,注解嵌套是一个非常有用的技巧。
默认值限制
编译器对于元素的默认值有些过于挑剔。首先,元素不能有不确定的值。也就是说,元素要么有默认值,要么就在使用注解时提供元素的值。也就是不能为null
这里有另外一个限制:任何非基本类型的元素, 无论是在源代码声明时还是在注解接口中定义默认值时,都不能使用 null 作为其值。这个限制使得处理器很难表现一个元素的存在或者缺失的状态,因为在每个注解的声明中,所有的元素都存在,并且具有相应的值。为了绕开这个约束,可以自定义一些特殊的值,比如空字符串或者负数用于表达某个元素不存在。
// annotations/SimulatingNull.javaimport java.lang.annotation.*;@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface SimulatingNull {int id() default -1;String description() default "";}
这是一个在定义注解的习惯用法。
生成外部文件
当有些框架需要一些额外的信息才能与你的源代码协同工作,这种情况下注解就会变得十分有用。除了定义 Java 类,程序员还必须忍受沉闷,重复的提供某些信息,例如类名和包名等已经在原始类中提供过的信息。每当你使用外部描述文件时,他就拥有了一个类的两个独立信息源,这经常导致代码的同步问题。同时这也要求了为项目工作的程序员在知道如何编写 Java 程序的同时,也必须知道如何编辑描述文件。
嵌套注解
// annotations/database/Constraints.javapackage annotations.database;import java.lang.annotation.*;@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)public @interface Constraints {boolean primaryKey() default false;boolean allowNull() default true;boolean unique() default false;}
// annotations/database/SQLInteger.javapackage annotations.database;import java.lang.annotation.*;@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)public @interface SQLInteger {String name() default "";Constraints constraints() default @Constraints;}
这些 SQL 类型具有 name() 元素和 constraints() 元素。后者利用了嵌套注解的功能,将数据库列的类型约束信息嵌入其中。注意 constraints() 元素的默认值是 @Constraints。由于在 @Constraints 注解类型之后,没有在括号中指明 @Constraints 元素的值,因此,constraints() 的默认值为所有元素都为默认值的 @Constraints 注解。如果要使得嵌入的 @Constraints 注解中的 unique() 元素为 true,并作为 constraints() 元素的默认值,你可以像如下定义:
// annotations/database/Uniqueness.java// Sample of nested annotationspackage annotations.database;public @interface Uniqueness {Constraints constraints()default @Constraints(unique = true);}
注解不支持继承
你不能使用 extends 关键字来继承 @interfaces。
使用javac处理注解
通过javac你可以通过创建编译时注解处理器在 Java 源文件上使用注解,而不是编译之后的 class 文件。但是这里有一个重大限制:你不能通过处理器来改变源代码。唯一影响输出的方式就是创建新的文件。
说白了:就是你用@Retention(RetentionPolicy.SOURCE) 这个注解可以用来新建类或者接口(也就是源文件 .java后缀的)
如果你的注解处理器创建了新的源文件,在新一轮处理中注解会检查源文件本身。工具在检测一轮之后持续循环,直到不再有新的源文件产生。然后它编译所有的源文件。
每一个你编写的注解都需要处理器,但是 javac 可以非常容易的将多个注解处理器合并在一起。你可以指定多个需要处理的类,并且你可以添加监听器用于监听注解处理完成后接到通知。
最简单的处理器
// annotations/simplest/Simple.java// A bare-bones annotationpackage annotations.simplest;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;import java.lang.annotation.ElementType;@Retention(RetentionPolicy.SOURCE)@Target({ElementType.TYPE, ElementType.METHOD,ElementType.CONSTRUCTOR,ElementType.ANNOTATION_TYPE,ElementType.PACKAGE, ElementType.FIELD,ElementType.LOCAL_VARIABLE})public @interface Simple {String value() default "-default-";}
@Retention现在的参数为SOURCE, 它意味着这个注释在编译后是不会继续保存的, 只在编译之前有效,它在这里指出javac是唯一有机会处理该注解的.说白了就是被这个注解标注后,你写的javac处理器可以对被标注的代码进行处理生成对应的java源文件
javac处理器
// annotations/simplest/SimpleProcessor.java// A bare-bones annotation processorpackage annotations.simplest;import javax.annotation.processing.*;import javax.lang.model.SourceVersion;import javax.lang.model.element.*;import java.util.*;@SupportedAnnotationTypes("annotations.simplest.Simple")@SupportedSourceVersion(SourceVersion.RELEASE_8)public class SimpleProcessorextends AbstractProcessor {@Overridepublic boolean process(Set<? extends TypeElement> annotations,RoundEnvironment env) {for(TypeElement t : annotations)System.out.println(t);for(Element el :env.getElementsAnnotatedWith(Simple.class))display(el);return false;}private void display(Element el) {System.out.println("==== " + el + " ====");System.out.println(el.getKind() +" : " + el.getModifiers() +" : " + el.getSimpleName() +" : " + el.asType());if(el.getKind().equals(ElementKind.CLASS)) {TypeElement te = (TypeElement)el;System.out.println(te.getQualifiedName());System.out.println(te.getSuperclass());System.out.println(te.getEnclosedElements());}if(el.getKind().equals(ElementKind.METHOD)) {ExecutableElement ex = (ExecutableElement)el;System.out.print(ex.getReturnType() + " ");System.out.print(ex.getSimpleName() + "(");System.out.println(ex.getParameters() + ")");}}}
@SupportedAnnotationTypes 和 @SupportedSourceVersion 注解(这是一个很好的用注解简化代码的示例)。
你唯一需要实现的方法就是 process(),这里是所有行为发生的地方。第一个参数告诉你哪个注解是存在的,第二个参数保留了剩余信息。我们所做的事情只是打印了注解(这里只存在一个),可以看 TypeElement 文档中的其他行为。通过使用 process() 的第二个操作,我们循环所有被 @Simple 注解的元素,并且针对每一个元素调用我们的 display() 方法。所有 Element 展示了自身的基本信息;例如,getModifiers() 告诉你它是否为 public 和 static 。
如果只是通过平常的方式来编译 SimpleTest.java,你不会得到任何结果。为了得到注解输出,你必须增加一个 processor 标志并且连接注解处理器类
javac -processor annotations.simplest.SimpleProcessor SimpleTest.java
现在编译器有了输出
annotations.simplest.Simple==== annotations.simplest.SimpleTest ====CLASS : [public] : SimpleTest : annotations.simplest.SimpleTestannotations.simplest.SimpleTestjava.lang.Objecti,SimpleTest(),foo(),bar(java.lang.String,int,float),main(java.lang.String[])==== i ====FIELD : [] : i : int==== SimpleTest() ====CONSTRUCTOR : [public] : <init> : ()void==== foo() ====METHOD : [public] : foo : ()voidvoid foo()==== bar(java.lang.String,int,float) ====METHOD : [public] : bar : (java.lang.String,int,float)voidvoid bar(s,i,f)==== main(java.lang.String[]) ====METHOD : [public, static] : main : (java.lang.String[])voidvoid main(args)
更复杂的处理器
当你创建用于 javac 注解处理器时,你不能使用 Java 的反射特性,因为你处理的是源代码,而并非是编译后的 class 文件。解决这个问题的方法是,通过允许你在未编译的源代码中查看方法、字段和类型。
如下是一个用于提取类中方法的注解,所以它可以被抽取成为一个接口:
// annotations/ifx/ExtractInterface.java
// javac-based annotation processing
package annotations.ifx;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface ExtractInterface {
String interfaceName() default "-!!-";
}
// annotations/ifx/Multiplier.java
// javac-based annotation processing
// {java annotations.ifx.Multiplier}
package annotations.ifx;
@ExtractInterface(interfaceName="IMultiplier")
public class Multiplier {
public boolean flag = false;
private int n = 0;
public int multiply(int x, int y) {
int total = 0;
for(int i = 0; i < x; i++)
total = add(total, y);
return total;
}
public int fortySeven() { return 47; }
private int add(int x, int y) {
return x + y;
}
public double timesTen(double arg) {
return arg * 10;
}
public static void main(String[] args) {
Multiplier m = new Multiplier();
System.out.println(
"11 * 16 = " + m.multiply(11, 16));
}
}
输出为
11 * 16 = 176
Multiplier 类(只能处理正整数)拥有一个 multiply() 方法,这个方法会多次调用私有方法 add() 来模拟乘法操作。add() 是私有方法,因此不能成为接口的一部分。其他的方法提供了语法多样性。注解被赋予 IMultiplier 的 InterfaceName 作为要创建的接口的名称。
创建一个新的 interface 源代码文件(这个源文件将会在下一轮中被自动编译)
// annotations/ifx/IfaceExtractorProcessor.java
// javac-based annotation processing
package annotations.ifx;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.util.*;
import java.util.*;
import java.util.stream.*;
import java.io.*;
@SupportedAnnotationTypes(
"annotations.ifx.ExtractInterface")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class IfaceExtractorProcessor
extends AbstractProcessor {
private ArrayList<Element>
interfaceMethods = new ArrayList<>();
Elements elementUtils;
private ProcessingEnvironment processingEnv;
@Override
public void init(
ProcessingEnvironment processingEnv) {
this.processingEnv = processingEnv;
elementUtils = processingEnv.getElementUtils();
}
@Override
public boolean process(
Set<? extends TypeElement> annotations,
RoundEnvironment env) {
for(Element elem:env.getElementsAnnotatedWith(
ExtractInterface.class)) {
String interfaceName = elem.getAnnotation(
ExtractInterface.class).interfaceName();
for(Element enclosed :
elem.getEnclosedElements()) {
if(enclosed.getKind()
.equals(ElementKind.METHOD) &&
enclosed.getModifiers()
.contains(Modifier.PUBLIC) &&
!enclosed.getModifiers()
.contains(Modifier.STATIC)) {
interfaceMethods.add(enclosed);
}
}
if(interfaceMethods.size() > 0)
writeInterfaceFile(interfaceName);
}
return false;
}
private void
writeInterfaceFile(String interfaceName) {
try(
Writer writer = processingEnv.getFiler()
.createSourceFile(interfaceName)
.openWriter()
) {
String packageName = elementUtils
.getPackageOf(interfaceMethods
.get(0)).toString();
writer.write(
"package " + packageName + ";\n");
writer.write("public interface " +
interfaceName + " {\n");
for(Element elem : interfaceMethods) {
ExecutableElement method =
(ExecutableElement)elem;
String signature = " public ";
signature += method.getReturnType() + " ";
signature += method.getSimpleName();
signature += createArgList(
method.getParameters());
System.out.println(signature);
writer.write(signature + ";\n");
}
writer.write("}");
} catch(Exception e) {
throw new RuntimeException(e);
}
}
private String createArgList(
List<? extends VariableElement> parameters) {
String args = parameters.stream()
.map(p -> p.asType() + " " + p.getSimpleName())
.collect(Collectors.joining(", "));
return "(" + args + ")";
}
}
Elements 对象实例 elementUtils 是一组静态方法的工具;我们用它来寻找 writeInterfaceFile() 中含有的包名。getEnclosedElements()方法会通过指定的元素生成所有的“闭包”元素。在这里,这个类闭包了它的所有元素。通过使用 getKind() 我们会找到所有的 public 和 static 方法,并将其添加到 interfaceMethods 列表中。接下来 writeInterfaceFile() 使用 interfaceMethods 列表里面的值生成新的接口定义。注意,在 writeInterfaceFile() 使用了向下转型到 ExecutableElement,这使得我们可以获取所有的方法信息。createArgList() 是一个帮助方法,用于生成参数列表。
Filer是 getFiler() 生成的,并且是 PrintWriter 的一种实例,可以用于创建新文件。我们使用 Filer 对象,而不是原生的 PrintWriter 原因是,这个对象可以运行 javac 追踪你创建的新文件,这使得它可以在新一轮中检查新文件中的注解并编译文件。
如下是一个命令行,可以在编译的时候使用处理器:
javac -processor annotations.ifx.IfaceExtractorProcessor Multiplier.java
新生成的 IMultiplier.java 的文件,正如你通过查看上面处理器的 println() 语句所猜测的那样,如下所示:
package annotations.ifx;
public interface IMultiplier {
public int multiply(int x, int y);
public int fortySeven();
public double timesTen(double arg);
}
这个类同样会被 javac 编译(在某一轮中),所以你会在同一个目录中看到 IMultiplier.class 文件。
基于注解的单元测试
Java 断言机制需要你在 java 命令行行加上 -ea 标志来开启
因为用的测试可能不同 这里不往下研究
