在Java企业级开发中,自定义注解常被用来标记需要特殊处理的方法,例如接口鉴权、操作日志、防重复提交等。但注解仅仅是元数据,本身不具备任何执行能力。要让注解发挥作用,必须借助AOP将拦截逻辑织入到标记了注解的方法调用前后。实现方式主要分为两类:一是利用AspectJ完整的切面框架,通过切点表达式匹配注解;二是基于JDK动态代理或CGLIB的原生代理机制,在代理对象中反射读取注解并执行增强。理解这两种路径的底层差异,是写出稳定拦截器的前提。

自定义注解的定义与元注解配置
实现注解拦截的第一步是定义自己的注解。Java中用@interface声明注解类型,并通过@Target和@Retention等元注解控制它的作用范围和生命周期。如果希望注解在运行期被AOP读取,@Retention必须设置为RetentionPolicy.RUNTIME;若使用编译期AspectJ织入,也可设为CLASS。作用目标通常选ElementType.METHOD,表示贴在方法上。注解内部可以定义属性,例如超时时间、日志描述,这些属性会在切面中被读取并影响拦截行为。
下面给出一个典型的自定义注解示例,用于标记需要记录耗时的方法。注解只有一个value属性,用来说明业务名称。注意,如果属性名为value且只设置一个值,使用注解时可省略属性名。这种轻量设计能让业务代码保持整洁,同时给切面提供足够上下文。
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogTime {
String value() default "";
}
定义完注解后,业务方法只需添加@LogTime("下单接口")即可完成标记。此时注解如同贴在方法上的标签,但还没有任何代码去处理它。很多初学者误以为框架会自动识别并输出日志,其实从JVM角度看,注解在运行期只是可通过反射获取的Annotation对象,真正触发动作的是后续编写的切面或代理逻辑。
基于AspectJ注解切面完成拦截
Spring AOP整合了AspectJ的注解风格,开发者可以用@Aspect和@Around等注解快速编写拦截器。切点的核心是表达式,例如@annotation(com.demo.LogTime)表示匹配所有带LogTime注解的方法。在@Around通知中,通过ProceedingJoinPoint获取方法签名,再读取注解实例,就能在方法执行前后插入逻辑。这种方式的优势是解耦彻底,业务方法零侵入,且切点表达式支持精细过滤。
下面的代码展示了一个完整的AspectJ切面。它在目标方法执行前打印开始时间,执行后计算耗时并输出。通过method.getAnnotation(LogTime.class)拿到注解后,使用其value属性作为日志标识。若方法抛异常,切面也会记录失败信息。这种结构可直接复用于监控、限流等场景。
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import java.lang.reflect.Method;
@Aspect
@Component
public class LogTimeAspect {
@Around("@annotation(com.demo.LogTime)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
LogTime logTime = method.getAnnotation(LogTime.class);
String name = logTime != null ? logTime.value() : "未知";
long start = System.currentTimeMillis();
try {
return pjp.proceed();
} finally {
long cost = System.currentTimeMillis() - start;
System.out.println(name + " 耗时: " + cost + "ms");
}
}
}
使用AspectJ风格时,需注意Spring容器必须开启@EnableAspectJAutoProxy,且切面类要被扫描为Bean。如果项目使用编译期织入,还需配置AspectJ编译器插件,此时切点不仅限于Spring管理的Bean,连普通POJO也能拦截。但这种重织入会增加构建复杂度,一般业务系统用运行期代理已足够。对比来看,AspectJ表达式灵活、功能全,适合统一治理;原生代理则更轻,下面具体说明。
原生JDK与CGLIB代理读取注解拦截
若不引入AspectJ,也可利用Spring的原生代理机制。当目标类实现接口时用JDK动态代理,否则用CGLIB子类代理。代理对象在调用真实方法前,会经过MethodInterceptor或InvocationHandler,这里同样可以反射读取方法上的注解。与原生代理配合自定义注解的思路是:在拦截器中判断method.isAnnotationPresent(LogTime.class),若存在则执行增强。这种方式不依赖切点表达式,逻辑直观,但需自己管理代理创建。
以下示例用CGLIB的MethodInterceptor实现同样耗时统计。它手动创建代理对象,在intercept方法中拿到目标方法和注解,控制执行与统计。可以看出,注解在这里只是判断条件,真正的拦截完全由代理链完成。对于不想引入AOP框架的老项目,这种写法侵入小、依赖少。
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class LogTimeProxy implements MethodInterceptor {
public Object getInstance(Class<?> targetClass) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(targetClass);
enhancer.setCallback(this);
return enhancer.create();
}
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
if (method.isAnnotationPresent(LogTime.class)) {
LogTime lt = method.getAnnotation(LogTime.class);
long start = System.currentTimeMillis();
try {
return proxy.invokeSuper(obj, args);
} finally {
System.out.println(lt.value() + " 耗时: "
+ (System.currentTimeMillis() - start) + "ms");
}
}
return proxy.invokeSuper(obj, args);
}
}
原生代理的局限在于只能拦截通过代理对象发起的调用,自调用(同类方法内部调用)会绕过代理,导致注解失效。AspectJ编译期织入则无此问题。此外,JDK动态代理要求接口存在,CGLIB不能代理final类。因此在选型时,若系统已用Spring且需全局规范,优先AspectJ;若仅需局部增强且追求简单,原生代理配合注解也完全可行。掌握这两条路线,自定义注解就能真正驱动拦截逻辑落地。