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

相关推荐
djjdjdjdjjdj2 小时前
如何用参数解构在函数入口处直接提取对象属性
jvm·数据库·python
MegaDataFlowers2 小时前
调用Service层操作数据
java·开发语言
forEverPlume2 小时前
mysql如何批量增加表的字段_脚本化DDL操作实践
jvm·数据库·python
精益数智工坊2 小时前
物料管理是什么?物料管理的具体工作有哪些?
大数据·前端·数据库·人工智能·精益工程
m0_596406372 小时前
CSS如何高效引入样式表_对比link标签与import指令的性能差异
jvm·数据库·python
行云的逆袭2 小时前
树莓派4B安装adminer数据库简易工具
数据库
solihawk2 小时前
服务器内存被谁“偷”走了?
服务器·数据库
user_admin_god2 小时前
SSE 流式响应 Chunk 被截断问题的排查与修复
java·人工智能·spring boot·spring·maven·mybatis
我命由我123453 小时前
Java 开发 - CountDownLatch 不需要手动关闭
android·java·开发语言·jvm·kotlin·android studio·android-studio