APE
文章标签归档关于

© 2026 APE.PUB

2026-07-06· 39 分钟

Annotation

javaannotation

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 的原理)

核心理解

注解 = 标签;标签本身没用,读标签的代码才赋予它意义。学注解的关键是搞清楚"谁在读这个注解,读到后做什么"。

← 返回首页