欢迎光临
我们一直在努力

Spring AOP 的八个概念,我用一个例子全串起来

刚开始学 Spring AOP 的时候,我被一堆名词搞懵了:切面、切点、连接点、通知、织入……每个字都认识,放一起就不知道在说什么。看了好几遍文档,感觉理解了,过两天又全忘了。

后来我强迫自己用一个完整的例子把这八个概念串了一遍,才算是真正记住。这篇就是这个整理的过程。

先说 AOP 是干嘛的

假设你写了一个用户服务,里面有注册、登录、修改密码这些方法。现在产品经理说,每个方法执行的时候都要记日志,还要加权限校验。

最直觉的做法是在每个方法里面加上日志和校验的代码。但问题是,这些代码和业务逻辑没关系,每个方法都复制一遍,既啰嗦又容易漏。而且哪天要改日志格式,你得把所有方法都改一遍。

AOP 就是为了解决这种事:把和业务无关的"横切"逻辑(日志、权限、事务)抽出来,统一放到一个地方管理。业务代码完全不用碰。

那它是怎么做到的?这就涉及到下面这些概念了。我用一个"办公楼安保"的比喻来串。

连接点(Join Point):哪些地方可以动手脚

程序运行过程中,有很多地方理论上可以被拦截。方法调用的时候、抛异常的时候、修改字段的时候,这些都叫连接点。

但在 Spring AOP 里,因为底层用的是动态代理,所以连接点只支持一种:方法的执行。也就是说,你只能拦截方法。

比喻的话,想象一栋办公楼,里面有很多房间。每个房间的门就是一个连接点,代表一个可以"做点事情"的位置。Spring AOP 的限制是,你只能在门口拦人,不能在房间里面装监控。

切点(Pointcut):到底拦哪些门

连接点有很多,程序里可能有几百个方法,但你不可能每个方法都去拦截。切点的作用就是从这些连接点里筛出你真正想拦的那几个。

你可以用正则匹配、路径匹配或者注解来定义规则。比如"拦截 com.example.service 包下所有类的所有方法",或者"只拦截带 @NeedLogin 注解的方法"。

回到办公楼的比喻:切点就是安保人员手里的"重点巡查名单"。只有名单上标了的房间才需要去查,其他的不用管。

@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}

这行代码定义了一个切点:service 包下所有类的所有方法。

通知(Advice):拦下来之后做什么

切点告诉你"在哪里拦",通知告诉你"拦下来之后干什么"以及"什么时候干"。

Spring 里有五种通知类型:

前置通知(Before):方法执行之前触发。比如在执行之前先检查一下用户有没有登录。

后置通知(After):方法执行之后触发,不管方法是正常结束还是抛了异常都会执行。类似于"不管发生什么,最后都要登记一下离开时间"。

返回通知(After-returning):方法正常返回之后触发。如果方法抛异常了,这个不会执行。可以用来记录方法的返回值。

异常通知(After-throwing):方法抛异常之后触发。可以用来记录错误日志或者发送告警。

环绕通知(Around):把目标方法整个包起来。你可以在方法执行前后做事,甚至可以决定要不要执行目标方法、修改参数、修改返回值。功能最强大,但也最容易写出问题。

用办公楼的比喻:前置通知是"进门前检查身份证",后置通知是"不管发生什么都登记离开时间",异常通知是"遇到突发情况立刻报警"。环绕通知就像是安保人员全程陪着你在房间里办事,你做什么他都知道,甚至可以打断你。

@Around("serviceLayer()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed(); // 执行目标方法
long cost = System.currentTimeMillis() – start;
log.info("方法执行耗时: {}ms", cost);
return result;
}

这段环绕通知做的事情很简单:记录方法执行花了多长时间。但它不需要写在任何一个业务方法里,只需要在这一个地方写一次。

切面(Aspect):把切点和通知打包到一起

切面就是一个类,里面同时包含切点和通知。它回答了两个问题:在哪里拦(切点),拦下来做什么(通知)。

在 Spring 里就是一个加了 @Aspect 注解的普通类:

@Aspect
@Component
public class LogAspect {

@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}

