Spring AOP

Spring AOP 入门到不懵:从原理到实战示例


一、什么是 AOP?

AOP(Aspect Oriented Programming,面向切面编程)是一种编程思想 + 运行期增强机制
它并不是一种语法,而是通过代理对象在运行期对方法进行增强

在真实业务中,我们经常会写出类似下面的代码:

public String echo() {
    log.info("进入方法");
    // 权限校验
    // 参数校验
    // 性能统计
    return "ECHO";
}

问题在于:

  • 日志、权限、事务、性能统计
  • 并不是业务本身
  • 却散落在大量业务方法中,难以维护

AOP 的目标只有一个:

把这些“横切关注点”(日志、事务、鉴权等)
从业务代码中剥离出来,
做到 业务只关心业务

二、AOP的核心思想

不改业务代码,在方法执行的“前/后/异常/环绕”插入统一逻辑

Spring AOP的实现方式只有一个核心点

通过代理对象,拦截方法调用

⚠️ 注意关键词:代理对象、拦截调用

2.1 理解思路和代码实现

在真正引入Spring AOP之前,
如果不理解【代理对象是怎么工作的】,AOP一定会懵。
所以在之前,我们先完全手写一套“代理+横切逻辑”。

2.2 整体设计思路

StartService(客户端)
        ↓
ServiceFactory(工厂)
        ↓
JDK 动态代理对象(Proxy)
        ↓
ServiceProxy.invoke()(统一拦截)
        ↓
DeptServiceImpl(真实业务对象)

核心思想

所有业务方法的调用,先经过代理对象,再由代理转发给真实对象

2.3 业务接口(被代理的目标)

package com.yootk.service;

import com.yootk.vo.Dept;

public interface IDeptService {
    // 数据增加
    boolean add(Dept dept);
}

说明:

JDK动态代理只能代理接口
这一步是AOP能成立的前提之一

2.4 真实的业务实现类(Target)

package com.yootk.service.impl;

import com.yootk.service.IDeptService;
import com.yootk.vo.Dept;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DeptServiceImpl implements IDeptService {

    private static final Logger LOGGER =
            LoggerFactory.getLogger(DeptServiceImpl.class);

    @Override
    public boolean add(Dept dept) {
        LOGGER.info("【部门增加】编号:{},名称:{},位置:{}",
                dept.getDeptno(), dept.getDname(), dept.getLoc());
        return true;
    }
}

⚠️ 注意

  • 这里的业务类完全不知道代理的存在
  • 也没有任何事务、日志控制代码
  • 只关心业务本身

2.5 InvocationHandler:代理的核心(重点)

