登陆、权限拦截、日志处理,以及各种 Java 框架,如 Spring,Hibernate,JUnit 提到注解就不能不说反射,Java 自定义注解是通过运行时靠反射获取注解。实际开发中,例如我们要获取某个方法的调用日志,可以通过 AOP(动态代理机制)给方法添加切面,通过反射来获取方法包含的注解,如果包含日志注解,就进行日志记录。反射的实现在 Java 应用层面上讲,是通过对 Class 对象的操作实现的,Class 对象为我们提供了一系列方法对类进行操作。在 JVM 这个角度来说,Class 文件是一组以 8 位字节为基础单位的二进制流,各个数据项目按严格的顺序紧凑的排列在 Class 文件中,里面包含了类、方法、字段等等相关数据。通过对 Class 数据流的处理我们即可得到字段、方法等数据。

以上是注解机制的骨架。下面补全三块实战中最需要的内容:注解本身的定义要素、运行时读取的完整链路、以及一个可运行的自定义注解实现。

定义注解的四个要素

@Target(ElementType.METHOD)          // 能标在哪里:方法/类/字段/参数
@Retention(RetentionPolicy.RUNTIME) // 活到什么时候:RUNTIME 才能反射读
@Documented
public @interface OpLog {
String value(); // 操作描述
LogLevel level() default LogLevel.INFO;
}
  • @Retention 是关键:SOURCE(编译期丢弃,如 @Override)、CLASS(进 class 文件但运行期不可读)、RUNTIME(反射可见)。自定义功能注解几乎总是 RUNTIME
  • @Target 约束使用位置,编译期检查
  • 成员类型受限:基本类型、String、Class、枚举、注解及它们的数组;default 值让使用方省略参数

运行时读取链路

注解在 Class 文件中存于 RuntimeVisibleAnnotations 属性;类加载后 JVM 把它解析为注解代理对象。应用层读取就是一条反射链:

Method m = UserService.class.getMethod("create", User.class);
OpLog ann = m.getAnnotation(OpLog.class); // 无注解返回 null
if (ann != null) log(ann.value(), ann.level());

配合 AOP 时,切面拦截方法执行,从 MethodSignature 拿到 Method 再取注解——**注解负责声明”做什么”,切面负责执行”怎么做”**,声明与实现分离,这就是注解驱动的全部秘密。

一个完整的日志注解实现

@Aspect
@Component
public class OpLogAspect {

@Around("@annotation(opLog)")
public Object around(ProceedingJoinPoint pjp, OpLog opLog) throws Throwable {
long start = System.nanoTime();
try {
Object ret = pjp.proceed();
log.info("[{}] 成功, 耗时{}ms, 参数={}",
opLog.value(), (System.nanoTime() - start) / 1_000_000,
Arrays.toString(pjp.getArgs()));
return ret;
} catch (Throwable t) {
log.error("[{}] 失败: {}", opLog.value(), t.getMessage());
throw t;
}
}
}

// 业务侧零侵入:
@OpLog("创建用户")
public User create(User user) { ... }

典型场景清单

  • 横切关注点:日志(如上)、权限(@PreAuthorize)、限流(@RateLimit)、重试(@Retryable)、缓存(@Cacheable)
  • 框架配置元数据:Spring 的 @Service/@RequestMapping、MyBatis 的 @Select、JUnit 的 @Test——注解替代 XML 成为配置载体
  • 编译期处理:Lombok 用 SOURCE 级注解 + 注解处理器(APT)在编译期改写 AST 生成 getter;这是另一条路线,运行期零开销
  • 校验与文档:@NotNull、@Schema 等声明式约束

设计自定义注解的经验

  • 注解只放元数据,不放逻辑;逻辑在处理器(切面/处理器/扫描器)里
  • 命名用动词性或名词性短语表达意图(@OpLog、@Retryable),避免 @Config1 这类无语义命名
  • 提供合理 default,让 80% 的使用场景只需写 @Xxx 一个词
  • 想清楚 Retention:能在编译期解决的(生成代码)不要用 RUNTIME,运行期反射是有成本的

注解的本质是”贴在代码上的结构化注释 + 一套读取它的处理器”。自定义注解不难,难的是把”声明”与”处理”的边界划干净——划干净了,业务代码里就只剩下一个个意图清晰的标记。