1.AOP
AOP即面向切面编程,那么对于为什么要面向切面编程(AOP),SpringAOP又是什么意思?接下来我将详细解释:
首先我们先来说为什么要面向切面编程,我们想象你在项目中遇到的这样一种情况:我们需要知道调用这个接口需要花费的代价(时间),那么在这种情况下我们需要对于每个接口都去打印耗费时间的日志,那么我们需要再每个接口中都去重复这些代码吗?显然这样做工作量太大了,面向切面编程就很好的解决了这一问题,这种编程方式是对面向对象编程的补充和增强,它会把程序中多个模块都会用到的横切关注点统一的提取出来,进行模块化的实现。
那么典型的横切关注点,比如:日志,安全检查,权限控制,事务处理等。
AOP是一种思想,它的实现方式有很多,有SpringAOP,AspectJ,CGLIB等等, SpringAOP是其中的一种实现方式。
2.AOP的核心概念
我们首先罗列一下核心概念,之后在进行一个详细的解释。
概念 | 解释 |
---|---|
切面(Aspect) | 横切关注点的模块化,比如日志、事务。是 AOP 的核心。 |
连接点(JoinPoint) | 程序中可以插入切面的地方,如方法调用、异常抛出等。 |
通知(Advice) | 实际插入到连接点的代码,如前置、后置、环绕等方法。 |
切点(Pointcut) | 定义通知应用在哪些连接点的规则(用表达式来描述)。 |
织入(Weaving) | 将切面应用到目标对象上,并创建代理对象的过程。 |
目标对象(Target) | 被 AOP 增强的业务对象。 |
在学习AOP的时候我们首先应该先去把它用起来,用起来之后在回过头去慢慢理解。 AOP在Spring框架中的实现方式主要有两种配置方式:注解方式和XML配置方式,我们只介绍注解的实现方式。我们为大家罗列一些常用的注解,方便查找。
注解 | 说明 |
---|---|
@Aspect | 声明一个类为切面 |
@Before | 前置通知,方法执行前运行 |
@After | 后置通知,方法执行后运行(无论是否异常) |
@AfterReturning | 返回通知,方法成功返回后执行 |
@AfterThrowing | 异常通知,方法抛出异常时执行 |
@Around | 环绕通知,方法执行前后都可以执行逻辑(最强大) |
@Pointcut | 定义切点表达式 |
3.AOP的使用
在使用之前我们应该先去pom.xml文件中去导入我们的依赖,不要忘记去刷新我们的maven。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
我们来看一个例子,是用来记录接口的调用时间
@Slf4j
@Component
@Aspect
public class TimeRecordAspect {
@Around("execution(* com.project.librarymanagementsystem.controller.*.*(..))")
public Object record(ProceedingJoinPoint pj) throws Throwable {
long start = System.currentTimeMillis();
Object result = pj.proceed();
log.info(pj.getSignature()+"cost time:"+(System.currentTimeMillis()-start)+"ms");
log.info(pj.getSignature().toString()); //获取签名(方法的声明)
log.info(pj.toLongString()); //类名、方法名、参数全路径
log.info(pj.toShortString()); //简略格式的描述
log.info(String.valueOf(pj.getArgs())); //获取参数
return result;
}
}
首先,我们要在类之间添加注解@Aspect,声明这个类是一个切面,同时添加注解@Component交给spring来管理,添加注解@Slf4j是用来方便打印日志。
@Around是环绕通知注解,表示下面的方法将在目标方法执行前后都执行,可以控制目标方法的执行过程。注解里面的内容是切点表达式。
3.1 切点表达式
切点表达式用于定位连接点,即我们要在哪些方法上应用AOP逻辑
基本语法:
execution([修饰符] 返回类型 [包名.]类名.方法名(参数)) 其中:[]表示可以省略
有关于切点表达式的每一部分含义:
部分 | 说明 |
---|---|
execution | 指定是方法执行这个连接点类型(Spring AOP 只支持这种) |
| 通配符,匹配任意类型或任意方法名 |
| 方法的访问权限,如 public、private,通常省略 |
| 可以写具体类型,如 void 、String ,也可以写 * 表示任意返回类型 |
| 包路径,可以使用 .. 表示多级包 |
| 类名,可以使用 * 表示任意类 |
| 方法名,可以使用 * 表示任意方法 |
| 精确类型、通配符 * (一个参数)、.. (任意个参数,含0个)都可以使用 |
上述代码里面的通配符execution(* com.project.librarymanagementsystem.controller.*.*(..))
这里面就省略了修饰符,最前面的*是表示任意的返回值,并且其表示的含义是,拦截com.project.librarymanagementsystem.controller.*这个包下面的所有类,因为*是通配符可以用来匹配任意类型,所以可以匹配这个包下面的所有类,下一个*的含义也是如此,匹配所有的方法名,最后*(..)表示的是匹配带有任意参数的方法。
我们为大家举一些例子:
- @Around("execution(* com.example.service.UserService.*(..))")
- @Around("execution(String com.example..*.*(..))")
- @Around("execution(* com.example.service.*.get*(..))")
- @Around("execution(* com.example.service.UserService.updateUser(String, int))")
- @Around("execution(public * *(..))")
答案:
- 拦截 UserService 类中所有方法,不管返回什么、不管参数是什么。
- 拦截 com.example 及其子包中,所有返回类型是 String 的方法。
- 匹配 com.example.service 包下所有类中,所有以 get 开头的方法。
- 拦截 UserService 中参数为 (String, int) 的方法。
- 匹配所有 public 的方法,不管类名和包名。
3.2 连接点
上面代码中的参数ProceedingJoinPoint就代表着连接点,它是AOP提供的一个接口,是 JoinPoint 的子接口,专门用于 @Around 环绕通知。它代表连接点(Join Point),即程序中被 AOP 拦截的方法调用。
简单来说,JoinPoint表示的是方法执行前,执行中,执行后的连接点;ProceedingJoinPoint是环绕通知中的JoinPoint,多了一个功能-可以执行目标方法
那么pj又是什么?pj 是 ProceedingJoinPoint 类型的参数,是 Spring AOP 自动传进来的,它表示你拦截到的那个方法的信息和控制器,你可以把它当作: 一个“遥控器 + 方法描述器”
举个例子来解释:
假设你写了一个控制器方法:
@GetMapping("/hello")
public String hello(String name) {
return "Hello, " + name;
}
现在你定义了这个 AOP 切面来拦截:
@Around("execution(* com.project.librarymanagementsystem.controller.*.*(..))")
public Object record(ProceedingJoinPoint pj) throws Throwable {
...
}
那么当你访问 /hello?name=Tom 时:
Spring 会调用你的切面 record() 方法,并把**“hello 方法”对应的信息**封装到 pj 里面传给你!
方法 | 作用 |
---|---|
Object proceed() | 执行原始目标方法 |
Object[] getArgs() | 获取方法参数 |
Signature getSignature() | 获取方法签名对象(方法名、返回类型等) |
String toShortString() | 获取简短方法描述(类名.方法名) |
String toLongString() | 获取完整方法描述(包含参数类型全名) |
Object getTarget() | 获取被代理的目标对象 |
Object getThis() | 获取AOP代理对象 |
3.3 通知
通知就是在切面中编写的“增强逻辑”,比如日志,权限,事务,异常处理等,它描述了你要在目标方法的哪个阶段做什么事情。上面我们为大家罗列的各种注解,那么我们现在要关注的是各种注解的执行顺序是怎样的。
我们首先先写一个Testcontroller类,用来测试我们的注解的执行顺序是怎样的。
package com.study.aopstudy.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("/t1")
public String t1(){
log.info("执行t1方法");
return "t1";
}
@RequestMapping("/t2")
public String t2(){
log.info("执行t2方法");
return "t2";
}
}
之后我们在AspectDemo类中写下如下的方法
package com.study.aopstudy.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class AspectDemo {
@Before("execution(* com.study.aopstudy.controller.*.*(..))")
public void doBefore(){
log.info("执行AspectDemo.before方法");
}
@After("execution(* com.study.aopstudy.controller.*.*(..))")
public void doAfter(){
log.info("执行AspectDemo.after方法");
}
@Around("execution(* com.study.aopstudy.controller.*.*(..))")
public Object doAround(ProceedingJoinPoint pj){
log.info("执行AspectDemo.doAround 目标方法前...");
Object result = null;
try {
result = pj.proceed();
} catch (Throwable throwable) {
log.error(pj.toShortString()+"发生异常, e:",throwable);
}
log.info("执行AspectDemo.doAround 目标方法后...");
return result;
}
}
我们在浏览器的输入127.0.0.1:8080/test/t1 观察我们控制台打印的结果。
通过图片我们可以清晰的看到其注解执行的先后顺序。那么,我们思考在程序运行结果不出错的前提下我们的注解都能正常执行,如果程序出现了bug我们的执行结果又会有什么不同呢。我们对我们的AspectDemo类增加一些新的注解。
@AfterReturning("execution(* com.study.aopstudy.controller.*.*(..))")
public void doAfterReturn(){
log.info("执行AspectDemo.doAfterReturn方法...");
}
@AfterThrowing("execution(* com.study.aopstudy.controller.*.*(..))")
public void doAfterThrow(){
log.info("执行AspectDemo.doAfterThrow方法...");
}
同时我们将t2方法增加一行代码int a = 10/0,这样就会让程序报错,导致结果发生变化,以便我们对两种结果控制台打印的日志进行对比,我们先来看正常情况下打印的日志:
在对t2添加语句后的日志进行查看:
我们可以看到@AfterThrowing在目标方法抛出异常时才执行,@AfterReturning在目标方法正常返回时才会执行。而@Around在方法执行前后都可以增加逻辑,同时还能捕获异常。
3.4切点
从上述的代码我们可以清楚地看到对于每一个切点我们的代码高度重合。无论是 @Before、@After、@AfterReturning 还是 @Around 等通知,切点表达式几乎都是如下形式:
execution(* com.study.aopstudy.controller.*.*(..))
这其实就是 AOP 中的切点表达式(Pointcut Expression)。它用来描述:我要拦截哪些类、哪些方法,从而将通知织入到这些方法中。
切点(Pointcut)用于定义“横切逻辑”作用的目标方法范围,是 Spring AOP 中通知的“入口过滤器”。你可以把它理解为一个“过滤器”或“条件表达式”,用来告诉 Spring:“我只想增强 controller 包里的所有方法,其他方法不要拦!”
当你像我们这样在每个通知中都写一遍 execution(...),虽然功能上没问题,但却存在几个明显的问题:
- 代码重复,不好维护
- 不易阅读,逻辑分散
- 一旦切点需要修改,所有通知都要改,非常繁琐
解决方法:Spring AOP 提供了 @Pointcut 注解,我们可以将切点表达式单独封装成一个方法,其他通知只引用这个方法即可。我们对之前的代码进行一些改造:
@Pointcut("execution(* com.study.aopstudy.controller.*.*(..))")
public void pt(){}
@Before("pt()")
public void doBefore(){
log.info("执行AspectDemo.before方法");
}
@After("pt()")
public void doAfter(){
log.info("执行AspectDemo.after方法");
}
这样修改的优点显而易见:
- 改一次切点,全局生效
- 通知逻辑与切点范围分离,结构更清晰
在实际开发中,随着项目的逐渐复杂,我们通常会把切点表达式进行统一管理。例如我们可能在一个类中专门定义多个切点方法,然后在多个切面中进行复用,如果我们在另一个类中引用我们需要注意引用的格式,我们必须使用全限定类名 + 静态切点方法。
//@After("pt()")错误用法
@After("com.springaop.aop.aspect.AspectDemo.pt()") //正确用法
接下来,我们思考一个问题,当我们定义多个切面类的时候,多个切面类的执行顺序是怎样的,我们应该如何控制他们的执行顺序。假如我们定义了两个切面类它们都有 @Before 通知,都会作用在同一个控制器方法上。但如果你没有加顺序注解,Spring 不保证谁先执行,取决于扫描顺序。但是,如果我们定义的类名称是类似AspectDemo+数字这样的类,那么他会默认按照名称来先后执行的,但是这种方式对于执行顺序来说也是不可控的。
我们需要通过@Order来明确执行的顺序,我们要注意的是再使用@Order注解时我们只能用在类上,不能加在方法上。@Order适用于所有类型通知,以 @Around 为例,执行顺序体现为嵌套:
Order(1) doAround 前
Order(2) doAround 前
controller 方法执行
Order(2) doAround 后
Order(1) doAround 后
3.5自定义注解
我们在做项目中,随着项目越来越大,我们可能不希望再根据包名,类名,方法名去拦截,而是更加希望通过打了某个注解的方式,才能被增强,此时自定义+AOP切点表达式就能够用上了。
我们首先要自定义一个注解类:TimeRecord(这个名字主要是为了说明这个自定义注解的主要功能是用来记录调用接口所需要的时间)
package com.study.aopstudy.aspect;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 记录方法的执行时间
*/
@Retention(RetentionPolicy.RUNTIME)//注解的生命周期
@Target({ElementType.METHOD})//声明作用的地方
public @interface TimeRecord {
}
注解解释:
- @Target(ElementType.METHOD),只能标注在方法上
- @Retention(RetentionPolicy.RUNTIME),在运行时可通过反射读取
- 自定义名TimeRecord可随意命名,例如用于标记“需要统计耗时”的方法
接下来我们要创建一个切面类,专门拦截带这个注解的方法
package com.study.aopstudy.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class AspectDemo2 {
@Around("@annotation(com.study.aopstudy.aspect.TimeRecord)")
public Object timeRecord(ProceedingJoinPoint joinPoint){
Long start = System.currentTimeMillis();
Object result = null;
try {
result = joinPoint.proceed();
} catch (Throwable e) {
log.error(joinPoint.toShortString()+"发生异常,e:",e);
}
log.info(joinPoint.toString() + "执行时间:" + (System.currentTimeMillis() - start) + "ms");
return result;
}
}
之后就是使用注解了,使用非常简单就需要在在控制器中加上注解就可以。
@Slf4j
@RequestMapping("/test")
@RestController
public class TestController {
@TimeRecord
@RequestMapping("/t1")
public String t1(){
log.info("执行t1方法");
return "t1";
}
我们看一下打印台日志的打印结果,
使用自定义注解 + AOP 是企业项目中最常用的做法之一,能够:实现功能增强与业务逻辑完全解耦,提高可读性和可维护性,避免“全包拦截”,支持精确控制。
4.代理模式
代理模式是结构型设计模式,它提供了一个代理对象来控制对目标对象的访问。代理对象在客户端和目标对象之间起到中介作用,可以在访问目标对象前后增加一些功能,比如权限控制,缓存,延迟加载等。代理模式主要使用在对一个类不太适合直接访问的情况下使用。
在代理模式模式中主要由三大核心:Subject(抽象主题),RealSubject(真实主题),Proxy(代理对象),抽象主题主要是定义目标对象与代理对象共同实现的接口(通过这样确保可以被代理);真实对象就是实现实际业务逻辑的类;代理对象就是包裹真实对象,实现相同的接口并且添加增强逻辑。
我们aop的底层正是代理模式应用的场景,spring使用代理模式为方法调用织入横切逻辑。从而体现出aop的本质是用代理对象去封装真实业务对象,在调用方法前后执行切面逻辑。
aop使用两种代理方式:
类型 | 使用条件 |
JDK动态代理 | 有接口的类 |
CGLIB动态代理 | 没有接口或强制代理类 |
4.1静态代理
我们首先通过代码来了解一下静态代理,我这里创建了一个接口HouseSubject,两个类分别是HouseProxy和RealHouseSubject。
package com.study.aopstudy.proxy;
public interface HouseSubject {
void rentHouse();
void saleHouse();
}
package com.study.aopstudy.proxy;
public class HouseProxy implements HouseSubject{
private HouseSubject houseSubject;
public HouseProxy(HouseSubject houseSubject) {
this.houseSubject = houseSubject;
}
@Override
public void rentHouse() {
System.out.println("我是中介, 我开始代理");
houseSubject.rentHouse();
System.out.println("我是中介, 我结束代理");
}
@Override
public void saleHouse() {
System.out.println("我是中介, 我开始代理");
houseSubject.saleHouse();
System.out.println("我是中介, 我结束代理");
}
}
package com.study.aopstudy.proxy;
public class RealHouseSubject implements HouseSubject{
@Override
public void rentHouse() {
System.out.println("我是房东,出租房子");
}
@Override
public void saleHouse() {
System.out.println("我是房东, 我出售房子");
}
}
我们来对应一下之间说的,
代理模式角色 | 对应类名 |
Subject(抽象主题) | HouseSubject 接口 |
RealSubject(真实对象) | RealHouseSubject 实现类 |
Proxy(代理对象) | HouseProxy 实现类(封装了目标对象) |
首先我们在HouseSubject接口中,定义了房屋服务的通用接口,RealHouseSubject 和 HouseProxy 都要实现它,以便客户端统一调用,AOP 中也是通过接口让代理对象与目标对象具有相同行为。然后实际业务逻辑由RealSubject提供,HouseProxy 去包装RealSubject类并且在方法前后添加增强行为。这三者结构与 AOP 中的代理机制一一对应,只是 AOP 是动态代理 + 通知注解 + 自动织入,不需要自己显式写代理类而已。
4.2JDK动态代理
我们同样通过上面这个中介代理租房的例子来进行讲解。我们创建一个JDKInvocationHandler类。
package com.study.aopstudy.proxy;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
@Slf4j
public class JDKInvocationHandler implements InvocationHandler {
private Object target;
public JDKInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("JDK动态代理开始");
//调用目标函数
Object result = method.invoke(target, args);
log.info("JDK动态代理结束");
return result;
}
}
我们在Main函数中这样调用
//JDK动态代理
//目标类
HouseSubject target = new RealHouseSubject();
//生成代理对象
HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
new Class[]{HouseSubject.class},
new JDKInvocationHandler(target));
proxy.rentHouse();
proxy.saleHouse();
我们将静态代理升级成了 JDK 动态代理——仍然遵循代理模式的三角色,但把「代理类」的生成交给 JDK 在运行期完成。我们之前说过JDK代理和CGLIB代理有一定的区别,我们在额外创建一个类RealHouseSubject,但是这个类我们并不去实现接口而是写成一个普通类,去观察运行结果的区别。
package com.study.aopstudy.proxy;
public class RealHouseSubject2 {
public void rentHouse() {
System.out.println("我是房东, 我出租房子");
}
public void saleHouse() {
System.out.println("我是房东, 我出售房子");
}
}
RealHouseSubject2 target = new RealHouseSubject2();
RealHouseSubject2 proxy = (RealHouseSubject2) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
new Class[]{RealHouseSubject2.class},
new JDKInvocationHandler(target));
proxy.rentHouse();
proxy.saleHouse();
我们发现我们把一个普通类穿进去之后,代码抛出异常,展现出JDK不能代理普通类的问题。但是在CGLIB中我们可以去代理普通类。
4.3CGLIB动态代理
我们同样通过上面这个中介代理租房的例子来进行讲解。我们创建一个CGLibMethodInterceptor类。
package com.study.aopstudy.proxy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
@Slf4j
public class CGLibMethodInterceptor implements MethodInterceptor {
private Object target;
public CGLibMethodInterceptor(Object target) {
this.target = target;
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
log.info("CGLib动态代理开始");
//调用目标方法
Object result = method.invoke(target, objects);
log.info("CGLib 动态代理结束");
return result;
}
}
//CGlib动态代理
HouseSubject target1 = new RealHouseSubject();
HouseSubject proxy1 = (HouseSubject) Enhancer.create(
target1.getClass(),
new CGLibMethodInterceptor(target1));
proxy1.rentHouse();
proxy1.saleHouse();
//CGLib代理非接口类
RealHouseSubject2 target2 = new RealHouseSubject2();
RealHouseSubject2 proxy2 = (RealHouseSubject2) Enhancer.create(
target2.getClass(),
new CGLibMethodInterceptor(target2));
proxy2.rentHouse();
proxy2.saleHouse();
无论真实对象是不是一个接口,CGLIB都可以代理。
4.4代理选择
Spring Framework与Spring Boot在AOP代理的的底层实现都是JDK和CGLIb。
但是他俩也有不同的地方,对于Spring Framework来说,如果代理的是接口,那么就使用JDK,如果代理的是没有实现接口的类,就是用CGLIb;对与Spring Boot来说在SpringBoot 2.x版本之后,默认配置使用CGLIb代理,代理的无论是否实现了接口,都是用CGLIb代理,如果需要使用JDK代理,需要手动设置(即使手动动配置了,经过一些判断之后,最后也可能使用CGLIb代理)。在SpringBoot 2.x版本之前他与Spring Framework一样默认使用的JDK代理。
5.总结
AOP(面向切面编程)是一种编程思想,用于将横切关注点(如日志、事务、权限控制等)从业务逻辑中分离出来,实现模块化管理。Spring AOP是其一种实现方式,通过动态代理技术在运行时将切面逻辑织入目标方法中。核心概念包括切面(Aspect)、连接点(JoinPoint)、通知(Advice)、切点(Pointcut)等,开发者可以通过注解(如@Aspect、@Around等)快速实现AOP功能。Spring AOP支持JDK动态代理(基于接口)和CGLIB动态代理(基于类),默认情况下Spring Boot 2.x以上版本优先使用CGLIB。AOP的应用显著提升了代码的可维护性和复用性,是Spring框架中不可或缺的重要特性。