package com.yootk.service.proxy;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ServiceProxy implements InvocationHandler {

    private static final Logger LOGGER =
            LoggerFactory.getLogger(ServiceProxy.class);

    // 保存真实业务对象(Target)
    private Object target;

    /**
     * 绑定真实对象,并创建代理对象
     */
    public Object bind(Object target) {
        this.target = target;

        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(),   // 类加载器
                target.getClass().getInterfaces(),    // 代理接口
                this                                 // 方法拦截处理器
        );
    }

    /**
     * 判断当前方法是否需要开启事务
     */
    private boolean needTransaction(Method method) {
        return method.getName().startsWith("add");
    }

    /**
     * 所有代理对象的方法调用,都会进入这里
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        boolean openTx = needTransaction(method);

        try {
            // 前置通知(@Before)
            if (openTx) {
                LOGGER.info("【JDBC事务】开启事务");
            }

            // 执行目标方法
            Object result = method.invoke(this.target, args);

            // 返回通知(@AfterReturning)
            if (openTx) {
                LOGGER.info("【JDBC事务】提交事务");
            }

            return result;

        } catch (Throwable ex) {
            // 异常通知(@AfterThrowing)
            if (openTx) {
                LOGGER.info("【JDBC事务】事务回滚");
            }

            // 反射调用时,真实异常在 getCause() 中
            throw ex.getCause() != null ? ex.getCause() : ex;

        } finally {
            // 最终通知(@After)
            if (openTx) {
                LOGGER.info("【JDBC事务】释放资源");
            }
        }
    }
}

这一段在干什么?

  • invoke():统一拦截
    • 日志
    • 事务
    • 权限
  • 本质如下:
代码里的位置Spring AOP 等价物
method.invoke开始前@Before
method.invoke() 正常返回@AfterReturning
catch 块@AfterThrowing
finally 块@After
整个 invoke()@Around

JDK 动态代理的本质是:

  • 面向接口(而不是面向对象)
  • 运行期生成一个 实现了目标接口的代理类

2.6 ServiceFactory:手动“造代理”的地方

package com.yootk.factory;

import com.yootk.service.proxy.ServiceProxy;

public class ServiceFactory {

    private ServiceFactory() {}

    public static <T> T getInstance(Class<T> clazz) throws Exception {
        // 创建真实业务对象
        Object target = clazz.getDeclaredConstructor().newInstance();

        // 创建代理对象
        Object proxy = new ServiceProxy().bind(target);

        // 返回代理,而不是 target 所以不能用 clazz.cast
        return (T) proxy;
    }
}

这一步,决定了所有调用是否会被拦截

⚠️ 关键点(非常重要)

从工厂返回的 不是 DeptServiceImpl
而是 Proxy 对象
所以不能用 clazz.cast(proxy) // clazz = DeptServiceImpl.class,如上所说这是JDK代理面相接口

(T)proxy 为什么不担心泛型擦拭

因为真实的情况:
proxy 实际类型 = jdk.proxy1.$Proxy0
proxy implements = IDeptService

这里的强转安全性并不来自泛型,
而是来自 JDK 动态代理生成的代理类“实现了目标接口” 这一事实。

泛型只是为了让调用方少写一次强转。

代码中用的是

IDeptService deptService = ServiceFactory.getInstance(DeptServiceImpl.class);

接口是匹配的
JVM不需要知道T是谁
调用方法只走接口

2.7 客户端调用

package com.yootk.main;

import com.yootk.factory.ServiceFactory;
import com.yootk.service.IDeptService;
import com.yootk.service.impl.DeptServiceImpl;
import com.yootk.vo.Dept;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class StartService {

    private static final Logger LOGGER =
            LoggerFactory.getLogger(StartService.class);

    public static void main(String[] args) throws Exception {

        IDeptService deptService =
                ServiceFactory.getInstance(DeptServiceImpl.class);

        Dept dept = new Dept();
        dept.setDeptno(10L);
        dept.setDname("教学研发部");
        dept.setLoc("北京");

        // 业务调用
        LOGGER.info("【部门增加结果】{}", deptService.add(dept));
    }
}

2.8 最终效果

【JDBC事务】开启事务
【部门增加】部门编号:10、部门名称:教学研发部、部门位置:北京
【JDBC事务】提交事务
【JDBC事务】释放资源
【部门数据增加】true
StartService.add()
        ↓
Proxy.invoke()
        ↓
【开启事务】
        ↓
DeptServiceImpl.add()
        ↓
【提交事务】

业务方法一行都没改
事务却统一执行

这套代码虽然能跑
每个Service都要通过Factory创建
方法规则写死(startswith(“add”))

三、正式引入Spring AOP

调用方
   ↓
Spring 创建的代理对象
   ↓
AOP 通知(Before / Around / After / Exception)
   ↓
真实业务方法

3.1

3.1.1 开启Spring AOP支持
@EnableAspectJAutoProxy
@ComponentScan("com.yootk.service")
public class AOPConfig {
}

⚠️ 注意:

没有 @EnableAspectJAutoProxy
@Aspect 写得再漂亮也不会生效

@EnableAspectJAutoProxy 等价告诉Spring开启AOP注解
@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true)
exposeProxy=true:允许在类内部获取代理对象,避免 this 调用绕过 AOP,不写默认false
proxyTargetClass=true:强制使用 CGLIB 代理,false=JDK代理,不写默认混合
后边会详细介绍这俩

3.2.1 定义切面类
@Aspect
@Component
public class ServiceAdvice {
}

⚠️ 两个注解缺一不可

注解作用
@Component注册为 Spring Bean
@Aspect告诉 Spring:这是一个切面
3.2.2 定义切点
@Pointcut("execution(* com.yootk..service..*.*(..))")
public void point() {} 

常见的写法:

execution(* com.yootk…service….(…)) // 任意参数
execution(* com.yootk…service….()) // 无参方法
execution(public * com.yootk…service….(String)) // 单一 String 参数
execution(public * com.yootk…service….(String, Integer)) // 两个参数
execution(public * com.yootk…service….(String, …)) // 第一个是String,后边任意个参数
execution(public * com.yootk…service….(*, *)) // 正好两个参数,类型不用关心

部分含义
public仅匹配 public 方法(可省略)省略后匹配除private之外的方法
*返回值任意
com.yootk..service..com.yootk 下任意层级,且路径中包含 service
*任意类
*任意方法
(..)任意参数
(String)参数必须且只能是一个 String
args(msg)运行期参数绑定
msg接收方法中的 String 实参 必须和函数参数名一致

给这个切点表达式起一个“point”的别名,便于复用
可以在多个通知方法(@Before、@After、@Around 等)中引用

3.2.3 无参的AOP
@Before("point()")
public void beforeHandle() {
    LOGGER.info("beforeHandle(): 方法执行前");
}

也可以这么写
@Before(“execution(* com.yootk…service….())”)
@Before(value = “execution(* com.yootk…service….())”)

@After("point()")
public void afterHandle() {
    LOGGER.info("afterHandle(): 方法执行后(无论是否异常)");
}
3.2.4 有参的AOP

带参的切点

@Pointcut("execution(public * com.yootk..service..*.*(String))")
public void stringArgServiceMethods() {}
@Before(value = "stringArgServiceMethods() && args(msg)", argNames = "msg")
public void beforeHandle(String msg) {
    LOGGER.info("beforeHandle(String msg):参数 = {}", msg);
}
// 俩参数的
@Before(
  value = "execution(public * com.yootk..service..*.*(String,String)) && args(msg, test)",
  argNames = "msg,test"
)
public void beforeHandle(String msg, String test) {
    LOGGER.info("msg={}, test={}", msg, test);
}

⚠️ 注意(非常容易踩坑)

  • args(msg)和方法参数必须一一对应
  • argNames 不写,在复杂场景下可能绑定失败
3.2.4 返回值通知(@AfterReturning)
@AfterReturning(
    pointcut = "stringArgServiceMethods() && args(value)",
    returning = "result",
    argNames = "value,result"
)
public void afterReturningHandle(String value, String result) {
    LOGGER.info("返回通知:参数 = {},返回值 = {}", value, result);
}

@AfterReturning 中的 returning
用于绑定目标方法“正常返回”的返回值,
仅在方法未抛异常时生效,且返回值类型必须与通知方法参数兼容。

目标方法(Target Method)
实际执行业务逻辑的方法,通常是
Service / Controller / Repository 中的业务函数。

通知方法(Advice Method)
定义在 AOP 切面(@Aspect)中的方法,
用来在目标方法执行的前 / 后 / 返回 / 异常 / 环绕时织入额外逻辑。

3.2.5 环绕通知(@Around)
@Around("execution(* com.yootk..service..*.*(..))")
public Object handleRound(ProceedingJoinPoint point) throws Throwable {

    LOGGER.info("【环绕前】参数:{}", Arrays.toString(point.getArgs()));

    Object result;

    try {
        // 执行目标方法
        result = point.proceed(point.getArgs());
    } catch (Exception e) {
        LOGGER.info("【环绕异常】{}", e.getMessage());
        throw e;
    }

    LOGGER.info("【环绕后】原始返回值:{}", result);

    // 可以直接篡改返回值
    return "【环绕通知处理后的结果】";
}

⚠️注意:
生产环境中 极少直接篡改返回值
这里只是为了演示 @Around 拥有“完全控制权”的能力。

四、@EnableAspectJAutoProxy:Spring AOP 的“总开关”

@EnableAspectJAutoProxy 的作用可以用一句话概括:

告诉 Spring:
使用代理对象来执行方法调用,并允许使用 @Aspect 注解定义切面

@EnableAspectJAutoProxy

这行代码本身并不复杂,但它背后的两个参数却非常容易被误解:

  • exposeProxy
  • proxyTargetClass

4.1 exposeProxy:解决「类内部方法调用绕过 AOP」的问题

@EnableAspectJAutoProxy(exposeProxy = true)

exposeProxy = true 的真实含义是:

允许在当前线程中,获取“当前正在执行的代理对象”

⚠️ 注意:

它不是“外部可以随便拿到代理对象”,
而是 Spring 在 AOP 调用过程中,把代理对象暴露到 ThreadLocal 中。

为什么会出现「AOP 失效」的问题?
先看一个典型的业务场景(伪代码):

@Service
public class TestService {

    public void a() {
        // 方法 A
        this.b();
    }

    public void b() {
        System.out.println("hi");
    }
}

假设:

a() 方法 ✔ 符合 AOP 切点
b() 方法 ✔ 也符合 AOP 切点

正常的 AOP 调用链(外部调用)

外部调用
   ↓
TestService 代理对象
   ↓
a() 方法(被 AOP 拦截)

到这里,一切都正常。
问题出现在:类的【内部调用】

TestServiceProxy.a()
        ↓
真实对象 TestService.a()
        ↓
this.b()

⚠️ 关键点

this 指向的是"目标对象本身",而不是代理对象

也就是说

  • a()是通过代理对象调用的
  • b()是通过this直接调用的
    结果就是:

b()完全绕过了代理→没有AOP→切面不生效

当exposeProxy = true时候
Spring会在AOP执行期间

  • 把当前代理对象放入ThreadLocal
  • 允许你在方法内部 主动获取代理对象

正确写法如下

import org.springframework.aop.framework.AopContext;

@Service
public class TestService {

    public void a() {
        // 使用代理对象调用,而不是 this
        ((TestService) AopContext.currentProxy()).b();
    }

    public void b() {
        System.out.println("hi");
    }
}

但是不推荐上面写法

  • 这个类 只能运行在 Spring AOP 环境
  • 脱离 Spring(单测 / main 方法 / 迁移)就直接炸
  • 业务代码 知道了 AOP 的存在
  • 违背 AOP 的初衷:业务代码“无感知”

推荐写法

@Service
public class AService {
    @Autowired
    private BService bService;

    public void a() {
        bService.b(); // 天然走代理
    }
}

推荐将需要 AOP 的方法拆分到不同的 Service 中,通过依赖注入进行调用。
因为 Spring 注入的 bService 本身就是代理对象,
调用 bService.b() 一定会经过代理,从而触发事务、日志、权限等 AOP 逻辑,
不存在 this 调用绕过 AOP 的问题。
如果 b() 方法是类内部的私有逻辑,
或者明确不需要任何 AOP(如事务、日志、鉴权等),
则无需强行拆分,直接放在同一个类中使用 this.b() 即可。

外部 → TestServiceProxy.a()
                    ↓
               TestService.a()
                    ↓
      TestServiceProxy.b()   再次被 AOP 拦截

4.2 proxyTargetClass:代理方式的选择(JDK vs CGLIB)

@EnableAspectJAutoProxy(proxyTargetClass = true)
参数值代理方式说明
trueCGLIB基于 子类继承
falseJDK 动态代理基于 接口
不写混合策略优先 JDK,没有接口再用 CGLIB
4.2.1 JDK 动态代理(默认优先)

特点:

  • 只能代理 接口
  • Spring 最早期、最稳定的方式
  • 性能开销小
public interface IMessageService {
    String echo();
}

@Service
public class MessageServiceImpl implements IMessageService {
}

默认情况下,这种结构 一定是 JDK 代理

4.2.2 CGLIB 代理(面向类)

特点:

  • 不需要接口
  • 通过 生成子类 实现代理
  • 无法代理 final 类和 final 方法
@Service
public class OrderService {
    public void createOrder() {}
}

这种情况必须使用 CGLIB

为什么会需要CGLIB
常见原因:

  • 老项目没有接口
  • 统一代理方式,避免混乱
  • 框架源码、第三方类没有接口

结论

  • exposeProxy = true 解决类内部方法调用绕过 AOP
  • proxyTargetClass = true 决定代理是面向接口还是面向类

五、一页总结(面试 / 速记版)

  • Spring AOP 的本质:代理对象 + 方法拦截
  • 默认代理策略:
    • 有接口 → JDK 动态代理
    • 无接口 → CGLIB
  • this 调用为什么绕过 AOP?
    • 因为 this 指向目标对象,而不是代理对象
  • exposeProxy = true 是干嘛的?
    • 把当前代理对象暴露到 ThreadLocal 中
    • 仅用于补救内部调用绕过 AOP 的问题
  • 为什么不推荐 AopContext.currentProxy()?
    • 业务代码感知 AOP,破坏解耦
  • 最佳实践:
    • 需要 AOP 的方法拆到不同 Service
    • 通过 Spring 注入调用,天然走代理
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值