Java 注解
1. 概念 (Concept)
Java 注解 (Annotation) 是 J2SE 5.0 引入的一种元数据 (metadata) 机制。它允许开发者将元数据嵌入到源代码中。这些注解本身不做任何事情,但可以被其他工具或库读取和处理。
注解可以用于:
- 为编译器提供信息 - 例如,
@Override注解告诉编译器,被注解的方法意图覆盖超类中的方法。如果方法签名不正确,编译器会发出警告。 - 编译时和部署时处理 - 软件工具可以处理注解信息以生成代码、XML 文件等。
- 运行时分析 - 可以在运行时通过反射查询注解,并据此改变程序的行为。
2. 原理 (Principle)
注解的实现依赖于 Java 的反射 API。当编译器编译 Java 代码时,它会将注解信息存储在生成的 .class 文件中。在运行时,JVM 可以通过反射机制读取这些注解信息,从而实现相应的功能。
注解的生命周期由 @Retention策略定义:
RetentionPolicy.SOURCE: 注解仅保留在源文件中,编译器会丢弃它。RetentionPolicy.CLASS: 注解被保留到.class文件中,但 JVM 加载.class文件时会忽略它。这是默认策略。RetentionPolicy.RUNTIME: 注解被保留到.class文件中,并且在运行时可以被 JVM 读取。
3. 使用方法 (Usage)
3.1 内置注解 (Built-in Annotations)
Java 提供了一些内置的注解。
@Override: 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。@Deprecated: 标记过时的方法。如果使用该方法,编译器会发出警告。@SuppressWarnings: 指示编译器去忽略注解中声明的警告。
示例:
public class Parent {
public void sayHello() {
System.out.println("Hello from Parent");
}
}
public class Child extends Parent {
@Override
public void sayHello() {
System.out.println("Hello from Child");
}
@Deprecated
public void oldMethod() {
System.out.println("This is an old method.");
}
@SuppressWarnings("deprecation")
public void useOldMethod() {
oldMethod();
}
}3.2 自定义注解 (Custom Annotations)
我们可以创建自己的注解。定义注解的语法类似于定义接口,但使用 @interface 关键字。
步骤 1: 定义注解
@Target 定义了注解可以应用在哪些元素上(如类、方法、字段)。
@Retention 定义了注解的生命周期。
Note
注解注解的注解称之为元注解
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 MyCustomAnnotation {
String value() default "default value";
int count() default 1;
}步骤 2: 使用注解
public class MyService {
@MyCustomAnnotation(value = "test method", count = 3)
public void doSomething() {
System.out.println("Executing doSomething...");
}
}步骤 3: 处理注解 (通过反射)
import java.lang.reflect.Method;
public class AnnotationProcessor {
public static void main(String[] args) throws Exception {
Class<?> clazz = MyService.class;
Method method = clazz.getMethod("doSomething");
if (method.isAnnotationPresent(MyCustomAnnotation.class)) {
MyCustomAnnotation annotation = method.getAnnotation(MyCustomAnnotation.class);
String value = annotation.value();
int count = annotation.count();
for (int i = 0; i < count; i++) {
System.out.println("Value: " + value);
}
// 调用方法
method.invoke(new MyService());
}
}
}运行 AnnotationProcessor 的输出:
Value: test method
Value: test method
Value: test method
Executing doSomething...
这演示了如何定义自定义注解,将其应用于方法,然后使用反射在运行时读取注解的值并根据这些值执行操作。
4. 应用场景
使用注解开发的简易版 JUnit 框架的示例代码。
这个示例将包含三个核心部分:
-
MyTest.java: 自定义注解。 -
SampleTest.java: 包含待测试方法的类。 -
MyJunitRunner.java: 模拟 JUnit 的主程序,用于执行测试。
第一步:定义自定义注解 @MyTest
这个注解将作为标记,告诉我们的执行器哪些方法是需要被执行的测试方法。
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) 限制这个注解只能用于方法上。
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyTest {
}第二步:创建包含测试方法的类
这个类中包含多个方法,其中一部分使用了我们刚刚定义的 @MyTest 注解,另一部分则没有。
public class SampleTest {
@MyTest
public void testMethod1() {
System.out.println("测试方法 testMethod1() 被执行...");
}
public void normalMethod() {
System.out.println("我是一个普通方法,不应该被执行。");
}
@MyTest
public void testMethod2() {
System.out.println("测试方法 testMethod2() 被执行...");
}
@MyTest
public void testMethodWithException() {
System.out.println("测试方法 testMethodWithException() 被执行,即将抛出异常...");
int a = 1 / 0; // 模拟一个执行失败的测试
}
public void anotherNormalMethod() {
System.out.println("我也是一个普通方法。");
}
}第三步:创建测试执行器
这是我们简易框架的核心。它会加载 SampleTest 类,通过反射检查所有方法,如果方法上存在 @MyTest 注解,就执行该方法。
import java.lang.reflect.Method;
public class MyJunitRunner {
public static void main(String[] args) throws Exception {
// 1. 获取目标测试类的Class对象
Class<SampleTest> clazz = SampleTest.class;
// 2. 创建测试类的实例
SampleTest obj = clazz.getDeclaredConstructor().newInstance();
// 3. 获取该类中定义的所有方法
Method[] methods = clazz.getDeclaredMethods();
// 4. 遍历所有方法
for (Method method : methods) {
// 5. 判断方法上是否存在 @MyTest 注解
if (method.isAnnotationPresent(MyTest.class)) {
try {
// 6. 如果存在,则调用(执行)该方法
System.out.println("即将执行被 @MyTest 标注的方法: " + method.getName());
method.invoke(obj);
} catch (Exception e) {
// 捕获并打印测试方法在执行中抛出的异常
System.out.println("方法 " + method.getName() + " 执行时发生异常: " + e.getCause());
}
}
}
}
}预期输出
控制台看到类似以下的输出(方法的执行顺序可能因反射机制而不同):
即将执行被 @MyTest 标注的方法: testMethod1
测试方法 testMethod1() 被执行...
即将执行被 @MyTest 标注的方法: testMethod2
测试方法 testMethod2() 被执行...
即将执行被 @MyTest 标注的方法: testMethodWithException
测试方法 testMethodWithException() 被执行,即将抛出异常...
方法 testMethodWithException 执行时发生异常: java.lang.ArithmeticException: / by zero
代码解析
-
@Retention(RetentionPolicy.RUNTIME): 这是自定义注解中至关重要的一步。Java注解有三种保留策略:SOURCE(仅源码可见)、CLASS(编译后可见,但运行时丢弃)、RUNTIME(运行时依然保留)。我们必须使用RUNTIME,这样才能在程序运行时通过反射机制读取到它。 -
@Target(ElementType.METHOD): 这指定了我们的@MyTest注解只能用来修饰方法,如果尝试用在类或者字段上,编译器会报错。 -
MyJunitRunner.java:-
Class.forName("SampleTest")或SampleTest.class: 获取目标类的 “类模板” 对象。 -
clazz.getDeclaredMethods(): 使用反射获取这个类中声明的所有方法,无论其访问修饰符是public,private还是protected。 -
method.isAnnotationPresent(MyTest.class): 检查当前遍历到的方法上是否存在MyTest类型的注解。 -
method.invoke(obj): 如果注解存在,就使用反射来调用这个方法。invoke的第一个参数是需要调用该方法的对象实例。
-