前言
注解是 Java 提供的一种元编程能力,它像标签一样贴在代码的类、方法、字段上,可以被编译器或运行时读取并处理。从 Java 5 引入至今,注解已经彻底改变了 Java 生态 ------ Spring、Lombok、JUnit 等框架的核心都离不开注解。
但很多开发者对注解的理解停留在 @Override 和 @Autowired 的用法上,不清楚注解的本质 是什么,也不知道如何编写自定义注解,更不了解编译期注解处理器(APT) 这种黑科技。
本文将带你从零到一掌握注解,并手写一个简单的 @Getter 注解处理器,生成 getter 方法。
一、注解的本质是什么?
先看一个常见注解 @Override 的源码:
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
使用 @interface 关键字定义注解,它本质上是一个继承自 java.lang.annotation.Annotation 接口的特殊接口。反编译后可以看到:
java
public interface Override extends Annotation {
}
注解可以包含成员(属性),形式类似于方法:
java
public @interface MyAnnotation {
String value() default "";
int count() default 0;
}
使用时:@MyAnnotation(value = "hello", count = 5)
二、元注解 ------ 注解的注解
| 元注解 | 作用 |
|---|---|
@Target |
限定注解可以贴在哪些位置(类、方法、字段、参数等) |
@Retention |
注解保留到何时(源码、字节码、运行时) |
@Documented |
是否被 Javadoc 收录 |
@Inherited |
子类是否可以继承父类上的该注解 |
@Repeatable (Java 8) |
允许在同一位置重复使用同一注解 |
2.1 Retention 的三种策略
| 策略 | 生效时机 | 典型场景 |
|---|---|---|
SOURCE |
仅存在源码中,编译后丢弃 | 代码检查(@Override)、Lombok |
CLASS |
编译时保留在字节码中,但运行时不可见 | 字节码插桩工具 |
RUNTIME |
运行时仍可通过反射读取 | Spring、JUnit(运行时需要读取) |
java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
2.2 Target 常用取值
java
ElementType.TYPE // 类、接口、枚举
ElementType.FIELD // 字段
ElementType.METHOD // 方法
ElementType.PARAMETER // 方法参数
ElementType.CONSTRUCTOR // 构造器
ElementType.LOCAL_VARIABLE // 局部变量
ElementType.ANNOTATION_TYPE // 注解类型
ElementType.PACKAGE // 包
三、自定义注解并运行时解析(反射)
3.1 定义一个权限检查注解
java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiresPermission {
String value();
}
3.2 使用注解
java
public class UserService {
@RequiresPermission("admin")
public void deleteUser(Long userId) {
System.out.println("删除用户: " + userId);
}
public void listUsers() {
System.out.println("列出所有用户");
}
}
3.3 通过反射读取注解并处理
java
public class SecurityAspect {
public static void invokeWithPermissionCheck(Object obj, String methodName, Object... args)
throws Exception {
Method method = obj.getClass().getMethod(methodName, getParameterTypes(args));
if (method.isAnnotationPresent(RequiresPermission.class)) {
RequiresPermission anno = method.getAnnotation(RequiresPermission.class);
String required = anno.value();
if (!"admin".equals(getCurrentUserRole())) {
throw new SecurityException("权限不足,需要: " + required);
}
}
method.invoke(obj, args);
}
private static String getCurrentUserRole() {
// 模拟获取当前登录用户角色
return "guest";
}
private static Class<?>[] getParameterTypes(Object[] args) {
return Arrays.stream(args).map(Object::getClass).toArray(Class[]::new);
}
public static void main(String[] args) throws Exception {
UserService service = new UserService();
invokeWithPermissionCheck(service, "deleteUser", 1L); // 抛出异常
}
}
运行时注解是 Spring AOP 的实现基石,但反射有一定性能开销。
四、编译期注解处理器(APT)------ Lombok 的秘密
Lombok 的 @Getter、@Data 就是在编译期修改抽象语法树(AST),生成代码。我们来实现一个简化版 @Getter。
4.1 定义注解(保留到 SOURCE)
java
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface Getter {
}
4.2 编写注解处理器
需要依赖 javax.annotation.processing.* 和 com.sun.source.tree.*,Maven 需添加 tools.jar。
java
@SupportedAnnotationTypes("com.example.Getter")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class GetterProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(Getter.class)) {
if (element.getKind() != ElementKind.FIELD) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@Getter only applies to fields", element);
continue;
}
// 获取包含该字段的类
Element enclosingClass = element.getEnclosingElement();
String fieldName = element.getSimpleName().toString();
TypeMirror fieldType = element.asType();
String methodName = "get" + capitalize(fieldName);
// 生成 getter 方法的代码字符串
String code = "public " + fieldType + " " + methodName + "() { return " + fieldName + "; }";
// 通过 Filer 写入新文件过于复杂,实际 Lombok 直接修改 AST
// 这里简化演示:输出信息
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,
"Generated getter: " + code, element);
}
return true;
}
private String capitalize(String str) {
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
}
注意 :真正修改类字节码需要使用 Java 编译器 API 或 Lombok 的
JavacAnnotationHandler机制。感兴趣的可以研究 Lombok 源码。但了解 APT 工作原理已经能帮你理解很多框架。
4.3 注册注解处理器
在 resources/META-INF/services/javax.annotation.processing.Processor 文件中写入处理器的全限定名。
五、注解在主流框架中的应用
| 框架 | 注解示例 | 用途 |
|---|---|---|
| Spring | @Controller, @Autowired, @Transactional |
IoC、AOP、声明式事务 |
| JUnit | @Test, @BeforeEach, @ParameterizedTest |
单元测试生命周期 |
| Lombok | @Data, @Slf4j |
编译期代码生成 |
| Jackson | @JsonProperty, @JsonIgnore |
序列化/反序列化控制 |
| Hibernate | @Entity, @Column, @OneToMany |
ORM 映射 |
| Feign | @FeignClient, @GetMapping, @RequestParam |
声明式 HTTP 客户端 |
| Swagger | @ApiOperation, @ApiParam |
API 文档生成 |
六、最佳实践与常见坑
6.1 ❌ 错误:试图在 SOURCE 注解上使用反射
java
@Retention(RetentionPolicy.SOURCE)
@interface A {}
// 运行时获取不到,返回 null
A a = SomeClass.class.getAnnotation(A.class); // null
6.2 ❌ 错误:注解成员类型不受支持
支持的成员类型:基本类型、String、Class、枚举、注解、以及上述类型的数组。不能用普通对象或包装类。
java
// 编译错误
public @interface Bad {
Integer value(); // Integer 不允许,应用 int
}
6.3 ✅ 使用默认值减少冗余
java
public @interface Mapping {
String path() default "";
RequestMethod method() default RequestMethod.GET;
}
6.4 ✅ 注解作为元数据,业务的逻辑应由代码实现
注解不是魔法,它是数据。真正干活的还是你的拦截器、AOP、处理器。
七、面试高频题
Q1: 注解可以被继承吗?
被 @Inherited 标记的注解,当贴在父类上时,子类可以自动继承(但仅限于类继承,接口不行)。反射获取时需要看是否有该元注解。
Q2: 如何在同一属性上重复使用同一个注解?
Java 8 引入 @Repeatable,定义容器注解:
java
@Repeatable(Roles.class)
@interface Role { String value(); }
@interface Roles { Role[] value(); }
// 使用
@Role("admin") @Role("user")
public void doSomething() {}
Q3: 运行时注解的性能影响?
每次 getAnnotation 都会遍历注解属性并生成动态代理对象,频繁调用有明显损耗。建议在启动时缓存。
总结
| 保留策略 | 可见性 | 典型应用 |
|---|---|---|
| SOURCE | 编译后消失 | Lombok、代码检查 |
| CLASS | 字节码保留,运行时不可见 | 字节码框架(ASM) |
| RUNTIME | 反射可见 | Spring、ORM、测试框架 |
注解是 Java 元编程的入口,掌握它意味着你能理解主流框架的设计思想,甚至写出自己的代码生成工具。如果你对 APT 感兴趣,强烈建议去读一下 Lombok 源码,会让你对 Java 编译过程的理解跃升一个台阶。