1. 元注解
元注解就是用于修饰注解的注解,例如下面是@Override的注解定义,Override被@Target和@Retention修饰,这两个注解就是元注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
元注解有六种:
- @Document
- @Target
- @Retention
- @Inherited
- @Native
- @Repeatable
1. @Target
Target注解的作用是:描述注解的使用范围(即被修饰的注解可以用在什么地方).
Target注解用来说明那些被它所注解的注解类可修饰的对象范围:注解可以用于修饰 packages、types(类、接口、枚举、注解类)、类成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数),在定义注解类时使用了@Target 能够更加清晰的知道它能够被用来修饰哪些对象,它的取值范围定义在ElementType 枚举中.
源码 ```java @Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}
Target注解后面的参数为可以传入的值,这里的值是个数组,因此可以有多个作用范围,ElementType的枚举种类如下:
```java
public enum ElementType {
TYPE, // 类、接口、枚举类
FIELD, // 成员变量(包括:枚举常量)
METHOD, // 成员方法
PARAMETER, // 方法参数
CONSTRUCTOR, // 构造方法
LOCAL_VARIABLE, // 局部变量
ANNOTATION_TYPE, // 注解类
PACKAGE, // 可用于修饰:包
TYPE_PARAMETER, // 类型参数,JDK 1.8 新增
TYPE_USE // 使用类型的任何地方,JDK 1.8 新增
}
2. @Retention
可以赋值 RetentionPolicy
类型,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
}
RetentionPolicy.SOURCE
:表明注解会被编译器丢弃,字节码中不会带有注解信息RetentionPolicy.CLASS
:表明注解会被写入字节码文件,且是@Retention
的默认值RetentionPolicy.RUNTIME
:表明注解会被写入字节码文件,并且能够被JVM 在运行时获取到,可以通过反射的方式解析到
生命周期大小排序为 SOURCE < CLASS < RUNTIME,前者能使用的地方后者一定也能使用。如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解;如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS 注解;如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 注解。
3. @Documented
Documented注解的作用是:描述在使用 javadoc 工具为类生成帮助文档时是否要保留其注解信息。
这里验证@Documented的作用,我们创建一个自定义注解:
@Documented
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface MyDocumentedt {
public String value() default "这是@Documented注解为文档添加的注释";
}
复制代码
然后创建一个测试类,在方法和类上都加入自定义的注解
@MyDocumentedt
public class MyDocumentedTest {
/**
* 测试 document
* @return String the response
*/
@MyDocumentedt
public String test(){
return "sdfadsf";
}
}
复制代码
打开java文件所在的目录下,打开命令行输入:
javac .\MyDocumentedt.java .\MyDocumentedTest.java
复制代码
javadoc -d doc .\MyDocumentedTest.java .\MyDocumentedt.java
复制代码
运行成功后,打开生成的帮助文档,可以看到在类和方法上都保留了 MyDocument 的注解信息。如下图所示:
4. @Inherited
@Inherited 是一个标记注解,用来指定该注解可以被继承。使用 @Inherited 注解的 Class 类,表示这个注解可以被用于该 Class 类的子类。就是说如果某个类使用了被 @Inherited 修饰的注解,则其子类将自动具有该注解。
例如如下三个类,TestA用@MyInherited修饰,那么TestA子类TestB和TestC也会自动被@MyInherited修饰
@Target({ ElementType.TYPE })
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface MyInherited {
}
@MyInherited
public class TestA {
public static void main(String[] args) {
System.out.println(TestA.class.getAnnotation(MyInherited.class));
System.out.println(TestB.class.getAnnotation(MyInherited.class));
System.out.println(TestC.class.getAnnotation(MyInherited.class));
}
}
class TestB extends TestA {
}
class TestC extends TestB {
}
运行结果:
@MyInherited()
@MyInherited()
@MyInherited()
5. @Repeatable
@Repeatable注解表明标记的注解可以多次应用于相同的声明或类型,此注解由Java SE 8版本引入。在java8之前不可以在同一个地方使用同一类型注解两次以上,例如我们定义注解
public @interface Schedule {
String dayOfMonth() default "first";
String dayOfWeek() default "Mon";
int hour() default 12;
}
如下图会报错
在没有@Repeatable注解的时候我们实现多注解需要如下这样,定义一个Schedules,里面存放的类型是Schedule,写起来也很麻烦:
package item39;
/**
* @author: qujundong
* @date: 2020/12/6 下午1:22
* @description:
*/
import java.lang.annotation.*;
public class Annotatioon {
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Schedule {
String dayOfMonth() default "first";
String dayOfWeek() default "Mon";
int hour() default 12;
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Schedules {
Schedule[] value();
}
@Schedules(value = {
@Schedule(dayOfMonth = "a", dayOfWeek = "b"),
@Schedule(dayOfMonth = "c", dayOfWeek = "d")
})
public static void test(){
}
public static void main(String[] args) {
test();
}
}
使用@Repeatable注解后,如下代码所示
第一步,先声明一个重复注解类:
import java.lang.annotation.Repeatable;
@Repeatable(Schedules.class)
public @interface Schedule {
String dayOfMonth() default "first";
String dayOfWeek() default "Mon";
int hour() default 12;
}
@Retention(RetentionPolicy.RUNTIME)
public @interface Schedules {
Schedule[] value();
}
创建一个测试类:
import java.lang.reflect.Method;
@Schedule(dayOfMonth="last")
@Schedule(dayOfWeek="Wed", hour=24)
public class RepetableAnnotation{
@Schedule(dayOfMonth="last")
@Schedule(dayOfWeek="Fri", hour=23)
public void doPeriodicCleanup(){}
public static void main(String[] args) throws NoSuchMethodException {
Method doPeriodicCleanup = RepetableAnnotation.class.getMethod("doPeriodicCleanup");
Schedules schedules = doPeriodicCleanup.getAnnotation(Schedules.class);
System.out.println("获取标记方法上的重复注解:");
for (Schedule schedule: schedules.value()){
System.out.println(schedule);
}
System.out.println("获取标记类上的重复注解:");
if (RepetableAnnotation.class.isAnnotationPresent(Schedules.class)){
schedules = RepetableAnnotation.class.getAnnotation(Schedules.class);
for (Schedule schedule: schedules.value()){
System.out.println(schedule);
}
}
}
}
运行结果:
获取标记方法上的重复注解
@org.springmorning.demo.javabase.annotation.meta.Schedule(hour=12, dayOfMonth=last, dayOfWeek=Mon)
@org.springmorning.demo.javabase.annotation.meta.Schedule(hour=23, dayOfMonth=first, dayOfWeek=Fri)
获取标记类上的重复注解:
@org.springmorning.demo.javabase.annotation.meta.Schedule(hour=12, dayOfMonth=last, dayOfWeek=Mon)
@org.springmorning.demo.javabase.annotation.meta.Schedule(hour=24, dayOfMonth=first, dayOfWeek=Wed)
6. @Native
使用 @Native 注解修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。对于 @Native 注解不常使用,了解即可。
2. 注解优先于命名 模式
过去,通常使用命名模式( naming patterns)来指示某些程序元素需要通过工具或框架进行特殊处理。 例如,在第4版之前,JUnit测试框架要求其用户通过以test[Beck04]开始名称来指定测试方法。 这种技术是有效的,但它有几个很大的缺点。 首先,拼写错误导致失败,但不会提示。 例如,假设意外地命名了测试方法tsetSafetyOverride
而不是testSafetyOverride
。 JUnit 3不会报错,但它也不会执行测试,导致错误的安全感。
命名模式的第二个缺点是无法确保它们仅用于适当的程序元素。 例如,假设调用了TestSafetyMechanisms
类,希望JUnit 3能够自动测试其所有方法,而不管它们的名称如何。 同样,JUnit 3也不会出错,但它也不会执行测试。
命名模式的第三个缺点是它们没有提供将参数值与程序元素相关联的好的方法。 例如,假设想支持只有在抛出特定异常时才能成功的测试类别。 异常类型基本上是测试的一个参数。 你可以使用一些精心设计的命名模式将异常类型名称编码到测试方法名称中,但这会变得丑陋和脆弱(条目 62)。 编译器无法知道要检查应该命名为异常的字符串是否确实存在。 如果命名的类不存在或者不是异常,那么直到尝试运行测试时才会发现。
注解很好地解决了所有这些问题,JUnit从第4版开始采用它们。在这个项目中,我们将编写我们自己的测试框架来显示注解的工作方式。 假设你想定义一个注解类型来指定自动运行的简单测试,并且如果它们抛出一个异常就会失败。 以下是名为Test
的这种注解类型的定义:
// Marker annotation type declaration
import java.lang.annotation.*;
/**
* Indicates that the annotated method is a test method.
* Use only on parameterless static methods.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
Test注解类型的声明本身使用Retention
和Target
注解进行标记。 注解类型声明上的这种注解称为元注解。 @Retention(RetentionPolicy.RUNTIME)
元注解指示Test注解应该在运行时保留。 没有它,测试工具就不会看到Test注解。@Target.get(ElementType.METHOD)
元注解表明Test注解只对方法声明合法:它不能应用于类声明,属性声明或其他程序元素。
在Test注解声明之前的注释说:“仅在无参静态方法中使用”。如果编译器可以强制执行此操作是最好的,但它不能,除非编写注解处理器来执行此操作。 有关此主题的更多信息,请参阅javax.annotation.processing
文档。 在缺少这种注解处理器的情况下,如果将Test注解放在实例方法声明或带有一个或多个参数的方法上,那么测试程序仍然会编译,并将其留给测试工具在运行时来处理这个问题 。
以下是Test注解在实践中的应用。 它被称为标记注解,因为它没有参数,只是“标记”注解元素。 如果程序员错拼Test或将Test注解应用于程序元素而不是方法声明,则该程序将无法编译。
// Program containing marker annotations
public class Sample {
@Test public static void m1() { } // Test should pass
public static void m2() { }
@Test public static void m3() { // Test should fail
throw new RuntimeException("Boom");
}
public static void m4() { }
@Test public void m5() { } // INVALID USE: nonstatic method
public static void m6() { }
@Test public static void m7() { // Test should fail
throw new RuntimeException("Crash");
}
public static void m8() { }
}
Sample
类有七个静态方法,其中四个被标注为Test。 其中两个,m3和m7引发异常,两个m1和m5不引发异常。 但是没有引发异常的注解方法之一是实例方法,因此它不是注释的有效用法。 总之,Sample
包含四个测试:一个会通过,两个会失败,一个是无效的。 未使用Test注解标注的四种方法将被测试工具忽略。
Test注解对Sample类的语义没有直接影响。 他们只提供信息供相关程序使用。 更一般地说,注解不会改变注解代码的语义,但可以通过诸如这个简单的测试运行器等工具对其进行特殊处理:
// Program to process marker annotations
import java.lang.reflect.*;
public class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName(args[0]);
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
tests++;
try {
m.invoke(null);
passed++;
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
System.out.println(m + " failed: " + exc);
} catch (Exception exc) {
System.out.println("Invalid @Test: " + m);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n",
passed, tests - passed);
}
}
测试运行器工具在命令行上接受完全限定的类名,并通过调用Method.invoke
来反射地运行所有类标记有Test注解的方法。 isAnnotationPresent
方法告诉工具要运行哪些方法。 如果测试方法引发异常,则反射机制将其封装在InvocationTargetException
中。 该工具捕获此异常并打印包含由test方法抛出的原始异常的故障报告,该方法是使用getCause
方法从InvocationTargetException
中提取的。
如果尝试通过反射调用测试方法会抛出除InvocationTargetException
之外的任何异常,则表示编译时未捕获到没有使用的Test注解。 这些用法包括注解实例方法,具有一个或多个参数的方法或不可访问的方法。 测试运行器中的第二个catch块会捕获这些Test使用错误并显示相应的错误消息。 这是在RunTests
在Sample
上运行时打印的输出:
public static void Sample.m3() failed: RuntimeException: Boom
Invalid @Test: public void Sample.m5()
public static void Sample.m7() failed: RuntimeException: Crash
Passed: 1, Failed: 3
现在,让我们添加对仅在抛出特定异常时才成功的测试的支持。 我们需要为此添加一个新的注解类型:
// Annotation type with a parameter
import java.lang.annotation.*;
/**
* Indicates that the annotated method is a test method that
* must throw the designated exception to succeed.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
此注解的参数类型是Class<? extends Throwable>
。毫无疑问,这种通配符是拗口的。 在英文中,它表示“扩展Throwable的某个类的Class对象”,它允许注解的用户指定任何异常(或错误)类型。 这个用法是一个限定类型标记的例子(条目 33)。 以下是注解在实践中的例子。 请注意,类名字被用作注解参数值:
// Program containing annotations with a parameter
public class Sample2 {
@ExceptionTest(ArithmeticException.class)
public static void m1() { // Test should pass
int i = 0;
i = i / i;
}
@ExceptionTest(ArithmeticException.class)
public static void m2() { // Should fail (wrong exception)
int[] a = new int[0];
int i = a[1];
}
@ExceptionTest(ArithmeticException.class)
public static void m3() { } // Should fail (no exception)
}
现在让我们修改测试运行器工具来处理新的注解。 这样将包括将以下代码添加到main方法中:
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n", m);
} catch (InvocationTargetException wrappedEx) {
Throwable exc = wrappedEx.getCause();
Class<? extends Throwable> excType =
m.getAnnotation(ExceptionTest.class).value();
if (excType.isInstance(exc)) {
passed++;
} else {
System.out.printf(
"Test %s failed: expected %s, got %s%n",
m, excType.getName(), exc);
}
} catch (Exception exc) {
System.out.println("Invalid @Test: " + m);
}
}
此代码与我们用于处理Test注解的代码类似,只有一个例外:此代码提取注解参数的值并使用它来检查测试引发的异常是否属于正确的类型。 没有明确的转换,因此没有ClassCastException
的危险。 测试程序编译的事实保证其注解参数代表有效的异常类型,但有一点需要注意:如果注解参数在编译时有效,但代表指定异常类型的类文件在运行时不再存在,则测试运行器将抛出TypeNotPresentException
异常。
将我们的异常测试示例进一步推进,可以设想一个测试,如果它抛出几个指定的异常中的任何一个,就会通过测试。 注解机制有一个便于支持这种用法的工具。 假设我们将ExceptionTest
注解的参数类型更改为Class对象数组:
// Annotation type with an array parameter
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception>[] value();
}
注解中数组参数的语法很灵活。 它针对单元素数组进行了优化。 所有以前的ExceptionTest
注解仍然适用于ExceptionTest的新数组参数版本,并且会生成单元素数组。 要指定一个多元素数组,请使用花括号将这些元素括起来,并用逗号分隔它们:
// Code containing an annotation with an array parameter
@ExceptionTest({ IndexOutOfBoundsException.class,
NullPointerException.class })
public static void doublyBad() {
List<String> list = new ArrayList<>();
// The spec permits this method to throw either
// IndexOutOfBoundsException or NullPointerException
list.addAll(5, null);
}
修改测试运行器工具以处理新版本的ExceptionTest
是相当简单的。 此代码替换原始版本:
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n", m);
} catch (Throwable wrappedExc) {
Throwable exc = wrappedExc.getCause();
int oldPassed = passed;
Class<? extends Exception>[] excTypes =
m.getAnnotation(ExceptionTest.class).value();
for (Class<? extends Exception> excType : excTypes) {
if (excType.isInstance(exc)) {
passed++;
break;
}
}
if (passed == oldPassed)
System.out.printf("Test %s failed: %s %n", m, exc);
}
}
从Java 8开始,还有另一种方法来执行多值注解。 可以使用@Repeatable
元注解来标示注解的声明,而不用使用数组参数声明注解类型,以指示注解可以重复应用于单个元素。 该元注解采用单个参数,该参数是包含注解类型的类对象,其唯一参数是注解类型[JLS,9.6.3]的数组。 如果我们使用ExceptionTest
注解采用这种方法,下面是注解的声明。 请注意,包含注解类型必须使用适当的保留策略和目标进行注解,否则声明将无法编译:
// Repeatable annotation type
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
Class<? extends Exception> value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
ExceptionTest[] value();
}
下面是我们的doublyBad
测试用一个重复的注解代替基于数组值注解的方式:
// Code containing a repeated annotation
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() { ... }
处理可重复的注解需要注意。重复注解会生成包含注解类型的合成注解。 getAnnotationsByType
方法掩盖了这一事实,可用于访问可重复注解类型和非重复注解。但isAnnotationPresent
明确指出重复注解不是注解类型,而是包含注解类型。如果某个元素具有某种类型的重复注解,并且使用isAnnotationPresent
方法检查元素是否具有该类型的注释,则会发现它没有。使用此方法检查注解类型的存在会因此导致程序默默忽略重复的注解。同样,使用此方法检查包含的注解类型将导致程序默默忽略不重复的注释。要使用isAnnotationPresent
检测重复和非重复的注解,需要检查注解类型及其包含的注解类型。以下是RunTests程序的相关部分在修改为使用ExceptionTest注解的可重复版本时的例子:
// Processing repeatable annotations
if (m.isAnnotationPresent(ExceptionTest.class)
|| m.isAnnotationPresent(ExceptionTestContainer.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n", m);
} catch (Throwable wrappedExc) {
Throwable exc = wrappedExc.getCause();
int oldPassed = passed;
ExceptionTest[] excTests =
m.getAnnotationsByType(ExceptionTest.class);
for (ExceptionTest excTest : excTests) {
if (excTest.value().isInstance(exc)) {
passed++;
break;
}
}
if (passed == oldPassed)
System.out.printf("Test %s failed: %s %n", m, exc);
}
}
添加了可重复的注解以提高源代码的可读性,从逻辑上将相同注解类型的多个实例应用于给定程序元素。 如果觉得它们增强了源代码的可读性,请使用它们,但请记住,在声明和处理可重复注解时存在更多的样板,并且处理可重复的注解很容易出错。
这个项目中的测试框架只是一个演示,但它清楚地表明了注解相对于命名模式的优越性,而且它仅仅描绘了你可以用它们做什么的外观。 如果编写的工具要求程序员将信息添加到源代码中,请定义适当的注解类型。当可以使用注解代替时,没有理由使用命名模式。
这就是说,除了特定的开发者(toolsmith)之外,大多数程序员都不需要定义注解类型。 但所有程序员都应该使用Java提供的预定义注解类型(条目40,27)。 另外,请考虑使用IDE或静态分析工具提供的注解。 这些注解可以提高这些工具提供的诊断信息的质量。 但请注意,这些注解尚未标准化,因此如果切换工具或标准出现,可能额外需要做一些工作。