目录
AOP介绍
面向切面编程(AOP,Aspect-Oriented Programming)是一种编程范式,旨在通过分离横切关注点(cross-cutting concerns)来提升代码的模块化。以下是其核心要点:
AOP 的核心思想
-
横切关注点:指那些分散在多个模块中的通用功能(如日志、事务、安全等),传统OOP难以集中管理,导致代码冗余。
-
切面(Aspect):将横切关注点封装为独立模块,通过声明式或编程方式注入到业务逻辑中,实现关注点分离。
关键概念
切面(Aspect)
封装横切逻辑的模块(如日志切面),包含通知和切点定义。
切面是由通知和切点组成的
连接点(Join Point)
程序执行中可插入切面的点(如方法调用、异常抛出)。
连接点是程序运行流程中一个个“可插入切面代码的时机点”,比如方法被调用的瞬间、异常抛出的那一刻。说白了就是我们要调用增强的方法
通知(Advice)拦截到连接点之后我们要做的事情,即在什么时候增强,如何增强
切面在连接点执行的动作类型:
前置通知(Before):方法执行前。
后置通知(After):方法执行后(无论成功或异常)。
返回通知(AfterReturning):方法正常返回后。
异常通知(AfterThrowing):方法抛出异常后。
环绕通知(Around):包裹目标方法,控制其执行。
切点(Pointcut)
通过表达式(如正则或AspectJ语法)匹配哪些连接点需应用通知。例如:execution(* com.example.service.*.*(..))
匹配某包下所有方法。
织入(Weaving)将切面代码插入目标位置的过程,可在编译时、类加载时或运行时实现。
1. 织入的定义
织入(Weaving) 是AOP(面向切面编程)的核心机制,指将切面(Aspect)中定义的横切逻辑(如日志、事务等)插入到目标程序指定位置的过程。简单来说,它像是将分散的“增强代码”缝合到主业务逻辑中,使两者在运行时协同工作。
2. 织入的时机与方式
根据切面代码插入的时机,织入可分为以下三种主要类型:
织入类型 | 执行阶段 | 实现方式 | 典型框架 | 优点 | 缺点 |
---|---|---|---|---|---|
编译时织入 | 源代码编译期间 | 使用AspectJ编译器(ajc)直接修改字节码 | AspectJ | 无运行时开销,性能最优 | 需专用编译器,构建流程复杂 |
类加载时织入(LTW) | JVM加载类时 | 通过Java Agent动态修改字节码 | AspectJ + Spring | 无需重新编译,适合动态环境 | 需配置JVM参数,启动时间略长 |
运行时织入 | 应用程序运行期间 | 动态代理(JDK/CGLIB)生成代理对象 | Spring AOP | 简单易用,与Spring无缝集成 | 仅支持方法拦截,性能略低 |
AOP 的实现方式
-
动态代理(如Spring AOP):基于接口或CGLIB生成代理对象,在运行时拦截方法调用。
-
字节码操作(如AspectJ):编译时或加载时修改字节码,支持更细粒度的控制(如字段访问、构造方法)。
AOP 的优势
-
解耦:业务逻辑与横切关注点分离,代码更清晰。
-
复用性:通用功能集中管理,减少重复代码。
-
可维护性:修改横切逻辑时只需调整切面,无需改动业务代码。
典型应用场景
-
日志记录:自动记录方法入参、返回值或异常。
-
事务管理:统一开启/提交/回滚事务。
-
权限校验:在方法调用前验证用户权限。
-
性能监控:统计方法执行时间。
依赖引入
首先我们需要先引入AOP的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
切入点表达式
切入点表达式(Pointcut Expression)是AOP中用于定义 哪些连接点(Join Point)需要被切面拦截 的核心工具。它通过特定的语法规则描述目标方法、类或注解,从而精准控制切面的作用范围。以下是其核心内容:
1. 切入点表达式的核心语法
(1) 执行表达式(execution
)
最常用的表达式,用于匹配 方法执行 的连接点。
语法结构:
execution(修饰符? 返回类型 包名.类名.方法名(参数类型) 异常类型?)
-
?
表示可选部分(如修饰符和异常类型通常省略)。 -
使用通配符
*
匹配任意字符,..
匹配任意多个参数或包路径。
示例:
表达式 | 匹配目标 |
---|---|
execution(* com.example.service.*.*(..)) | com.example.service 包下所有类的任意方法(任意参数和返回类型)。 |
execution(public String getUser*(..)) | 所有以getUser 开头、返回String 类型的公共方法。 |
execution(* com.example..*.save*(..)) | com.example 包及其子包下所有类中以save 开头的方法。 |
切入点表达式(Pointcut Expression)是AOP中用于定义 哪些连接点(Join Point)需要被切面拦截 的核心工具。它通过特定的语法规则描述目标方法、类或注解,从而精准控制切面的作用范围。以下是其核心内容:
(2) 类型签名表达式(within
)
匹配 特定类或包内 的所有连接点(仅支持类和方法级别,不支持参数)。
示例:
within(com.example.service.UserService) // UserService类中的所有方法 within(com.example.service.*) // service包下所有类的所有方法 within(@com.example.anno.Secured *) // 被@Secured注解的类中的所有方法
(3) 参数匹配表达式(args
)
根据 方法参数类型 匹配连接点(不关心方法名和类名)。
示例:
args(String, int) // 匹配有两个参数且类型为String和int的方法
args(com.example.model.User) // 匹配参数中至少有一个User类型的方法
(4) 注解匹配表达式
通过注解精准拦截目标方法或类:
-
@annotation
:匹配 方法上带有指定注解 的连接点。@Before("@annotation(com.example.anno.Log)") // 拦截所有被@Log注解的方法
-
@within
:匹配 类上带有指定注解 的所有方法。@Around("@within(com.example.anno.Transactional)") // 拦截被@Transactional注解类的方法
-
@target
:匹配 目标对象(非代理对象)的类上带有指定注解 的方法。
(5) 逻辑运算符
组合多个表达式,实现复杂条件过滤:
-
&&
(与):同时满足两个条件。@Pointcut("execution(* com.example.service.*.*(..)) && args(user)") public void userServiceMethods(User user) {}
-
||
(或):满足任意一个条件。 -
!
(非):排除符合条件的连接点。
(6) Spring AOP 特有表达式
-
bean
:按Spring Bean的ID或名称匹配。@Before("bean(userService)") // 拦截ID为userService的Bean的所有方法 @Before("bean(*Service)") // 拦截所有名称以Service结尾的Bean的方法
前置通知@Before
我们要在连接点执行前要执行一定的逻辑代码的话我们就使用@Before来切入
首先我们要定义一个切面类,并将这个切面类交给Spring容器管理,切面类注解@Aspect
切面类
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAspect {
@Before("execution(* com.part1.Controller.*.*(..))")//第一个*是代表着访问权限即public等,后面是代表着com.part1.Controller包下所有的类下所有的方法,而参数任意就是..代替,代表着任意个参数
public void before(JoinPoint joinPoint) {
String string = joinPoint.getArgs()[0].toString();//获取参数的第一个的值
System.out.println("进入Before");
System.out.println("参数值为"+string);
System.out.println("离开Before");
}
}
我们定义的切入点就是* com.part1.Controller.*.*(..),他代表着com.part1.Controller包下面的所有的方法,注意我们定义的切入点都是要在execution函数里面的,而JoinPoint就是我们说的连接点,他可以拿到我们调用的方法的各方面数据,而这个方法里面定义的逻辑就是我们在调用连接点之前要进行的逻辑
controller
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("/test")
public String test(@RequestParam String name) {
System.out.println("进入了controller");
return "Hello " + name;
}
}
此时我们发起请求观察控制台
切面类添加@Component注解的原因
Spring AOP的核心机制是动态生成代理对象(运行时织入的方式)来包裹目标对象。切面类需要被Spring容器管理,才能让AOP框架识别到它,并将其逻辑织入到代理对象中。
具体流程:
-
Spring容器启动时,扫描所有Bean。
-
发现被
@Aspect
注解的类(切面类)且该类已被Spring管理(如@Component
)。 -
根据切面类中定义的切点表达式,确定需要代理的目标Bean。
-
为目标Bean创建代理对象(JDK动态代理或CGLIB代理),并将切面逻辑(通知)注入到代理对象中。
-
后续调用目标Bean的方法时,实际执行的是代理对象的方法,从而触发切面逻辑。
若切面类未交给Spring管理 → 框架无法发现切面 → 代理对象不会生成 → 切面逻辑失效。
JoinPoint的常用方法
方法名 | 返回值类型 | 说明 |
---|---|---|
getSignature() | Signature | 返回当前连接点的方法签名(MethodSignature 或 FieldSignature ),可进一步获取方法名、声明类等信息。 |
getArgs() | Object[] | 返回目标方法的参数列表。例如:Object[] args = joinPoint.getArgs() 。 |
getTarget() | Object | 返回被代理的目标对象(即原始对象)。例如:UserService target = (UserService) joinPoint.getTarget() 。 |
getThis() | Object | 返回代理对象本身(JDK动态代理或CGLIB代理)。例如:Object proxy = joinPoint.getThis() 。 |
getKind() | String | 返回连接点类型(如method-execution 、method-call 等),具体值取决于AOP框架(如AspectJ)。 |
toShortString() | String | 返回连接点的简短描述(如execution(UserService.saveUser) )。 |
toLongString() | String | 返回连接点的完整描述(包含类、方法、参数等详细信息)。 |
getStaticPart() | StaticPart | 返回连接点的静态部分(如方法签名、字段等),不包含动态信息(如参数值)。 |
getSignature():获取被调用的方法信息。
Signature对象的方法:
getName():获取被调用的方法名。比如:queryAllDepts
getDeclaringType():获取被调用方法所属的类Class
getDeclaringTypeName():获取被调用方法所属的类的全限定类名
toLongString():获取方法的完整名称。
比如:public java.util.List com.itheima.service.impl.DeptServiceImpl.queryAllDepts()
toShortString:获取方法的简短名称。
比如:DeptServiceImpl.queryAllDepts()
toString:获取方法信息。
比如:List com.itheima.service.impl.DeptServiceImpl.queryAllDepts()
@AfterReturning后置通知
这个注解标注的方法时在目标方法正常执行结束之后再执行的
代码
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAspect {
@AfterReturning("execution(* com.part1.Controller.*.*(..))")//第一个*是代表着访问权限即public等,后面是代表着com.part1.Controller包下所有的类下所有的方法,而参数任意就是..代替,代表着任意个参数
public void before(JoinPoint joinPoint) {
String string = joinPoint.getArgs()[0].toString();//获取参数的第一个的值
System.out.println("进入AfterReturning");
System.out.println("参数值为"+string);
System.out.println("离开AfterReturning");
}
}
此时发送请求查看控制台
可以看见@AfterReturning时在controller成功执行之后返回的执行
@AfterThrowing异常通知
代码
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAspect {
@AfterThrowing("execution(* com.part1.Controller.*.*(..))")//第一个*是代表着访问权限即public等,后面是代表着com.part1.Controller包下所有的类下所有的方法,而参数任意就是..代替,代表着任意个参数
public void before(JoinPoint joinPoint) {
String string = joinPoint.getArgs()[0].toString();//获取参数的第一个的值
System.out.println("进入AfterThrowing");
System.out.println("参数值为"+string);
System.out.println("离开AfterThrowing");
}
}
我们此时修改controller手动抛出一个异常观察
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("/test")
public void test(@RequestParam String name) {
System.out.println("进入了controller");
throw new RuntimeException();
// return "Hello " + name;
}
}
观察控制台
可以看见他调用了我们定义的切面方法
@After最终通知
在目标方法执行之后执行,无论他是否抛出异常
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAspect {
@After("execution(* com.part1.Controller.*.*(..))")//第一个*是代表着访问权限即public等,后面是代表着com.part1.Controller包下所有的类下所有的方法,而参数任意就是..代替,代表着任意个参数
public void before(JoinPoint joinPoint) {
String string = joinPoint.getArgs()[0].toString();//获取参数的第一个的值
System.out.println("进入AfterThrowing");
System.out.println("参数值为"+string);
System.out.println("离开AfterThrowing");
}
}
我们在controller手动抛出异常查看控制台
我们不抛出异常的时候
他还是调用了我们定义的切面方法
@Around环绕通知
他可以定义方法执行前和执行后的逻辑
@Component
@Aspect
public class MyAspect {
@Around("execution(* com.part1.Controller.*.*(..))")//第一个*是代表着访问权限即public等,后面是代表着com.part1.Controller包下所有的类下所有的方法,而参数任意就是..代替,代表着任意个参数
public Object before(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("进入到Around,还未进入被调用的方法");
Object proceed = proceedingJoinPoint.proceed();//这句话返回的是我们方法调用之后,对应方法的返回值,用Object接收的
System.out.println("从被调用的方法中退出,再次进入到Around,方法的返回值为"+proceed.toString());
return proceed;//返回结果,这一步代替了原来被调用的方法的return
}
}
Object proceed = proceedingJoinPoint.proceed()这一步是用来手动调用目标方法的,注意,必须要返回目标方法的返回值,如果没有返回目标方法的返回值那么我们请求接收的就是一个空值
我们调用 proceed的时候还可以传入一个Object数组来动态修改参数
查看返回结果
ProceedingJoinPoint的核心方法
方法名 | 返回值类型 | 说明 |
---|---|---|
proceed() | Object | 执行目标方法,返回其原始返回值。 |
proceed(Object[] args) | Object | 使用修改后的参数数组执行目标方法。 |
getArgs() | Object[] | 获取目标方法的参数数组(继承自 JoinPoint )。 |
getSignature() | Signature | 获取方法签名(继承自 JoinPoint ),可进一步提取方法名、返回类型等信息。 |
getTarget() | Object | 获取被代理的原始目标对象(继承自 JoinPoint )。 |
getThis() | Object | 获取代理对象本身(继承自 JoinPoint )。 |
@Pointcut公共切入点
当我们在一个切面类里面定义了多个方法,且他们的切入点都是相同的,我们此时就能额外的定义一个方法来抽出这个公共切入点
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAspect {
@Pointcut("execution(* com.part1.Controller.*.*(..))")
public void pointcut(){}
@Around("pointcut()")
public Object before(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("进入到Around,还未进入被调用的方法");
Object proceed = proceedingJoinPoint.proceed();//这句话返回的是我们方法调用之后,对应方法的返回值,用Object接收的
System.out.println("从被调用的方法中退出,再次进入到Around,方法的返回值为"+proceed.toString());
return proceed;
}
}
我们另外定义一个方法来承载这个切入点表达式,而其他通知注解里面直接调用这个方法就行了,注意这个方法的名字的自定义的
基于注解来选择要切入的连接点
我们有时候可能对一个包里面的某些方法来添加切面,但是他们的共性不强,比如他们的方法名没有共同点,此时我们就可以通过注解的方式来设置特定的方法执行切面
此时我们定义一个注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)//这一步代表我们要将这个注解加的范围,即通过target注解我们将这个注解加载方法上面
@Retention(RetentionPolicy.RUNTIME)//这个就是指定这个注解的作用时间,即在运行的时候作用
public @interface Myannotation {
}
切面类
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAspect {
@Around("@annotation(com.part1.Annotation.Myannotation)")//里面填入的是注解的全限定名
public Object before(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("进入到Around,还未进入被调用的方法");
Object proceed = proceedingJoinPoint.proceed();//这句话返回的是我们方法调用之后,对应方法的返回值,用Object接收的
System.out.println("从被调用的方法中退出,再次进入到Around,方法的返回值为"+proceed.toString());
return proceed;
}
}
我们此时在通知注解里面就不使用excution了,这个是指定包下面的某些方法的,而我们直接使用@annotation,注意在切入点表达式里面文明可以通过逻辑符号即&& 和 ||来定义多个注解即
场景 | 表达式示例 | ||
---|---|---|---|
匹配单个注解 | @annotation(com.example.Log) | ||
匹配多个注解(或) | `@annotation(com.example.Log) | ||
匹配多个注解(与) | @annotation(com.example.Log) && @annotation(com.example.Secure) | ||
匹配自定义组合注解 | @annotation(com.example.SecuredLog) | ||
结合其他切点表达式 | execution(* com.example.service.*.*(..)) && @annotation(com.example.Log) |
此时我们给controller层添加自定义的注解
import com.part1.Annotation.Myannotation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@Myannotation
@GetMapping("/test")
public String test(@RequestParam String name) {
System.out.println("进入了controller");
return "Hello " + name;
}
}
观察结果
自定义注解进阶
当我们在使用自定义的注解的时候我们可能会要到一种情况就是当我们给这个注解设置的值会影响到我们在切面的操作
比如
我们自定义一个枚举TestEnmu
/**
* 自定义枚举
*/
public enum TestEnmu {
/**
* 加法操作
*/
ADD,
/**
* 减法操作
*
*/
SUB
}
注解
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 Myannotation {
/**
* @return
* 给注解添加一个value参数,他接收的类型是TestEnmu类型
*/
TestEnmu value();
}
controller层
import com.part1.Annotation.Myannotation;
import com.part1.Enmu.TestEnmu;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@Myannotation(value = TestEnmu.ADD)
@GetMapping("/test")
public String test(@RequestParam String name) {
System.out.println("进入了controller");
return "Hello " + name;
}
}
然后我们修改切面里面的逻辑
import com.part1.Annotation.Myannotation;
import com.part1.Enmu.TestEnmu;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import javax.xml.crypto.dsig.SignatureMethod;
@Component
@Aspect
public class MyAspect {
@Around("@annotation(com.part1.Annotation.Myannotation)")//里面填入的是注解的全限定名
public Object before(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
Myannotation annotation = signature.getMethod().getAnnotation(Myannotation.class);
TestEnmu value = annotation.value();
if (value == TestEnmu.ADD){
System.out.println("加法");
return proceedingJoinPoint.proceed();
}
System.out.println("减法");
return proceedingJoinPoint.proceed();
}
}
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();这一步是在获取当前连接点的方法签名,proceedingJoinPoint.getSignature() 返回的是 Signature 接口的实例。MethodSignature 是 Signature 的子接口,专门表示方法签名,包含方法名、参数类型、返回类型等信息。
MyAnnotation annotation = signature.getMethod().getAnnotation(MyAnnotation.class);他是从当前方法签名上先获得目标对象,再从目标对象上面获得MyAnnotation这个注解的实例
而TestEnum value = annotation.value();是获得这个注解的value属性的值,这个是完整的过程
然后我们观察他是否进入了if判断条件里面,因为我们开始设置的value的值就是ADD
事实证明他确实进入了if判断语句,那么我们就成功的拿到了注解里面的value属性的值
以上大概就是AOP切面编程的使用,我只讲述了证明使用,但是他底层原理我没有讲。
最后
本人的第十四篇博客,以此来记录我的后端java学习。如文章中有什么问题请指出,非常感谢!!!