注解解析与反射协作 | JavaSE
注解解析与反射协作
一、学习目标
完成本章后,你应该能够:
- 能够解释什么是注解解析(Annotation Parsing),以及为什么注解通常需要配合反射使用。
- 能够建立“注解负责描述,反射负责读取和执行”的核心模型。
- 能够使用
AnnotatedElement提供的 API 判断、获取和读取运行时注解。 - 能够解析类、方法、字段、构造器上的注解。
- 能够手写一个基于
@MyTest的简易测试执行器。 - 能够理解注解驱动框架的基本运行流程,为后续 Spring、JUnit 等框架原理建立基础。
二、核心知识
2.1 什么是注解解析
上一章已经学习了如何定义和使用注解,例如:
@MyTest
public void loginTest() {
}
但是,仅仅把 @MyTest 写在方法上,程序不会自动执行任何特殊逻辑。
注解本质上是在程序元素上附加的一份元数据(Metadata)。
真正让注解产生作用的是另外一段程序:
- 找到被注解的类、方法、字段或构造器。
- 判断它上面是否存在指定注解。
- 获取注解对象。
- 读取注解中的属性。
- 根据这些属性决定接下来执行什么逻辑。
这个过程就叫:
注解解析。
因此必须建立一个非常重要的认识:
注解负责“描述规则”,解析器负责“读取规则并执行规则”。
例如:
@MyTest
public void testLogin() {
}
@MyTest 的意思不是:
JVM 看到它以后自动执行
testLogin()。
而更接近:
这里留下了一个标记,某个测试框架以后可以扫描这个标记,并决定是否执行这个方法。
2.2 注解与反射为什么天然需要协作
假设程序运行以后,需要回答:
UserService类的login()方法上有没有@MyTest?
普通 Java 代码如果提前不知道具体类和方法,很难实现通用处理。
而反射恰好可以在运行时得到:
类
↓
Class
构造器
↓
Constructor
字段
↓
Field
方法
↓
Method
这些反射对象不仅可以描述程序结构,还具有读取注解的能力。
于是:
注解
负责保存元数据
+
反射
负责运行时找到程序元素
↓
注解解析器
↓
根据注解决定程序行为
这就是:
注解 + 反射 = Java 框架中非常重要的一种元数据驱动机制。
2.3 核心原则:要解析谁,就先拿到谁
这是本章最重要的一句话。
解析类上的注解
先得到:
Class<?> clazz
然后:
clazz.getDeclaredAnnotation(...)
解析方法上的注解
先得到:
Method method
然后:
method.getDeclaredAnnotation(...)
解析字段上的注解
先得到:
Field field
然后:
field.getDeclaredAnnotation(...)
解析构造器上的注解
先得到:
Constructor<?> constructor
然后:
constructor.getDeclaredAnnotation(...)
因此可以总结:
类上的注解
↓
Class
方法上的注解
↓
Method
字段上的注解
↓
Field
构造器上的注解
↓
Constructor
这就是注解解析和反射协作的连接点。
2.4 AnnotatedElement 接口
Java 将“可以拥有并读取注解的程序元素”进行了统一抽象:
java.lang.reflect.AnnotatedElement
Class、Method、Field、Constructor 等反射类型都具有 AnnotatedElement 所定义的注解读取能力。
所以我们才可以统一写出:
clazz.isAnnotationPresent(...);
method.isAnnotationPresent(...);
field.isAnnotationPresent(...);
constructor.isAnnotationPresent(...);
常用 API 如下:
| API | 作用 |
| ----------------------------------- | -------------------------------------------- |
| isAnnotationPresent(...) | 判断指定注解是否存在 |
| getAnnotation(...) | 获取“存在于”当前元素上的指定注解 |
| getDeclaredAnnotation(...) | 获取直接声明在当前元素上的指定注解 |
| getAnnotations() | 获取存在于当前元素上的所有注解 |
| getDeclaredAnnotations() | 获取直接声明在当前元素上的所有注解 |
| getAnnotationsByType(...) | 获取指定类型的一个或多个注解,支持可重复注解 |
| getDeclaredAnnotationsByType(...) | 获取当前元素直接或间接声明的指定类型注解 |
最常见的入门组合是:
if (element.isAnnotationPresent(MyTest.class)) {
MyTest myTest = element.getDeclaredAnnotation(MyTest.class);
}
2.5 getAnnotation() 与 getDeclaredAnnotation()
两者非常容易混淆。
getDeclaredAnnotation()
强调:
直接声明在当前程序元素上的注解。
例如:
MyTest annotation =
method.getDeclaredAnnotation(MyTest.class);
没有直接声明则返回:
null
getAnnotation()
表示获取当前元素上“存在”的指定注解。
对于普通方法、字段等场景,两者通常没有明显差别。
但是对于类注解继承,需要额外注意 @Inherited。
例如:
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Role {
}
父类:
@Role
class Parent {
}
子类:
class Child extends Parent {
}
此时:
Child.class.getAnnotation(Role.class);
可能找到从父类继承而来的 @Role。
而:
Child.class.getDeclaredAnnotation(Role.class);
只关注:
Child自己有没有直接声明@Role。
因此在写框架或通用组件时,必须先明确:
我要找“直接声明”,还是“包含继承语义的存在关系”?
2.6 为什么运行时解析必须使用 RUNTIME
假设定义:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyTest {
}
其中:
RetentionPolicy.RUNTIME
非常关键。
三种保留策略可以理解为:
SOURCE
源码阶段存在
↓
编译后丢弃
CLASS
源码存在
↓
class 文件存在
↓
运行时不保证提供给反射读取
RUNTIME
源码存在
↓
class 文件存在
↓
JVM 运行期间仍然保留
↓
反射可以读取
因此:
希望通过反射解析的注解,通常必须使用
RetentionPolicy.RUNTIME。
否则可能出现:
method.isAnnotationPresent(MyTest.class)
永远得到:
false
即使源码里明明写着:
@MyTest
三、使用方法
3.1 定义一个运行时类注解
例如定义一个组件注解:
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 Component {
String value();
}
使用:
@Component("userService")
public class UserService {
}
这段代码本身不会自动注册 UserService。
我们需要写解析程序。
3.2 解析类上的注解
Class<UserService> clazz = UserService.class;
if (clazz.isAnnotationPresent(Component.class)) {
Component component =
clazz.getDeclaredAnnotation(Component.class);
String value = component.value();
System.out.println(value);
}
执行逻辑:
UserService.class
↓
获得 Class 对象
↓
判断有没有 @Component
↓
获得 Component 注解对象
↓
调用 component.value()
↓
获得 "userService"
这里有一个非常有意思的地方。
我们定义的是:
public @interface Component {
String value();
}
解析后却可以:
component.value();
这是因为运行时拿到的是一个实现了该注解接口语义的注解对象。
因此:
component.value()
就是读取:
@Component("userService")
中的:
userService
3.3 解析方法上的注解
定义:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyTest {
int order() default 0;
boolean enabled() default true;
}
使用:
public class UserService {
@MyTest(order = 1)
public void login() {
System.out.println("login");
}
}
解析:
Class<UserService> clazz = UserService.class;
Method method = clazz.getDeclaredMethod("login");
if (method.isAnnotationPresent(MyTest.class)) {
MyTest myTest =
method.getDeclaredAnnotation(MyTest.class);
System.out.println(myTest.order());
System.out.println(myTest.enabled());
}
整个过程可以记成六个字:
拿对象 → 查注解 → 读属性
3.4 通用注解解析流程
今后看到任何类似问题,都可以按照下面的模型解决。
第一步:获取 Class
Class<?> clazz = Xxx.class;
或者:
Class<?> clazz = obj.getClass();
第二步:得到目标程序元素
例如方法:
Method method =
clazz.getDeclaredMethod("login");
字段:
Field field =
clazz.getDeclaredField("name");
第三步:判断注解是否存在
boolean exists =
method.isAnnotationPresent(MyTest.class);
第四步:获取注解对象
MyTest annotation =
method.getDeclaredAnnotation(MyTest.class);
第五步:读取注解属性
int order = annotation.order();
boolean enabled = annotation.enabled();
第六步:根据元数据执行逻辑
例如:
if (enabled) {
method.invoke(target);
}
真正的框架核心往往就在第六步。
3.5 一个非常重要的模型:元数据驱动
传统代码经常是:
if (...) {
doA();
}
if (...) {
doB();
}
而框架型代码更希望做到:
@MyTest
public void testLogin() {
}
框架扫描以后:
发现 @MyTest
↓
识别规则
↓
执行 testLogin()
也就是将:
“程序要做什么”
的一部分信息写进:
注解
然后由一个统一框架解释这些信息。
这称为:
元数据驱动(Metadata-driven)设计。
3.6 综合案例:自制简易测试框架
先定义测试注解:
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 MyTest {
int order() default 0;
boolean enabled() default true;
}
业务类:
public class UserService {
@MyTest(order = 2)
public void deleteUser() {
System.out.println("deleteUser executed");
}
@MyTest(order = 1)
public void login() {
System.out.println("login executed");
}
public void helper() {
System.out.println("helper executed");
}
}
现在定义测试运行器:
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;
public class MyTestRunner {
public static void main(String[] args) throws Exception {
// 1. 获得被测试类
Class<UserService> clazz = UserService.class;
// 2. 创建被测试对象
UserService target =
clazz.getDeclaredConstructor().newInstance();
// 3. 获取类中声明的方法
Method[] methods = clazz.getDeclaredMethods();
// 4. 根据 @MyTest.order 排序
Arrays.sort(methods, Comparator.comparingInt(method -> {
MyTest test =
method.getDeclaredAnnotation(MyTest.class);
return test == null
? Integer.MAX_VALUE
: test.order();
}));
// 5. 扫描方法
for (Method method : methods) {
// 获取 @MyTest
MyTest test =
method.getDeclaredAnnotation(MyTest.class);
// 没有注解,不执行
if (test == null) {
continue;
}
// enabled = false,不执行
if (!test.enabled()) {
continue;
}
// 当前简易框架只支持无参数方法
if (method.getParameterCount() != 0) {
System.out.println(
"SKIP: " + method.getName()
+ " requires parameters"
);
continue;
}
System.out.println(
"RUN: " + method.getName()
);
// 反射调用测试方法
method.invoke(target);
}
}
}
输出可能类似:
RUN: login
login executed
RUN: deleteUser
deleteUser executed
注意:
helper()
没有:
@MyTest
所以不会执行。
这已经非常接近一个微型测试框架的基本思想。
四、原理与进阶
4.1 注解本身不会主动执行代码
这是初学者最容易形成的错误认识。
例如:
@Transactional
public void pay() {
}
不能简单理解成:
Java 语言看到
@Transactional就自动开始事务。
更准确的模型应该是:
@Transactional
只是留下元数据
↓
某个框架发现它
↓
框架按照自己的规则解释它
↓
框架执行事务相关逻辑
因此:
注解本身是“信息”,框架才是“行为”。
4.2 一个注解驱动框架通常经历什么
一个简化的框架流程可以抽象成:
① 扫描
发现 Class / Method
↓
② 识别
isAnnotationPresent()
↓
③ 读取
getAnnotation()
getDeclaredAnnotation()
↓
④ 解析
annotation.value()
annotation.xxx()
↓
⑤ 建模
保存为内部配置对象
↓
⑥ 执行
newInstance()
invoke()
get()
set()
...
↓
⑦ 缓存
减少重复扫描与反射解析
真正成熟的框架当然比这里复杂得多,但核心思想已经出现了。
4.3 注解解决“配置”,反射解决“通用”
例如现在有:
@MyTest
public void loginTest() {
}
如果没有反射,测试框架可能必须写:
obj.loginTest();
这样框架必须提前知道:
方法叫 loginTest
那么:
registerTest()
deleteTest()
addTest()
每出现一个新方法,框架代码就可能需要修改。
有反射以后:
Method[] methods = clazz.getDeclaredMethods();
框架不需要提前知道方法名。
再使用:
method.isAnnotationPresent(MyTest.class)
就可以动态筛选。
于是:
反射
解决“不提前知道具体是谁”
注解
解决“如何描述它应该做什么”
两者组合起来,就是很多框架通用能力的来源。
4.4 最好把框架逻辑拆成三层
实际开发时,不建议把所有逻辑塞进一个循环。
可以拆成:
发现 Discovery
↓
找到候选类和方法
解析 Parsing
↓
读取注解和属性
执行 Execution
↓
真正调用业务逻辑
例如:
scanMethods();
parseMetadata();
executeTests();
这样比:
for (...) {
// 扫描
// 判断
// 解析
// 校验
// 调用
// 异常处理
// 日志
}
全部堆在一起更容易维护。
4.5 反射解析通常会被缓存
如果一个框架每次方法调用都重新:
clazz.getDeclaredMethods();
再不断:
method.getDeclaredAnnotation(...);
会产生不必要的重复工作。
所以成熟框架通常会在:
启动阶段
或第一次使用阶段解析元数据,再保存起来。
例如概念上:
Map<Method, MyTestMetadata> cache;
后续直接使用缓存结果。
这也是:
框架启动阶段通常比普通业务代码做更多扫描和初始化工作的原因之一。
五、实践应用
5.1 单元测试框架
可以使用:
@Test
标识测试方法。
测试框架:
扫描类
↓
寻找 @Test
↓
创建测试对象
↓
调用测试方法
↓
统计结果
我们本章的:
@MyTest
就是对这种思想的简化模拟。
5.2 Web 请求映射
以后学习 Spring MVC 时会看到类似:
@GetMapping("/users")
它表达的是:
HTTP GET /users
↓
应该交给这个方法处理
框架可以通过注解元数据建立:
URL
↓
Controller Method
之间的映射关系。
5.3 依赖注入
例如:
@Autowired
其背后的基本思想之一也是:
扫描字段 / 构造器 / 方法
↓
读取注解
↓
找到需要的对象
↓
完成依赖关系装配
5.4 ORM 与对象映射
例如概念上:
@Table("user")
class User {
@Column("user_name")
private String username;
}
框架可以读取:
类注解
↓
表名
字段注解
↓
列名
再利用反射:
field.get(...)
field.set(...)
完成对象和数据之间的通用映射。
5.5 权限、缓存、事务等声明式功能
以后可能看到:
@RequiresRole("ADMIN")
@Cacheable
@Transactional
这种代码的共同思想都是:
程序员声明“我要什么”
↓
框架解析注解
↓
框架替程序员执行大量通用逻辑
这也是现代 Java 框架能够大量减少模板代码的重要原因。
六、常见问题
6.1 明明写了注解,为什么反射获取不到?
首先检查:
@Retention(RetentionPolicy.RUNTIME)
如果是:
SOURCE
或者:
CLASS
就不能按照普通运行时反射注解的思路处理。
6.2 getAnnotation() 返回什么?
找到:
MyTest
则返回:
MyTest
类型的注解对象。
找不到:
null
因此通常需要:
MyTest test =
method.getDeclaredAnnotation(MyTest.class);
if (test != null) {
...
}
6.3 isAnnotationPresent() 与 getAnnotation() 要不要同时调用?
可以:
if (method.isAnnotationPresent(MyTest.class)) {
MyTest test =
method.getDeclaredAnnotation(MyTest.class);
}
也可以直接:
MyTest test =
method.getDeclaredAnnotation(MyTest.class);
if (test != null) {
}
第二种有时更加紧凑,因为避免了“先判断、再读取”的重复表达。
具体使用哪种取决于代码可读性。
6.4 为什么 getMethod() 找不到私有方法?
因为:
getMethod()
主要用于获取公开方法。
如果要获取类自己声明的方法,包括非 public 方法,应考虑:
getDeclaredMethod()
同样:
getMethods()
与:
getDeclaredMethods()
的语义也不同。
6.5 为什么找到方法却不能执行?
可能涉及:
IllegalAccessException
或者方法参数不匹配。
例如:
public void test(String name)
却:
method.invoke(obj);
显然缺少参数。
因此框架必须定义自己的执行规则,例如:
当前
@MyTest只允许标记无参实例方法。
这就是:
框架约定(Convention)。
6.6 Method.invoke() 抛出的异常为什么看起来套了一层?
如果目标方法内部发生异常,反射调用通常会通过:
InvocationTargetException
包装目标方法抛出的异常。
真正的原始异常可以进一步通过:
getCause()
或:
getTargetException()
查看。
这也是以后实现测试框架时必须处理的问题。
6.7 @Inherited 会让方法注解也自动继承吗?
不能这样理解。
@Inherited 主要影响:
类声明上的注解继承查询。
不能简单认为父类方法上的某个注解会自动变成子类对应方法的注解。
6.8 注解是不是越多越高级?
不是。
注解适合:
- 描述元数据
- 声明配置
- 标记规则
- 配合框架统一解析
但如果把所有业务逻辑都隐藏到注解背后,代码反而可能:
- 难理解
- 难调试
- 难追踪执行流程
所以:
注解的价值在于表达稳定的声明式规则,而不是把代码变成“魔法”。
七、练习与验收
7.1 知识问答
- 什么是注解解析?注解本身为什么不会自动执行程序逻辑?
- 为什么解析类注解通常首先需要得到
Class对象? Class、Method、Field、Constructor与AnnotatedElement有什么关系?isAnnotationPresent()有什么作用?getAnnotation()与getDeclaredAnnotation()有什么区别?- 为什么运行时反射解析的自定义注解通常需要声明
RetentionPolicy.RUNTIME? - 如何理解“注解负责描述,反射负责读取和操作”?
- 一个简单的注解驱动框架通常需要经历哪些步骤?
7.2 代码阅读
阅读下面代码,在不运行程序的情况下分析:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface MyTest {
boolean enabled() default true;
}
class Demo {
@MyTest
public void a() {
System.out.println("A");
}
public void b() {
System.out.println("B");
}
}
回答:
Demo.class.getDeclaredMethods()可能得到哪些方法?- 哪个方法可以通过
isAnnotationPresent(MyTest.class)判断为存在指定注解? - 如果框架只执行具有
@MyTest的方法,哪些方法会执行? - 如果把
RetentionPolicy.RUNTIME改成SOURCE,运行时解析会受到什么影响?
7.3 手写代码
- 自定义一个:
@MyBook
要求可以记录:
- 书名
- 作者
- 价格
并编写程序通过反射读取。
- 自定义:
@MyTest
用于标记需要执行的方法。
- 编写一个注解解析器:
扫描一个类
↓
找到带 @MyTest 的方法
↓
输出方法名称
- 在上一题基础上,通过:
Method.invoke()
真正执行这些方法。
7.4 Debug
下面代码为什么可能无法解析注解?
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
@interface MyTest {
}
class Demo {
@MyTest
public void test() {
}
}
请:
- 找出问题。
- 说明原因。
- 修改代码,使运行时反射能够读取注解。
再分析:
Method method =
Demo.class.getDeclaredMethod("login");
MyTest test =
method.getDeclaredAnnotation(MyTest.class);
System.out.println(test.enabled());
如果 login() 没有 @MyTest,程序会发生什么?
请设计安全写法。
7.5 综合训练
实现一个简易测试框架:
@MyTest
要求:
- 只能标记方法。
- 注解运行时可见。
- 提供
enabled属性。 - 提供
order属性。 - 扫描指定测试类。
- 只执行标记了
@MyTest的方法。 enabled=false的方法不执行。- 按
order排序。 - 输出每个方法的执行状态。
- 某个测试方法失败后不能影响其他测试继续执行。
- 最终统计成功数、失败数、跳过数。
7.6 本章验收
如果能够在不查看资料的情况下完整解释下面这条链路,本章即可认为基本掌握:
自定义注解
↓
@Target
↓
@Retention(RUNTIME)
↓
标记类 / 方法 / 字段
↓
获取 Class
↓
获取 Method / Field / Constructor
↓
AnnotatedElement
↓
isAnnotationPresent()
↓
getDeclaredAnnotation()
↓
读取注解属性
↓
根据元数据执行逻辑
↓
Method.invoke()
最终必须能够口述:
注解本身只是元数据。框架通过反射在运行时发现程序元素,再通过 AnnotatedElement API 读取注解,根据注解中的配置决定如何创建对象、调用方法或执行其他通用逻辑。这就是注解解析与反射协作的核心。