当前位置: 首页 > news >正文

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,@TransactionalIoC、AOP、声明式事务
JUnit@Test,@BeforeEach,@ParameterizedTest单元测试生命周期
Lombok@Data,@Slf4j编译期代码生成
Jackson@JsonProperty,@JsonIgnore序列化/反序列化控制
Hibernate@Entity,@Column,@OneToManyORM 映射
Feign@FeignClient,@GetMapping,@RequestParam声明式 HTTP 客户端
Swagger@ApiOperation,@ApiParamAPI 文档生成

六、最佳实践与常见坑

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

http://www.jsqmd.com/news/705074/

相关文章:

  • 基于Git提交历史的本地AI代码助手:Machtiani深度解析与实践指南
  • AI代码沙箱化落地难题全解(2024企业级Docker隔离标准白皮书首发)
  • MCP 2026推理性能优化已进入“临界拐点”:2025年Q4起所有新上线模型将强制启用Dynamic Quantization Gate,你准备好这5项前置校验了吗?
  • 最后30天!Docker Hub官方宣布2026.0版本将停用旧版AI插件API:迁移 checklist、兼容性矩阵与回滚熔断方案(含CLI一键检测脚本)
  • 如何用开源项目Ryujinx在PC上免费畅玩Switch游戏?终极探索指南
  • 5分钟掌握ComfyUI-Impact-Pack:AI图像细节增强的终极指南
  • Inter字体完全指南:为数字界面选择最佳屏幕字体的终极解决方案
  • CyberChef:网络安全工程师的瑞士军刀终极指南
  • PyVision:让视觉大模型动态生成代码工具,突破传统视觉智能体局限
  • ThreadLocal 深度解析:从源码到内存泄漏,一篇就够了
  • EDMA3链式传输与中断机制深度解析
  • 苹果触控板在Windows系统的完美重生:mac-precision-touchpad驱动深度解析
  • ComfyUI-Crystools Pipe节点:彻底解决AI绘图工作流数据管理难题
  • 5步掌握罗技鼠标宏:让绝地求生压枪变得如此精准
  • 前端开发提效:用 OpenClaw 自动生成组件代码、兼容适配校验、打包部署前置检查实操
  • Dream-Creator:基于Stable Diffusion的本地AI图像生成工作站部署与实战
  • 哔咔漫画下载器完整指南:3倍速打造个人离线漫画库
  • 我现在能理解mvcc让读不阻塞,但是无法理解mvcc让写不阻塞??
  • EPIC-ADS7-PUC嵌入式系统:工业级性能与实时控制解析
  • 风控命中日志和决策日志怎么设计 别只讲概念,真正容易出问题的是链路、状态和治理
  • FanControl中文设置完全指南:5分钟让Windows风扇控制说中文
  • 如何快速搭建个人电视服务器:Tvheadend完整指南
  • WASM容器化部署为何在边缘失效?——资深SRE团队压测237个场景后的真实结论
  • 2026年Hermes Agent/OpenClaw如何部署?快速部署流程
  • ARM可信启动机制与安全实践解析
  • BrowserOS:基于AI智能体的开源浏览器自动化平台实战指南
  • 如何用录播姬BililiveRecorder实现专业级直播录制与修复
  • 如何用Win11Debloat给你的Windows系统做一次彻底的数字排毒 [特殊字符]
  • springboot基于Vue3的足球迷球圈网站内容文章更新系统的设计与实现
  • NetBox-Agent:自动化同步服务器硬件与网络信息至NetBox的实战指南