Java案例怎么实现前置通知?

wen java案例 24

Java案例怎么实现前置通知?从零掌握AOP核心编程

目录导读

  1. 前置通知是什么?为什么需要它?
  2. JDK动态代理实现前置通知(传统方式)
  3. CGLIB代理实现前置通知(无接口场景)
  4. Spring AOP注解实现前置通知(企业级实战)
  5. 前置通知的最佳实践与陷阱
  6. 常见问题答疑

前置通知是什么?为什么需要它?

前置通知(Before Advice) 是AOP(面向切面编程)中最基础的通知类型:它允许你在目标方法执行之前,插入一段自定义逻辑,想象一个场景:你在调用每个业务方法前,需要记录日志、检查权限、或者开启事务——如果每个方法都手动写一遍,代码将变得臃肿且难以维护,前置通知正是通过“切面”的方式,将这些横切关注点(Cross-cutting Concerns)与核心业务解耦。

Java案例怎么实现前置通知?

一个形象的比喻:前置通知就像你进入会议室前的门禁系统——它在你真正进入(执行方法)之前,先验证身份、记录来访时间,而会议室里的人(核心业务代码)并不需要关心门禁的存在。

为什么要用前置通知?

  • 代码复用:日志、权限检查等逻辑只需写一次
  • 关注点分离:业务代码只关心业务,非业务逻辑由切面管理
  • 可维护性高:修改日志格式,只需改动切面,不用改所有业务类

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 最佳实践

  1. 切面方法参数:使用JoinPoint可以获取方法名、参数、目标对象等信息,但注意JoinPoint必须在通知方法的第一个参数(无其他参数),或有ProceedingJoinPoint(仅环绕通知可用)。

  2. 异常处理:前置通知中抛出异常会阻止目标方法执行,适合权限验证失败等场景。

  3. 性能考虑:前置通知中的代码应尽量轻量,避免在切面中进行数据库查询或远程调用(建议放在后置或环绕通知中并做好缓存)。

  4. 切点复用:将切点表达式抽取为私有方法,使用@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环境验证,示例代码均已测试通过。

抱歉,评论功能暂时关闭!