Annotation
Java 注解(Annotation)完全指南
目录
- 一、什么是注解
- 二、注解的原理
- 三、注解的使用
- 四、自定义注解完整流程
- 五、元注解详解
- 六、反射读取注解 API
- 七、实战案例:自定义参数校验注解
- 八、Spring AOP 通知方法的入参
- 九、@Pointcut 注解详解
- 十、常见坑与学习路径
一、什么是注解
注解是一种元数据(metadata),本质上是给代码"贴标签"。它不直接影响代码逻辑,但可以被编译器、工具、或运行时框架读取并据此做出行为。
类比理解:就像快递包裹上的"易碎"、"加急"标签——标签本身不改变包裹内容,但搬运工看到标签后会采取不同的处理方式。
@Override // 告诉编译器:这是重写父类方法
@Deprecated // 告诉使用者:这个方法已过时
@Test // 告诉JUnit:这是测试方法
public void foo() {}
二、注解的原理
1. 注解的三个保留层次
| 层次 | 作用 | 示例 |
|------|------|------|
| SOURCE | 仅编译期可见,编译后丢弃 | @Override |
| CLASS | 编译进字节码,但运行时JVM不加载 | 默认级别 |
| RUNTIME | 运行时可通过反射读取 | @Autowired |
通过 @Retention 指定保留级别:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnotation {
String value() default "";
}
2. 工作原理
注解本身不做任何事,它只是标记。真正干活的是读取注解的代码:
- 编译期处理:编译器或注解处理器(APT, Annotation Processing Tool)扫描注解,生成代码或报错。例如 Lombok 的
@Data在编译期生成 getter/setter。 - 运行期处理:框架通过反射读取注解,然后执行对应逻辑。例如 Spring 看到
@Autowired就去容器里找 Bean 注入。
你写的注解 → 框架/编译器扫描 → 执行对应行为
(标签) (扫描器) (实际逻辑)
三、注解的使用
1. 标准注解(JDK 自带)
public class Demo extends Parent {
@Override // 标记重写
public String toString() { return ""; }
@Deprecated(since = "1.2", forRemoval = true) // 标记废弃
public void oldMethod() {}
@SuppressWarnings({"unchecked", "rawtypes"}) // 抑制警告
public void useRaw() {
List list = new ArrayList();
}
@SafeVarargs // 标记可变参数安全
public final <T> void safe(T... args) {}
@FunctionalInterface // 标记函数式接口
interface MyFunc { void run(); }
}
2. 框架注解示例(Spring)
@RestController // 类级别
@RequestMapping("/api/users")
public class UserController {
@Autowired // 字段级别
private UserService service;
@GetMapping("/{id}") // 方法级别
@Transactional(readOnly = true)
public User get(@PathVariable Long id, // 参数级别
@RequestParam(required = false) String type) {
return service.findById(id);
}
}
3. 注解赋值的几种写法
// (1) 无参数
@Override
public void foo() {}
// (2) 只有一个 value 属性,可省略属性名
@SuppressWarnings("unchecked") // 等价于 @SuppressWarnings(value = "unchecked")
// (3) 多个属性,必须写属性名
@RequestMapping(path = "/x", method = RequestMethod.GET)
// (4) 数组属性
@SuppressWarnings({"unchecked", "rawtypes"}) // 多个值
@SuppressWarnings(value = {"unchecked"}) // 一个值也可用数组
四、自定义注解完整流程
以实现一个方法执行耗时统计为例,走完整套流程。
步骤 1:定义注解
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME) // 运行时保留(反射可读)
@Target(ElementType.METHOD) // 只能用于方法
@Documented // 生成 Javadoc
public @interface LogTime {
String desc() default ""; // 描述
boolean enabled() default true; // 是否启用
int threshold() default 0; // 超过多少毫秒才打印
}
关键点:
- 用
@interface关键字(不是interface) - 属性写法像方法:
类型 属性名() default 默认值; - 支持的属性类型:基本类型、
String、Class、枚举、注解、以及以上类型的数组
步骤 2:在代码中使用注解
public class OrderService {
@LogTime(desc = "创建订单", threshold = 100)
public void createOrder(Order order) {
// 业务逻辑
Thread.sleep(150);
}
@LogTime(desc = "查询订单")
public Order queryOrder(Long id) {
return new Order();
}
}
步骤 3:写处理器(核心!没有处理器,注解就是死的)
方式 A:反射手动读取(理解原理)
import java.lang.reflect.Method;
public class AnnotationProcessor {
public static Object invokeWithLog(Object target, String methodName, Object... args)
throws Exception {
Class<?> clazz = target.getClass();
Method method = clazz.getMethod(methodName, getParamTypes(args));
// 读取注解
LogTime anno = method.getAnnotation(LogTime.class);
if (anno == null || !anno.enabled()) {
return method.invoke(target, args);
}
long start = System.currentTimeMillis();
Object result = method.invoke(target, args);
long cost = System.currentTimeMillis() - start;
if (cost >= anno.threshold()) {
System.out.printf("[%s] 耗时: %dms%n", anno.desc(), cost);
}
return result;
}
private static Class<?>[] getParamTypes(Object[] args) {
return java.util.Arrays.stream(args).map(Object::getClass).toArray(Class<?>[]::new);
}
}
方式 B:用 Spring AOP(实际项目推荐)
@Aspect
@Component
public class LogTimeAspect {
@Around("@annotation(logTime)")
public Object around(ProceedingJoinPoint pjp, LogTime logTime) throws Throwable {
if (!logTime.enabled()) {
return pjp.proceed();
}
long start = System.currentTimeMillis();
try {
return pjp.proceed();
} finally {
long cost = System.currentTimeMillis() - start;
if (cost >= logTime.threshold()) {
System.out.printf("[%s] %s 耗时: %dms%n",
logTime.desc(),
pjp.getSignature().getName(),
cost);
}
}
}
}
五、元注解详解
1. @Target:指定可作用的位置
@Target({
ElementType.TYPE, // 类、接口、枚举
ElementType.FIELD, // 字段
ElementType.METHOD, // 方法
ElementType.PARAMETER, // 参数
ElementType.CONSTRUCTOR, // 构造器
ElementType.LOCAL_VARIABLE, // 局部变量
ElementType.ANNOTATION_TYPE, // 注解本身
ElementType.PACKAGE, // 包
ElementType.TYPE_PARAMETER, // 泛型参数 (Java 8+)
ElementType.TYPE_USE // 类型使用 (Java 8+)
})
2. @Retention:指定保留策略
@Retention(RetentionPolicy.SOURCE) // 源码级,编译后丢弃,如 @Override
@Retention(RetentionPolicy.CLASS) // 字节码中保留,运行时不加载(默认)
@Retention(RetentionPolicy.RUNTIME) // 运行时保留,反射可读 ← 框架最常用
3. @Inherited:子类是否继承
@Inherited
public @interface Role {}
@Role
class Parent {}
class Child extends Parent {} // Child 也"拥有" @Role
注意:
@Inherited只对类生效,对接口和方法无效。
4. @Documented:是否生成到 Javadoc
5. @Repeatable(Java 8+):可重复注解
@Repeatable(Schedules.class)
public @interface Schedule {
String cron();
}
@interface Schedules { // 容器注解
Schedule[] value();
}
// 使用:
@Schedule(cron = "0 0 8 * * ?")
@Schedule(cron = "0 0 20 * * ?")
public void task() {}
六、反射读取注解 API
Class<?> clazz = OrderService.class;
// 类上的注解
MyAnno a1 = clazz.getAnnotation(MyAnno.class);
boolean has = clazz.isAnnotationPresent(MyAnno.class);
Annotation[] all = clazz.getAnnotations();
// 方法上的注解
Method method = clazz.getMethod("createOrder", Order.class);
LogTime lt = method.getAnnotation(LogTime.class);
// 字段上的注解
Field field = clazz.getDeclaredField("name");
Column col = field.getAnnotation(Column.class);
// 参数上的注解
Parameter[] params = method.getParameters();
for (Parameter p : params) {
PathVariable pv = p.getAnnotation(PathVariable.class);
}
// 重复注解 (Java 8+)
Schedule[] schedules = method.getAnnotationsByType(Schedule.class);
七、实战案例:自定义参数校验注解
定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface NotBlank {
String message() default "字段不能为空";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Length {
int min() default 0;
int max() default Integer.MAX_VALUE;
String message() default "长度不合法";
}
使用
public class UserDTO {
@NotBlank(message = "用户名必填")
@Length(min = 3, max = 20, message = "用户名长度3-20")
private String username;
@NotBlank
private String password;
// getter/setter
}
校验器
public class Validator {
public static List<String> validate(Object obj) throws IllegalAccessException {
List<String> errors = new ArrayList<>();
for (Field field : obj.getClass().getDeclaredFields()) {
field.setAccessible(true);
Object value = field.get(obj);
if (field.isAnnotationPresent(NotBlank.class)) {
NotBlank ann = field.getAnnotation(NotBlank.class);
if (value == null || value.toString().trim().isEmpty()) {
errors.add(field.getName() + ": " + ann.message());
continue;
}
}
if (field.isAnnotationPresent(Length.class)) {
Length ann = field.getAnnotation(Length.class);
String s = value == null ? "" : value.toString();
if (s.length() < ann.min() || s.length() > ann.max()) {
errors.add(field.getName() + ": " + ann.message());
}
}
}
return errors;
}
}
// 使用
UserDTO dto = new UserDTO();
dto.setUsername("ab");
List<String> errors = Validator.validate(dto);
errors.forEach(System.out::println);
八、Spring AOP 通知方法的入参
1. 五种通知类型及其入参规则
| 通知类型 | 第一个参数(可选/必需) | 说明 |
|---------|-----------------------|------|
| @Before | JoinPoint(可选) | 方法执行前 |
| @After | JoinPoint(可选) | 方法执行后(无论成功失败) |
| @AfterReturning | JoinPoint(可选) | 方法正常返回后 |
| @AfterThrowing | JoinPoint(可选) | 方法抛异常后 |
| @Around | ProceedingJoinPoint(必需且必须是第一个) | 环绕通知 |
核心规则:
@Around的第一个参数必须是ProceedingJoinPoint- 其他四种通知的第一个参数可以是
JoinPoint(想用就加,不用就不加) - 后续参数通过切点表达式绑定
2. JoinPoint vs ProceedingJoinPoint
// JoinPoint:只能"看",不能控制执行
public interface JoinPoint {
Object[] getArgs(); // 目标方法参数
Signature getSignature(); // 方法签名
Object getTarget(); // 目标对象(原始)
Object getThis(); // 代理对象
String getKind(); // 连接点类型
}
// ProceedingJoinPoint:继承 JoinPoint,多了 proceed()
public interface ProceedingJoinPoint extends JoinPoint {
Object proceed() throws Throwable; // 执行目标方法
Object proceed(Object[] args) throws Throwable; // 用新参数执行
}
为什么 @Around 必须用 ProceedingJoinPoint?
因为只有环绕通知能决定是否、何时、用什么参数调用目标方法,必须有 proceed()。
3. 各通知的入参写法
@Before
@Before("execution(* com.example.service.*.*(..))")
public void before(JoinPoint jp) {
System.out.println("方法名: " + jp.getSignature().getName());
System.out.println("参数: " + Arrays.toString(jp.getArgs()));
}
@AfterReturning——拿到返回值
@AfterReturning(
pointcut = "execution(* com.example.service.*.*(..))",
returning = "result" // ← 绑定返回值到参数 result
)
public void afterReturning(JoinPoint jp, Object result) {
System.out.println("返回值: " + result);
}
returning属性的值必须与方法参数名一致。参数类型可以是Object或具体类型(类型不匹配时通知不触发)。
@AfterThrowing——拿到异常
@AfterThrowing(
pointcut = "execution(* com.example.service.*.*(..))",
throwing = "ex" // ← 绑定异常到参数 ex
)
public void afterThrowing(JoinPoint jp, Throwable ex) {
System.err.println("异常: " + ex.getMessage());
}
@Around——最强大
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try {
Object[] args = pjp.getArgs();
Object result = pjp.proceed(args); // 或直接 pjp.proceed()
return result;
} catch (Throwable e) {
throw e;
} finally {
System.out.println("耗时: " + (System.currentTimeMillis() - start) + "ms");
}
}
@Around 必须:
- 返回值类型一般是
Object(或目标方法的返回类型) - 声明
throws Throwable - 显式调用
proceed(),否则目标方法不会执行!
4. 通过切点表达式绑定参数
args() 绑定方法参数
@Before("execution(* com.example.service.*.*(..)) && args(id,..)")
public void before(Long id) {
System.out.println("传入的 id: " + id);
}
@annotation() 绑定方法上的注解
@Around("@annotation(logTime)") // 参数名 logTime 与下方参数名一致
public Object around(ProceedingJoinPoint pjp, LogTime logTime) throws Throwable {
System.out.println("注解描述: " + logTime.desc());
return pjp.proceed();
}
@within() / @target() 绑定类上的注解
@Before("@within(service)") // 类被 @Service 标注
public void before(JoinPoint jp, Service service) {
System.out.println("Service value: " + service.value());
}
target() / this() 绑定目标对象/代理对象
@Before("execution(* *.*(..)) && target(svc)")
public void before(UserService svc) { // 只对 UserService 类型生效
System.out.println(svc);
}
5. 综合实战:自定义注解 + 多参数绑定
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimit {
int qps() default 10;
String key() default "";
}
@Service
public class OrderService {
@RateLimit(qps = 5, key = "createOrder")
public Order create(Long userId, Order order) {
return order;
}
}
@Aspect
@Component
public class RateLimitAspect {
@Around("@annotation(rateLimit) && args(userId, ..)")
public Object around(ProceedingJoinPoint pjp,
RateLimit rateLimit, // 来自 @annotation
Long userId) // 来自 args
throws Throwable {
String key = rateLimit.key() + ":" + userId;
int qps = rateLimit.qps();
if (!tryAcquire(key, qps)) {
throw new RuntimeException("限流: " + key);
}
return pjp.proceed();
}
private boolean tryAcquire(String key, int qps) { return true; }
}
6. 入参规则总结
| 规则 | 说明 |
|------|------|
| JoinPoint / ProceedingJoinPoint 必须是第一个参数 | 否则报错 |
| 切点绑定参数(args、@annotation 等)名称必须一致 | 表达式中的名字 = 方法参数名 |
| returning / throwing 的值 = 方法参数名 | 类型缩窄会过滤通知触发 |
| @Around 必须调 proceed() | 否则目标方法不执行 |
| @Around 必须 throws Throwable | 因为 proceed() 声明了 |
7. 执行顺序(多通知同时存在时)
正常返回流程:
@Around 前半段 → @Before → 目标方法 → @AfterReturning → @After → @Around 后半段
异常流程:
@Around 前半段 → @Before → 目标方法(抛异常) → @AfterThrowing → @After → @Around catch
九、@Pointcut 注解详解
1. @Pointcut 是什么
@Pointcut 用来给切点表达式起个名字,把切点定义和通知解耦。它本身不是通知,只是一个"命名的切点表达式"。
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {} // 方法名就是切点的"名字",方法体永远为空
之后可以用 serviceLayer() 这个名字代替那一长串表达式:
@Before("serviceLayer()")
public void before() {}
@Around("serviceLayer()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
return pjp.proceed();
}
2. 为什么可以直接 @Around("execution(...)") 不用 @Pointcut?
因为 @Before、@Around 等通知注解的 value 直接接受切点表达式——表达式可以是:
1. 直接写的表达式字符串:@Around("execution( .*(..))")
2. @Pointcut 定义的命名切点:@Around("serviceLayer()")
3. 两者组合:@Around("serviceLayer() && @annotation(logTime)")
所以 @Pointcut 不是必需的,它只是一种复用和组织的手段。
3. 对比:用 vs 不用 @Pointcut
不用(表达式重复)
@Aspect
@Component
public class LogAspect {
@Before("execution(* com.example.service.*.*(..)) && @annotation(logTime)")
public void before(LogTime logTime) {}
@AfterReturning("execution(* com.example.service.*.*(..)) && @annotation(logTime)")
public void afterReturning(LogTime logTime) {}
@Around("execution(* com.example.service.*.*(..)) && @annotation(logTime)")
public Object around(ProceedingJoinPoint pjp, LogTime logTime) throws Throwable {
return pjp.proceed();
}
}
问题:表达式重复多次,一处要改,处处要改。
用 @Pointcut(推荐)
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}
@Pointcut("@annotation(com.example.LogTime)")
public void logAnnotated() {}
@Pointcut("serviceLayer() && logAnnotated()")
public void logTarget() {} // 组合切点
@Before("logTarget() && @annotation(logTime)")
public void before(LogTime logTime) {}
@Around("logTarget() && @annotation(logTime)")
public Object around(ProceedingJoinPoint pjp, LogTime logTime) throws Throwable {
return pjp.proceed();
}
}
4. @Pointcut 的优势
| 优势 | 说明 |
|------|------|
| 复用 | 一处定义,多处引用 |
| 组合 | 用 &&、\|\|、! 组合多个切点 |
| 可读性 | serviceLayer() && logAnnotated() 比一长串表达式直观 |
| 跨类共享 | 可以放到公共类,其他切面引用 |
5. 跨切面共享切点
// 公共切点定义
public class CommonPointcuts {
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}
@Pointcut("execution(* com.example.controller.*.*(..))")
public void controllerLayer() {}
}
// 其他切面使用:用全限定方法名引用
@Aspect
@Component
public class LogAspect {
@Before("com.example.aop.CommonPointcuts.serviceLayer()")
public void log() {}
}
6. 组合运算
@Pointcut("execution(* com.example..service..*.*(..))")
public void anyService() {}
@Pointcut("execution(* com.example..*.read*(..))")
public void readMethod() {}
@Pointcut("anyService() && readMethod()") // 与
public void readService() {}
@Pointcut("anyService() && !readMethod()") // 非
public void writeService() {}
@Pointcut("readService() || writeService()") // 或
public void anyServiceMethod() {}
7. @Pointcut 方法的写法规则
@Pointcut("execution(* *.*(..))")
public void myPointcut() {} // 方法体必须为空
规则:
1. 返回类型必须是 void
2. 方法体必须为空(写了也不会执行)
3. 方法名就是切点的引用名
4. 访问修饰符决定可见性:
private:只能在本切面类内引用public:可被其他切面类引用(用全限定名)
8. 带参数绑定的 @Pointcut
@Pointcut("@annotation(logTime)")
public void withLogTime(LogTime logTime) {} // 参数声明在切点上
@Around("withLogTime(logTime)") // 引用时也带参数
public Object around(ProceedingJoinPoint pjp, LogTime logTime) throws Throwable {
System.out.println(logTime.desc());
return pjp.proceed();
}
9. 什么时候用 / 不用 @Pointcut
直接写表达式即可:
// 只用一次的简单场景
@Around("execution(* com.example.HelloService.sayHi(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
return pjp.proceed();
}
用 @Pointcut:
- 同一表达式被多个通知使用
- 表达式复杂,命名后语义更清晰
- 切点需要跨类共享
- 需要组合多个切点
10. 一句话总结
@Pointcut是切点表达式的"别名",让你把"在哪里织入"和"做什么"分开。通知注解(@Before、@Around等)的 value 接受任意切点表达式——直接写或引用命名切点都行,所以@Pointcut是可选的优化手段,不是必需的语法。
@Around("execution(...)") ← 直接写表达式:能用,简单
@Around("myPointcut()") ← 引用命名切点:复用、清晰、可组合
十、常见坑与学习路径
常见坑
1. RetentionPolicy.SOURCE/CLASS 用反射读不到——框架场景必须用 RUNTIME。
2. 注解属性不能为 null——只能用 ""、{}、特定哨兵值作为"无值"。
3. @Inherited 只对类生效,对接口和方法无效。
4. Spring AOP 的注解切面要求方法被代理对象调用——同类内方法直接调用 this.method() 不会触发 AOP。
5. 注解属性顺序无关,但只有一个 value 时才能省略属性名。
典型应用场景
1. 配置替代 XML:Spring 的 @Component、@Configuration
2. AOP 切点标记:@Transactional、@Cacheable
3. ORM 映射:JPA 的 @Entity、@Column
4. 参数校验:@NotNull、@Min
5. 测试框架:@Test、@Before
6. 编译期代码生成:Lombok @Data、MapStruct
学习路径建议
1. 先用 JDK 自带的 @Override、@Deprecated 熟悉语法
2. 自己写一个简单注解 + 反射处理器(耗时统计是经典例子)
3. 学 Spring 的 @Component/@Autowired,理解扫描+反射注入机制
4. 学 AOP 切面 + 自定义注解(如 @LogTime、@RateLimit)
5. 进阶:注解处理器 APT(编译期生成代码,如 Lombok、MapStruct 的原理)
核心理解
注解 = 标签;标签本身没用,读标签的代码才赋予它意义。学注解的关键是搞清楚"谁在读这个注解,读到后做什么"。