目录
一.什么是AOP?
二.SpringAOP的简单实现
三.Spring AOP 详解
1.切点(Pointcut)
切点的定义方式
切点表达式的语法
1) execution表达式
1. *(星号)
2. ..(两个点)
3. +(加号)
2)@annotation
2.连接点(Join Point)
3.通知(Advice)
特点和分类
4.切面(Aspect)
四.Spring AOP 的辅助注解
1. @PointCut注解
2. @Order注解
五.Spring AOP 原理
一.什么是AOP?
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,旨在使横切关注点的实现更加模块化和可维护。横切关注点是那些影响应用程序中多个类或模块的功能,例如日志记录、事务管理、安全性和缓存等。
AOP通过将横切关注点从它们所影响的对象中分离出来,然后以一种更模块化的方式将其应用于应用程序的组件中。这样可以避免将相同的代码散布在整个应用程序中,提高了代码的可重用性和可维护性。
(AOP是⼀种思想, 它的实现方法有很多, 而我们要介绍的Spring AOP是其中的⼀种实现方式)
二.SpringAOP的简单实现
在具体介绍SpringAOP之前,我们先简单实现一个AOP来统计方法的执行时间.
1)引入AOP相关的配置
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2)引入作为例子的实体类Cal(计算器)
@Component
public class Cal {//加法运算public int add(int num1,int num2){return num1+num2;}//减法运算public int sub(int num1,int num2){return num1-num2;}//乘法运算public int mul(int num1,int num2){return num1*num2;}//除法运算public int div(int num1,int num2){return num1/num2;}}
3)实现关于时间统计的AOP类
@Component
@Aspect
public class TimeAspect {@Around("execution(* com.example.demo.aop.Cal.*(..))")public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {//记录开始时间long begin=System.currentTimeMillis();//执行目标类的方法Object result=pjp.proceed();//记录结束时间long end=System.currentTimeMillis();//打印结果System.out.println(pjp.getSignature().getName()+"方法的执行时间为:"+(end-begin));//返回原方法的结果return result;}
}
- @Aspect: 标识这是⼀个切面类
- @Around: 环绕通知, 在目标方法的前后都会被执行,后面的表达式表示对哪些方法进行增强.
- ProceedingJoinPoint.proceed() 让原始方法执行
4)在Test类中进行测试
@SpringBootApplication
public class Test {public static void main(String[] args) {//获取IOC容器ApplicationContext context=new AnnotationConfigApplicationContext("com.example.demo.aop");//获取容器里的代理类Cal cal=context.getBean(Cal.class);//运行代理类中的方法System.out.println("结果为"+cal.add(5, 4));}
}结果为:add方法的执行时间为:8结果为9Process finished with exit code 0
@SpringBootApplication:
该注解本质上是以下三个注解的组合:
@Configuration
: 表示这个类可以使用Spring IoC容器作为bean定义的来源。@EnableAutoConfiguration
: 告诉Spring Boot根据添加的jar依赖自动配置Spring应用程序。@ComponentScan
: 告诉Spring在该包及其子包中寻找其他组件、配置和服务
在你的主类(通常是包含main
方法的类)上使用@SpringBootApplication
注解,Spring Boot会自动扫描和配置你的组件。
三.Spring AOP 详解
1.切点(Pointcut)
在面向切面编程(AOP)中,切点(Pointcut)是指你希望在应用程序中插入横切关注点的位置的定义。换句话说,切点确定了哪些方法调用或类会触发切面的逻辑执行。在Spring中,切点用于定义那些需要被AOP增强的方法集合。
切点的定义方式
在Spring中,切点通常使用切点表达式(Pointcut Expression)来定义,切点表达式指定了切点的匹配规则,可以基于方法的名称、返回类型、参数类型等来选择特定的方法。切点表达式的语法类似于正则表达式,但更专注于描述方法的签名和执行位置。
切点表达式的语法
Spring的切点表达式语法基于AspectJ表达式语言(AspectJ Expression Language)。常见的切点表达式语法包括:
-
execution: 匹配方法执行的连接点,是最常用的切点表达式。
- 示例:
execution(* com.example.service.*.*(..))
匹配com.example.service
包下所有类的所有方法。
- 示例:
-
within: 匹配指定类型内的所有方法执行。
- 示例:
within(com.example.service.*)
匹配com.example.service
包及其子包内所有类的所有方法。
- 示例:
-
args: 匹配指定参数类型的方法执行。
- 示例:
args(java.lang.String)
匹配方法有一个String类型参数的执行。
- 示例:
-
@annotation
: 匹配使用了特定注解的方法执行。- 示例:
@annotation(org.springframework.transaction.annotation.Transactional)
匹配所有使用了@Transactional
注解的方法。
- 示例:
-
bean
: 匹配指定bean名称或类型的方法执行。- 示例:
bean(userService)
匹配名称为userService
的bean的所有方法。
- 示例:
在上面AOP的实现例子中,切点指的就是@Around注解后面的部分
("execution(* com.example.demo.aop.Cal.*(..))")
意思为:这个切点表达式匹配了com.example.demo.aop
包中名为Cal
的所有类的所有方法
切点表达式中最常用的两种表达方式为:
execution
: 匹配方法执行的连接点@annotation
: 匹配使用了特定注解的方法执行
我们将详细介绍这两种方式的表达方式
1) execution表达式
作为匹配方法的表达式,语法为:
execution(<访问修饰符> <返回类型> <包名.类名.⽅法(⽅法参数)> <异常>)execution(* com.example.demo.aop.Cal.*(..))//其中:访问修饰符和异常可以省略
切点表达式支持通配符表达:
1. *
(星号)
- 作用:
*
通配符用来匹配任意数量、任意字符的部分。 - 位置: 可以用于方法修饰符、返回类型、类名、方法名以及参数类型的任意部分。
- 示例:
execution(* com.example.service.*.*(..)):匹配 com.example.service 包中任意类的任意方法。execution(public * *(..)):匹配任意公有方法。execution(* get*()):匹配以 get 开头且没有参数的方法。
2. ..
(两个点)
- 作用:
..
通配符用来匹配任意数量的子包或者方法参数。 - 位置: 主要用于包路径和方法参数的部分。
- 示例:
execution(* com.example..*.*(..)):匹配 com.example 包及其子包中任意类的任意方法。execution(* save*(..)):匹配方法名以 save 开头的方法,可以有任意数量的参数。
3. +
(加号)
- 作用:
+
通配符用于匹配指定类及其子类中的方法。 - 位置: 只能用于类名部分,不能用于其他位置。
- 示例:
execution(* com.example.service.SomeService+.*(..)):匹配 SomeService 类及其子类中的任意方法。
组合使用示例
execution(public * com.example..*Service.*(..)):匹配 com.example 包及其子包中所有类名以 Service 结尾的公共方法。execution(* delete*(Long, ..)):匹配方法名以 delete 开头,第一个参数为 Long 类型,第二个及其后的参数任意类型的方法。
2)@annotation
@annotation适合匹配多个无规则的方法,它的实现流程是:
- 编写自定义注解
- 使用 @annotation 表达式来描述切点
- 在连接点的方法上添加自定义注解
示例如下:
1.定义自己的@annotation类:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {}//@Target 标识了Annotation 所修饰的对象范围,即该注解可以⽤在什么地⽅//@Retention 指Annotation被保留的时间⻓短,标明注解的⽣命周期
2.在切面类中使用 @annotation 切点表达式定义切点
@Component
@Aspect
public class TimeAspect {//com.example.demo.aop.MyAnnotation表示对aop包的所有添加了@MyAnnotation的方法生效@Around("@annotation(com.example.demo.aop.MyAnnotation)")public Object recordTime2(ProceedingJoinPoint pjp) throws Throwable {//记录开始时间long begin=System.currentTimeMillis();//执行目标类的方法Object result=pjp.proceed();//记录结束时间long end=System.currentTimeMillis();//打印结果System.out.println(pjp.getSignature().getName()+"方法的执行时间为:"+(end-begin));//返回原方法的结果return result;}}
3.在要生效的方法中添加自己定义的注解
@Component
public class Cal {//加法运算@MyAnnotationpublic int add(int num1,int num2){return num1+num2;}//减法运算public int sub(int num1,int num2){return num1-num2;}//乘法运算public int mul(int num1,int num2){return num1*num2;}//除法运算public int div(int num1,int num2){return num1/num2;}}
4.在测试类中测试方法
@SpringBootApplication
public class Test {public static void main(String[] args) {//获取IOC容器ApplicationContext context=new AnnotationConfigApplicationContext("com.example.demo.aop");//获取容器里的代理类Cal cal=context.getBean(Cal.class);//运行代理类中的方法System.out.println("结果为"+cal.add(5, 4));}
}//结果为:add方法的执行时间为:19
结果为9Process finished with exit code 0
2.连接点(Join Point)
和切点类似,满足切点表达式规则的方法, 就是连接点. 也就是可以被AOP控制的方法
简单来说,切点就是定义哪些方法符合要求,而连接点就是满足要求的具体方法
3.通知(Advice)
通知(Advice)是面向切面编程(AOP)中的一个重要概念,它表示在特定的连接点(Join Point)上执行的代码。通知定义了切面的具体行为,例如在方法调用之前执行日志记录、在方法执行之后处理事务提交等。通知与连接点结合使用,可以理解为连接点上执行的具体操作。
特点和分类
-
与连接点结合:通知与连接点相关联,当特定的连接点被AOP框架拦截时,通知定义的逻辑就会被执行。
-
多种类型:
- 前置通知(Before Advice):在连接点之前执行,例如验证输入参数。
- 后置通知(After Returning Advice):在连接点正常执行完毕后执行,例如记录方法返回值。
- 异常通知(After Throwing Advice):在方法抛出异常后执行,例如处理异常情况。
- 最终通知(After Finally Advice):无论连接点如何结束,都执行,例如释放资源。
- 环绕通知(Around Advice):包围连接点的整个执行过程,可以在方法调用前后添加自定义行为,是最强大也是最灵活的一种通知类型。
在上面实现的例子中,通知对应的就是注解@Around,它表示可以对方法执行的前后都可以增强方法
与它同类型的注解举例如下:
@Aspect
@Component
public class LoggingAspect {//@Before: 前置通知, 此注解标注的通知⽅法在⽬标⽅法前被执⾏@Before("execution(* com.example.service.*.*(..))")public void logBefore(JoinPoint joinPoint) {String methodName = joinPoint.getSignature().getName();System.out.println("Before executing method: " + methodName);}//@AfterReturning: 返回后通知, 此注解标注的通知⽅法在⽬标⽅法后被执⾏, 有异常不会执⾏@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")public void logAfterReturning(JoinPoint joinPoint, Object result) {String methodName = joinPoint.getSignature().getName();System.out.println("After returning from method: " + methodName + ", result: " + result);}//@AfterThrowing: 异常后通知, 此注解标注的通知⽅法发⽣异常后执⾏@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")public void logAfterThrowing(JoinPoint joinPoint, Exception ex) {String methodName = joinPoint.getSignature().getName();System.out.println("Exception thrown by method: " + methodName + ", exception: " + ex);}//@After: 后置通知, 此注解标注的通知⽅法在⽬标⽅法后被执⾏, ⽆论是否有异常都会执⾏@After("execution(* com.example.service.*.*(..))")public void logAfter(JoinPoint joinPoint) {String methodName = joinPoint.getSignature().getName();System.out.println("After executing method: " + methodName);}//@Around: 环绕通知, 此注解标注的通知⽅法在⽬标⽅法前, 后都被执⾏@Around("execution(* com.example.service.*.*(..))")public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {String methodName = joinPoint.getSignature().getName();System.out.println("Before around method: " + methodName);Object result = joinPoint.proceed(); // 执行方法System.out.println("After around method: " + methodName + ", result: " + result);return result;}
}
@Before
、@AfterReturning
、@AfterThrowing
、@After
、@Around
注解分别定义了不同类型的通知,分别对应于前置、后置、异常、最终和环绕通知。
4.切面(Aspect)
切面(Aspect) = 切点(Pointcut) + 通知(Advice)
通过切面就能够描述当前AOP程序需要针对于哪些方法, 在什么时候执行什么样的操作.
四.Spring AOP 的辅助注解
1. @PointCut注解
@Pointcut
是 Spring AOP 中用来定义切点的注解。切点定义了在应用程序中哪些方法需要被织入(应用)切面逻辑。使用 @Pointcut
注解可以将一组方法定义为一个切点,以便在后续的通知(Advice)中引用。
示例如下:
@Pointcut("execution(* com.example.service.*.*(..))")
private void serviceMethods() {}这里的 @Pointcut 注解标注了一个私有方法 serviceMethods(),
它定义了一个切点,用来匹配 com.example.service 包中所有类的所有方法。
切点表达式 "execution(* com.example.service.*.*(..))" 描述了方法的执行
带入我们上面的计算器例子中:
@Component
@Aspect
public class TimeAspect {//定义切点@Pointcut("@annotation(com.example.demo.aop.MyAnnotation)")public void pt(){};//使用切点@Around("pt()")public Object recordTime2(ProceedingJoinPoint pjp) throws Throwable {//记录开始时间long begin=System.currentTimeMillis();//执行目标类的方法Object result=pjp.proceed();//记录结束时间long end=System.currentTimeMillis();//打印结果System.out.println(pjp.getSignature().getName()+"方法的执行时间为:"+(end-begin));//返回原方法的结果return result;}
}
2. @Order注解
在 Spring AOP 中,@Order
注解用于控制切面的优先级顺序。当一个方法被多个切面所通知时,通过 @Order
注解可以指定这些切面的执行顺序,从而控制它们的优先级。
示例:
@Aspect
@Order(1)
public class MyFirstAspect {// Aspect code
}@Aspect
@Order(2)
public class MySecondAspect {// Aspect code
}
@Order注解标识的切面类,执行顺序如下:
• @Before 通知:数字越小先执行
• @After 通知:数字越大先执行
五.Spring AOP 原理
Spring AOP是基于动态代理来实现AOP的
关于动态代理,可以去看看另一篇博客:
常见的设计模式-CSDN博客
Spring AOP 主要使用两种方式来实现动态代理:
1. 基于 JDK 动态代理
- 基于接口:JDK 动态代理要求目标对象必须实现至少一个接口。Spring 使用
java.lang.reflect.Proxy
类来创建基于接口的代理对象。 - 工作原理:当应用程序通过接口调用方法时,代理对象会拦截方法调用并执行额外的横切逻辑,然后将控制转交给实际的目标对象。
2. 基于 CGLIB 动态代理
- 不依赖接口:CGLIB(Code Generation Library)动态代理可以对没有实现接口的类进行代理。它通过继承目标类并重写其中的方法来实现代理功能。
- 工作原理:CGLIB 在运行时生成目标类的子类,并拦截对父类方法的调用,执行额外的横切逻辑。
关于Spring AOP 的讲解到这里就结束了~