@Around("serviceLayer()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long cost = System.currentTimeMillis() – start;
log.info("方法执行耗时: {}ms", cost);
return result;
}
}

用比喻来说,切面就是一份完整的"安保方案"。方案里写清楚了哪些房间要查(切点),以及查的时候具体怎么做(通知)。有了这份方案,安保工作就有章可循了。

目标对象(Target Object):被代理的那个原始对象

就是你的业务类本身。比如 UserService,里面有注册、登录的方法,这些是核心业务逻辑。它不应该关心日志怎么记、权限怎么验,它只管做自己该做的事。

比喻里就是办公楼里正常办公的员工,只管自己的本职工作,安保的事不用他操心。

AOP 代理(AOP Proxy):外部实际调用的那个对象

当你用了 AOP 之后,Spring 不会直接把 UserService 交给你用,而是给你一个代理对象。你调方法的时候,实际上是调的代理。代理会在调用真正的方法前后,帮你执行通知里的逻辑。

也就是说,你以为你调的是 UserService,其实调的是 Spring 给你包了一层的东西。

Spring 里生成代理有两种方式:

JDK 动态代理:基于接口。要求你的目标对象至少实现了一个接口。代理对象会实现同样的接口,然后把请求转发给目标对象。

CGLIB 动态代理:基于继承。代理对象是目标对象的子类,重写了目标对象的方法。所以即使目标对象没有实现任何接口,CGLIB 也能做代理。

Spring 默认的策略是:如果目标对象有接口,用 JDK 动态代理;没有接口,就用 CGLIB。不过 Spring Boot 2.x 之后默认改成了 CGLIB,因为有些场景下 JDK 代理会有限制。

用比喻来说,代理就是大楼的前台接待处。访客(调用者)来了不直接找员工(目标对象),而是先到前台。前台在让你见员工之前先查你的身份证(前置通知),你办完事走了再登记离开时间(后置通知)。

织入(Weaving):把方案落地的过程

前面说了切面是方案,代理是最终效果。织入就是从方案到效果的那个执行过程:把切面应用到目标对象,生成代理对象。

织入可以在不同时机发生:

  • 编译期:代码编译的时候就生成代理。AspectJ 的编译器可以做这件事。
  • 类加载期:类加载到 JVM 的时候修改字节码。AspectJ 有一个 Load-Time Weaving 就是这种。
  • 运行期:程序跑起来之后动态生成代理。Spring AOP 默认用的就是这种方式。

Spring AOP 选了运行期织入,因为对开发者最友好。你不用换编译器,不用加 JVM 参数,只要配好切面,Spring 启动的时候自动就帮你搞定。

比喻:织入就是"把安保方案落实到办公楼里"这件事本身。

引介(Introduction):给对象加一个新能力

这个概念比较少见。引介允许你在运行时给一个对象动态加上新的接口和实现,即使那个对象原来并没有实现这个接口。

比如你的 UserService 本来没有实现 Serializable 接口,通过引介可以让它在运行时具备序列化的能力。

说实话我在实际项目中没用过引介。大部分场景下,如果一个类需要某个接口,直接在类定义里加上就好了,不需要运行时动态添加。不过知道有这个东西,以后遇到特殊场景的时候不至于完全没概念。

比喻:引介就像给一个普通员工临时发了一个"特殊权限徽章",让他能进某些特定的区域,但员工本身的编制没变。

回过头来看这八个概念的关系

用一句话串一遍:

程序运行中有很多方法可以被拦截,这些方法就是连接点。你通过切点从中筛出你真正想拦的方法。拦下来之后做什么、什么时候做,由通知定义。切面把切点和通知打包在一起,形成一个完整的模块。Spring 把这些逻辑应用到目标对象上,通过织入生成一个AOP 代理,外部调用的其实是这个代理。至于引介,是运行时给对象加新接口的特殊能力,平时用得少。

这八个概念看着多,但核心就三件事:在哪里拦(连接点 + 切点),做什么(通知 + 切面),怎么生效(目标对象 + 代理 + 织入)。搞清楚这三件事,其他的概念自然就串起来了。

赞(0)
未经允许不得转载:171主机测评 » Spring AOP 的八个概念,我用一个例子全串起来
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址