Java注解从入门到实战:5个案例教你玩转自定义注解与反射
目录导读
- 注解的本质:不仅仅是
@Override - 自定义
@Log注解实现方法日志埋点 @NotNull参数校验——告别冗长的if判断- 基于注解的依赖注入(简易版Spring IOC)
@Scheduled定时任务注解的底层实现原理- 注解+动态代理实现权限控制
- 常见问题问答(FAQ)
- 注解的适用场景与性能陷阱
注解的本质:不仅仅是@Override
很多开发者使用@Override、@Deprecated多年,却从未尝试自己定义一个注解,注解在Java中是一种“元数据”形式,它本身不包含业务逻辑,而是通过反射或编译期处理(如APT)来驱动逻辑,举个例子:@Override只是告诉编译器“请帮我检查方法签名”,而@Transactional则通过Spring的AOP在运行时解析。

核心认知:注解 = 标记 + 解释器,没有解释器的注解,如同贴了一张没有翻译的外文标签。
案例一:自定义@Log注解实现方法日志埋点
场景:假设你想在业务层的每个方法执行前打印“调用开始”,执行后打印“调用结束”。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String value() default "";
}
配套拦截器(基于AOP):
@Aspect
@Component
public class LogAspect {
@Around("@annotation(log)")
public Object around(ProceedingJoinPoint pjp, Log log) throws Throwable {
System.out.println("开始执行: " + log.value());
Object result = pjp.proceed();
System.out.println("结束执行: " + log.value());
return result;
}
}
使用:
@Log("用户服务-查询")
public User getUserById(Long id) { ... }
解释:这里利用Spring AOP的@annotation切点表达式,自动捕捉被@Log标记的方法。注意:@Retention(RUNTIME)是必须的,否则运行时反射拿不到注解信息。
案例二:@NotNull参数校验——告别冗长的if判断
定义注解:
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotNull {
String message() default "参数不能为空";
}
核心校验器(利用反射+代理):
public class ParamValidator {
public static void validate(Object obj, Method method, Object[] args) throws Exception {
Annotation[][] annotations = method.getParameterAnnotations();
for (int i = 0; i < annotations.length; i++) {
for (Annotation annotation : annotations[i]) {
if (annotation instanceof NotNull && args[i] == null) {
throw new IllegalArgumentException(((NotNull) annotation).message());
}
}
}
}
}
实战:当你的Controller接收请求时,在调用Service前先执行ParamValidator.validate(),避免了每个方法里写if (param == null)的重复劳动。
案例三:基于注解的依赖注入(简易版Spring IOC)
定义@Inject注解:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject { }
简易容器实现:
public class SimpleIOC {
public static <T> T getBean(Class<T> clazz) throws Exception {
T obj = clazz.newInstance();
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(Inject.class)) {
field.setAccessible(true);
field.set(obj, getBean(field.getType())); // 递归注入
}
}
return obj;
}
}
优势:通过递归构造依赖树,实现了最小的IOC容器,理解了这个原理,再看Spring的@Autowired就豁然开朗了。
案例四:@Scheduled定时任务注解的底层实现原理
先看常见的Spring用法:
@Scheduled(cron = "0 0 12 * * ?")
public void report() { ... }
底层链路:
- 第一步:Spring容器启动时,扫描所有Bean方法,解析
@Scheduled注解,存入任务注册表。 - 第二步:核心是
ScheduledAnnotationBeanPostProcessor(继承BeanPostProcessor),它在postProcessAfterInitialization阶段拦截。 - 第三步:将任务包装成Runnable,交给
ThreadPoolTaskScheduler,内部依赖ScheduledExecutorService。
启示:注解本身无魔法,靠的是后置处理器拦截和线程池调度。
案例五:注解+动态代理实现权限控制
定义@RequiresRole注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresRole {
String value();
}
动态代理实现:
public class SecurityProxy implements InvocationHandler {
private Object target;
private String userRole;
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
RequiresRole roleAnnotation = method.getAnnotation(RequiresRole.class);
if (roleAnnotation != null && !userRole.equals(roleAnnotation.value())) {
throw new AccessDeniedException("权限不足");
}
return method.invoke(target, args);
}
}
核心优势:权限逻辑与业务代码完全解耦,你会看到,Java的动态代理+注解是AOP的“祖师爷”。
常见问题问答(FAQ)
Q1:@Retention的三种值有什么区别?
SOURCE:编译期丢弃(如@Override)。CLASS:保留到class文件,但运行时JVM不读取(默认)。RUNTIME:运行时可通过反射读取(自定义注解必须用它)。
Q2:为什么注解无法继承?
注解本身不支持extends,但@Inherited元注解可以让子类继承父类上的注解(只对类有效,对方法无效)。
Q3:注解会影响性能吗?
反射解析注解确实有轻微开销,对于运行频繁的接口,建议缓存解析结果(如Spring的AnnotationUtils)。
注解的适用场景与性能陷阱
适用场景:框架开发(Spring、MyBatis)、单元测试(@Test)、代码生成(Lombok)、声明式事务。
陷阱提示:
- 切勿在
for循环内反复调用getAnnotation(),应将注解信息缓存在Map中。 - 注意
@Target的作用域,避免误用在类上导致逻辑混乱。 - 自定义注解务必设置
@Retention(RUNTIME),否则反射获取为null。
最后建议:去读一读java.lang.annotation包源码和javax.annotation下的@Resource、@PostConstruct,你会发现注解只是接口,真正强大的是那些“注解处理器”,从今天起,尝试在项目里写一个自己的@CheckPassword或@CacheResult,你会理解“约定优于配置”的力量。