Java 注解(Annotation)详解:从基础到 APT 实战

前言

注解是 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 编译过程的理解跃升一个台阶。

相关推荐
Dicky-_-zhang1 分钟前
日志管理实战:ELK与Loki对比选型与落地实践
java·jvm
nJI74egg117 分钟前
JavaEE初阶---《JUC 并发编程完全指南:组件用法、原理剖析与面试应答》
java·面试·java-ee
刮风那天25 分钟前
Android AMS创建进程不用Binder而用Socket?
android·java·binder
程序员老邢31 分钟前
【技术底稿 37】Spring Boot 3.x 自动装配 “死锁” 排查:3 个注解实现条件化装配与 Mock 兜底
java·spring boot·后端·自动装配·rag·技术底稿
用户4343092416938 分钟前
Day29:图片上传 + 存数据库(Multer + MySQL)
数据库·后端
日月云棠1 小时前
JAVA数据结构与算法 - 基础:链表
java·后端
日月云棠1 小时前
JAVA数据结构与算法 - 基础:栈 (Stack) 深度解析
java·后端
xiguolangzi1 小时前
java使用Map映射遍历方法
java·后端
日月云棠1 小时前
JAVA数据结构与算法 - 基础:队列 (Queue) 全方位解析
java·后端
lolo大魔王1 小时前
MongoDB 索引机制详解:单字段索引、复合索引、唯一索引与性能优化
数据库·mongodb