自定义注解的核心分两步:定义注解(用 @interface 声明)和使用注解(通过反射或 AOP 读取注解信息并执行逻辑)。注解本身只是“标记”,不包含业务逻辑,真正干活的是读取它的代码。
第一步:定义注解
用 @interface 关键字声明,配合元注解(@Target、@Retention 等)限定它的用法和生命周期。
import java.lang.annotation.*;
@Target(ElementType.METHOD) // 只能标在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时保留,反射才能读到
@Documented // 生成 Javadoc 时包含该注解
public @interface AdminOnly {
String value() default ""; // 注解属性,带默认值
int level() default 1; // 可以有多个属性
}
| 元注解 | 作用 |
| @Target | 限定注解能标在哪里:METHOD、FIELD、TYPE、PARAMETER、CONSTRUCTOR等 |
| @Retention | 注解保留到什么时候:SOURCE(编译丢弃)、CLASS(默认,类文件保留但运行时不可见)、RUNTIME(运行时可通过反射读取) |
| @Documented | 生成 Javadoc 时包含该注解 |
| @Inherited | 子类是否继承父类的该注解(只对类注解有效) |
要能被反射或 AOP 读取,@Retention 必须是 RUNTIME,这是最常见的坑。
第二步:使用注解
public class UserService {
@AdminOnly(value = “删除用户”, level = 2)
public void deleteUser(Long userId) {
// …
}
}
第三步:读取注解并执行逻辑
注解标记了,但不会自动生效,需要有代码去“读”它。两种常见方式:
方式一:反射读取(适合普通方法、字段、类)
Method method = UserService.class.getMethod("deleteUser", Long.class);
if (method.isAnnotationPresent(AdminOnly.class)) {
AdminOnly anno = method.getAnnotation(AdminOnly.class);
System.out.println("操作:" + anno.value() + ",级别:" + anno.level());
// 在这里执行权限校验等逻辑
}
反射读取适合在框架初始化、工具类、测试等场景手动调用。
方式二:AOP 切面读取(适合 Spring 项目,自动拦截)
这是实际项目中最常用的方式——定义注解 + 写切面,方法被调用时自动触发逻辑。
@Aspect
@Component
public class AdminOnlyAspect {
@Before("@annotation(adminOnly)")
public void check(JoinPoint jp, AdminOnly adminOnly) {
// adminOnly 就是目标方法上的注解实例,可以读属性
Long userId = (Long) jp.getArgs()[0];
if (!PermissionService.isAdmin(userId)) {
throw new PermissionDeniedException("需要管理员权限:" + adminOnly.value());
}
}
}
@annotation(adminOnly) 切点会匹配所有标注了 @AdminOnly 的方法,并把注解实例绑定到切面方法参数 adminOnly 上。这样只要方法加了注解,切面就自动生效,不需要在每个方法里手写校验。
几个关键注意点
@Retention(RUNTIME)必须加:否则反射和 AOP 都读不到注解,注解等于白写。- 注解属性只能是基本类型、String、Class、枚举、注解或它们的数组,不能是普通对象。
- 注解本身不执行逻辑:它只是元数据,必须有反射或 AOP 代码去读取并执行。很多人以为加了注解就自动生效,其实缺了“读取”这一步。
- AOP 只拦截 Spring Bean 的外部调用:同类内部
this.调用不经过代理,切面不生效。 - 注解属性用
default给默认值,使用时可以省略,代码更简洁。 - 组合注解:可以把多个注解组合成一个自定义注解(如 Spring 的
@SpringBootApplication就是组合注解),用@AliasFor处理属性别名。



