Java案例怎么实现前置通知?从零掌握AOP核心编程
目录导读
- 前置通知是什么?为什么需要它?
- JDK动态代理实现前置通知(传统方式)
- CGLIB代理实现前置通知(无接口场景)
- Spring AOP注解实现前置通知(企业级实战)
- 前置通知的最佳实践与陷阱
- 常见问题答疑
前置通知是什么?为什么需要它?
前置通知(Before Advice) 是AOP(面向切面编程)中最基础的通知类型:它允许你在目标方法执行之前,插入一段自定义逻辑,想象一个场景:你在调用每个业务方法前,需要记录日志、检查权限、或者开启事务——如果每个方法都手动写一遍,代码将变得臃肿且难以维护,前置通知正是通过“切面”的方式,将这些横切关注点(Cross-cutting Concerns)与核心业务解耦。

一个形象的比喻:前置通知就像你进入会议室前的门禁系统——它在你真正进入(执行方法)之前,先验证身份、记录来访时间,而会议室里的人(核心业务代码)并不需要关心门禁的存在。
为什么要用前置通知?
- 代码复用:日志、权限检查等逻辑只需写一次
- 关注点分离:业务代码只关心业务,非业务逻辑由切面管理
- 可维护性高:修改日志格式,只需改动切面,不用改所有业务类
JDK动态代理实现前置通知(传统方式)
1 原理与适用场景
JDK动态代理基于接口实现,当你有一个接口和实现类时,可以用java.lang.reflect.Proxy生成代理对象,在调用目标方法前插入前置逻辑,适用于目标对象必须实现接口的情况。
2 案例:给用户服务添加日志前置通知
步骤1:定义接口与实现类
public interface UserService {
void addUser(String name);
}
public class UserServiceImpl implements UserService {
@Override
public void addUser(String name) {
System.out.println("添加用户:" + name);
}
}
步骤2:实现InvocationHandler
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class BeforeAdviceHandler implements InvocationHandler {
private Object target;
public BeforeAdviceHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 前置通知:在真实方法执行前打印日志
System.out.println("[前置通知] 准备调用方法:" + method.getName());
// 调用真实目标方法
Object result = method.invoke(target, args);
return result;
}
}
步骤3:生成代理并测试
import java.lang.reflect.Proxy;
public class BeforeDemo {
public static void main(String[] args) {
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new BeforeAdviceHandler(target)
);
proxy.addUser("张三");
}
}
输出结果:
[前置通知] 准备调用方法:addUser
添加用户:张三
问答:JDK动态代理的局限是什么? 答:它只能代理接口,如果目标类没有实现任何接口,JDK动态代理会抛出异常,此时需要CGLIB代理(见下一节)。
CGLIB代理实现前置通知(无接口场景)
1 原理与区别
CGLIB(Code Generation Library)通过继承目标类生成子类作为代理,无需接口,它基于字节码生成技术,性能较高,Spring AOP默认会优先使用JDK动态代理,若目标类没有接口则自动切换到CGLIB。
2 案例:没有接口的类加前置通知
步骤1:引入依赖(Maven)
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
步骤2:实现MethodInterceptor
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class BeforeInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
// 前置通知1:方法调用前记录
System.out.println("[CGLIB前置通知] 即将执行:" + method.getName());
// 调用目标方法(通过super调用,避免无限循环)
Object result = proxy.invokeSuper(obj, args);
return result;
}
}
步骤3:生成代理并测试
import net.sf.cglib.proxy.Enhancer;
public class CglibBeforeDemo {
public static void main(String[] args) {
// 创建Enhancer对象
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class); // 直接设置类,无需接口
enhancer.setCallback(new BeforeInterceptor());
UserServiceImpl proxy = (UserServiceImpl) enhancer.create();
proxy.addUser("李四");
}
}
输出结果:
[CGLIB前置通知] 即将执行:addUser
添加用户:李四
问答:CGLIB代理的注意事项? 答:
- 目标类不能是final类(因为继承需要)
- 目标方法不能是final或static(CGLIB不能重写final方法,static方法属于类)
- 性能略高于JDK代理(但现代JDK代理优化后差距很小)
Spring AOP注解实现前置通知(企业级实战)
在实际开发中,99%的场景不需要手写动态代理,而是使用Spring AOP配合@Before注解,Spring内部封装了JDK/CGLIB代理的细节,开发者只需定义切面即可。
1 配置与依赖
Spring Boot项目(推荐):
<!-- pom.xml引入AOP依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2 定义切面与前置通知
步骤1:创建切面类
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LogBeforeAspect {
@Before("execution(* com.example.service..*(..))")
public void beforeLog(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("[Spring AOP前置通知] 方法:" + methodName + ",参数:" + Arrays.toString(args));
}
}
步骤2:创建业务Service
@Service
public class OrderService {
public void createOrder(String orderId) {
System.out.println("创建订单:" + orderId);
}
}
步骤3:测试
@SpringBootTest
public class BeforeAopTest {
@Autowired
private OrderService orderService;
@Test
public void test() {
orderService.createOrder("ORD-001");
}
}
输出结果:
[Spring AOP前置通知] 方法:createOrder,参数:[ORD-001]
创建订单:ORD-001
3 切点表达式详解
execution(* com.example.service..*(..))意思:
- 任意返回类型
com.example.service..:包及其子包- 任意方法名
- 任意参数
你可以更精准地定位方法,
@Before("execution(public * com.example.service.*.add*(String))"):只匹配add开头、参数为String的公有方法
问答:Spring AOP和AspectJ的关系? 答:Spring AOP集成了AspectJ的注解风格,但实现机制是自己基于代理模式,AspectJ是完整的AOP框架,通过编译时织入(不需要代理),性能更高,一般业务场景使用Spring AOP足够,性能敏感场景才考虑AspectJ。
前置通知的最佳实践与陷阱
1 最佳实践
-
切面方法参数:使用
JoinPoint可以获取方法名、参数、目标对象等信息,但注意JoinPoint必须在通知方法的第一个参数(无其他参数),或有ProceedingJoinPoint(仅环绕通知可用)。 -
异常处理:前置通知中抛出异常会阻止目标方法执行,适合权限验证失败等场景。
-
性能考虑:前置通知中的代码应尽量轻量,避免在切面中进行数据库查询或远程调用(建议放在后置或环绕通知中并做好缓存)。
-
切点复用:将切点表达式抽取为私有方法,使用
@Pointcut注解,如:@Pointcut("execution(* com.example.service..*(..))") public void servicePointcut() {} @Before("servicePointcut()") public void beforeLog() { ... }
2 常见陷阱
- 循环依赖:如果切面自身调用了其他被代理的方法,可能导致递归,例如切面中调用了当前类的方法(特别是用
this调用),此时this不是代理对象。 - 内部调用失效:同一个类中的方法A调用方法B,若B上有切面注解,这个切面不会生效(因为调用是内部
this,不是代理对象),解决方案:注入代理对象(SelfClass)AopContext.currentProxy()或使用@Autowired注入自身(注意循环依赖)。 - final方法/静态方法:Spring AOP无法对其织入前置通知,因为代理无法重写final方法,静态方法不属于实例。
常见问题答疑
Q1:前置通知的执行顺序是怎样的?
答:如果有多个前置通知作用于同一个连接点,可以通过@Order注解或实现Ordered接口指定顺序,数值越小越先执行,默认按自然顺序(可按切面类名排序)。
Q2:前置通知和环绕通知有什么区别? 答:
- 前置通知:在方法执行前固定执行,无法控制目标方法的执行(除非抛出异常)。
- 环绕通知:可以在方法前后都执行自定义逻辑,并且可以选择是否调用目标方法(通过
proceed()),甚至修改返回值。 - 如果只做简单的日志/权限检查,前置通知更简洁。
Q3:前置通知能获取目标方法的返回值吗?
答:不能,因为此时目标方法还未执行,需要获取返回值应使用后置通知(@AfterReturning)或环绕通知。
Q4:是否可以在Spring Boot中不使用任何配置就启用AOP?
答:需要添加@EnableAspectJAutoProxy(Spring Boot自动配置了,但必须确保spring-boot-starter-aop依赖存在),默认会扫描@Aspect注解的Bean。
从JDK动态代理到CGLIB,再到Spring AOP注解,前置通知的实现方式不断简化,在实际开发中,优先选择Spring AOP注解方式,它既保留了代理的灵活性,又解放了开发者对底层细节的关注。
掌握前置通知,相当于掌握了AOP的“门禁钥匙”,当你需要为系统添加统一的日志、权限、事务控制时,前置通知是最直接、最优雅的解决方案,如果这篇文章能帮你理清前置通知的实现脉络,欢迎收藏或分享给需要的小伙伴。
本文基于JDK 11、Spring Boot 2.7、CGLIB 3.3环境验证,示例代码均已测试通过。