Spring——AOP

什么是AOP?

基本概念

  1. AOP(Aspect-Oriented Programming)是一种编程范式,旨在提高软件的模块化和可维护性。AOP 是在 OOP(Object-Oriented Programming,面向对象编程)之上的一种补充,它主要关注在软件开发中的横切关注点(cross-cutting concerns),例如日志记录、安全性、事务管理等,这些关注点可能存在于应用程序的不同部分之中,而不仅仅局限于单个类或对象。
  2. AOP 的核心思想是将这些横切关注点从应用程序的核心业务逻辑中分离出来,以提高代码的模块化和可维护性。它通过定义切面(Aspect)来实现这一目标。切面是横切关注点的模块化单元,其中包含了特定的横切逻辑。
  3. 在 AOP 中,切面可以通过在应用程序的不同点上进行织入(Weaving)来将其与核心业务逻辑连接起来,从而实现横切关注点的处理。织入可以在编译时、加载时或运行时进行,这取决于 AOP 框架的实现方式。
  4. AOP 的一个主要优点是它能够降低代码的重复性,因为横切关注点的处理逻辑可以集中在切面中,而不需要在应用程序的多个地方重复编写。这样可以提高代码的可维护性和可读性,并且使得修改和扩展横切关注点的逻辑变得更加容易。
  5. 常见的 AOP 框架包括 Java 中的 Spring AOP、AspectJ 等,它们提供了丰富的功能来支持切面的定义和织入。

AOP实现方式

  1. 基于动态代理方式(内置AOP实现)
  2. 使用框架 AspectJ 实现

基本说明

通知方法

  1. 前置通知:@Before

    在目标方法执行之前执行的通知方法。前置通知通常用于执行一些预处理操作,例如权限检查、参数验证等。

  2. 返回通知:@AfterReturning

    在目标方法执行成功后(没有抛出异常)执行的通知方法。后置通知通常用于执行一些清理操作,例如资源释放、日志记录等。

  3. 异常通知:@AfterThrowing

    在目标方法抛出异常后执行的通知方法。异常通知通常用于处理异常情况,例如捕获异常并进行日志记录、异常处理等。

  4. 后置通知:@After

    无论目标方法是否正常执行完成(包括抛出异常或正常返回),最终通知都会执行。最终通知通常用于执行一些必须在目标方法执行之后执行的清理操作,例如资源释放、日志记录等。

  5. 环绕通知:@Around

    环绕通知包围目标方法的执行。它提供了对目标方法的完全控制,可以在目标方法执行前后自定义处理逻辑。通常,环绕通知会在通知方法内部调用目标方法的执行,但也可以选择不调用目标方法,从而完全改变目标方法的行为。

案例演示

【注意】:

  1. 依旧采用动态代理时用过的例子
  2. 使用AOP编程时,需要引入相关需要的jar

Calculate接口

1
2
3
4
5
6
public interface Calculate {

public float getSum(float num1,float num2);

public float getSub(float num1,float num2);
}

Phone类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public class Phone implements Calculate {
@Override
public float getSum(float num1, float num2) {
float result = num1 + num2;
System.out.println("方法内部打印:result=" + result);
return result;
}

@Override
public float getSub(float num1, float num2) {
float result = num1-num2;
System.out.println("方法内部打印:result=" + result);
return result;
}
}

CalculateAspect类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Aspect
@Component
public class CalculateAspect {

@Before(value = "execution(public float com.mhl.spring.aop.proxy3.Phone.getSum(float, float))")
public void showBeginLog(JoinPoint joinPoint){
System.out.println("前置通知");
Signature signature = joinPoint.getSignature();
System.out.println("-日志-方法名-" + signature.getName() + "-参数" + Arrays.asList(joinPoint.getArgs()));
}

@AfterReturning(value = "execution(public float com.mhl.spring.aop.proxy3.Phone.getSum(float,float))")
public void showSuccessEndLog(JoinPoint joinPoint){
System.out.println("返回通知");
Signature signature = joinPoint.getSignature();
System.out.println("-日志-方法名-" + signature.getName() + "-方法正常结束");
}

@AfterThrowing(value = "execution(public float com.mhl.spring.aop.proxy3.Phone.getSum(float,float))")
public void showExceptionLog(JoinPoint joinPoint){
System.out.println("异常通知");
}

@After(value = "execution(public float com.mhl.spring.aop.proxy3.Phone.getSum(float,float))")
public void showFinallyLog(JoinPoint joinPoint){
System.out.println("最终通知");
}
}

AopAspectjTest类

1
2
3
4
5
6
7
8
9
public class AopAspectjTest {
@Test
public void CalculateTest(){
ApplicationContext ioc = new ClassPathXmlApplicationContext("bean03.xml");
Calculate bean = ioc.getBean(Calculate.class);
float sum = bean.getSum(10,2);
System.out.println("sum=" + sum);
}
}

bean03.xml文件

1
2
3
<context:component-scan base-package="com.mhl.spring.aop.proxy3"/>
<!--开启基于注解的AOP功能 -->
<aop:aspectj-autoproxy/>

至此,就演示完了一个基础的AOP编程的过程。

下面需要对其中的细节部分进行详细探讨。

切入点表达式

切入点表达式即为需要为哪个包下的哪个类的哪个方法进行切面编程

@Before(value = "execution(public float com.mhl.spring.aop.proxy3.Phone.getSum(float, float))")

切入点表达式语法格式:

execution ([权限修饰符] [返回值类型] [简单类名/全类名] [方法名]([参数列表]))

以下是常见的模糊匹配:

  1. *(星号):匹配任意数量(包括零个)字符。
    • execution(* com.example.service.*.*(..)):匹配com.example.service包中的任何类的任何方法。
  2. ..(两个连续点号):匹配任意数量的参数。
    • execution(* com.example.service.UserService.*(..)):匹配UserService类中的任何方法,不论参数数量。
  3. +(加号):匹配指定类及其子类的方法。
    • execution(* com.example.service.UserService+.*(..)):匹配UserService类及其子类的任何方法。
  4. ..*组合:可以组合使用连续点号和星号,以匹配多级包或类名。
    • execution(* com.example..*.*(..)):匹配com.example包及其子包中的任何类的任何方法。
  5. 参数类型匹配:可以根据方法参数的类型进行匹配。
    • execution(* com.example.service.UserService.*(..)) && args(String):匹配UserService类中参数为String类型的任何方法。

【注意】

  1. 在同一个包下可以省略包名。
  2. 切入表达式也可以指向类的方法,这时切入表达式会对该类/对象生效。
  3. 切入表达式也可以指向接口的方法,这时切入表达式会对实现了该接口的类生效。
  4. 切入表达式也可以对没有实现接口的类进行切入。
  5. 基于接口使用的是JDK的Proxy,基于没有实现接口的类使用的是Spring的CGLib

JoinPoint

通过JoinPoint 可以获得调用方法的签名

常用方法:

1
2
3
4
5
6
7
8
9
public  void beforeMethod(JoinPoint joinPoint){
joinPoint.getSignature().getName();//获取目标方法名
joinPoint.getSignature().getDeclaringType().getSimpleName();//获取目标方法所属类的简单类名
joinPoint.getSignature().getDeclaringTypeName();//获取目标方法所属类的类名
joinPoint.getSignature().getModifiers();//获取目标方法权限修饰符
Object[] args = joinPoint.getArgs();//获取目标方法的参数,返回一个数组
joinPoint.getTarget();//获取被代理的对象
joinPoint.getThis();//获取代理对象自己
}

返回通知获取返回结果

在返回通知方法获取返回的结果

即在value后加入returning,在方法中传入Object

1
2
3
4
5
6
@AfterReturning(value = "execution(public float  com.mhl.spring.aop.proxy3.Phone.getSum(float,float))",returning = "res")
public void showSuccessEndLog(JoinPoint joinPoint,Object res){
System.out.println("返回通知");
Signature signature = joinPoint.getSignature();
System.out.println("-日志-方法名-" + signature.getName() + "-方法正常结束,结果为" + res);
}

异常通知获取异常

在异常通知中获取异常信息

即在value后加入throwing,在方法中传入Throwable

1
2
3
4
5
@AfterThrowing(value = "execution(public float  com.mhl.spring.aop.proxy3.Phone.getSum(float,float))",throwing = "throwable")
public void showExceptionLog(JoinPoint joinPoint, Throwable throwable){
System.out.println("异常通知"+ throwable);
}

环绕通知

环绕通知就是通过try-catch-finally方式,完成以上四种通知的功能。(了解即可)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;

@Aspect //表示这个类是一个切面类
@Component //需要加入到 IOC 容器
public class SmartAnimalAspect {
//=====环绕通知 start=====
@Around(value="execution(public float
com.hspedu.spring.aop.joinpoint.SmartDog.getSum(float, float))")
public Object doAround(ProceedingJoinPoint joinPoint) {
Object result = null;
String methodName = joinPoint.getSignature().getName();
try {
//1.相当于前置通知完成的事情
Object[] args = joinPoint.getArgs();
List<Object> argList = Arrays.asList(args);
System.out.println("AOP 环绕通知--" + methodName + "方法开始了--参数有:
" + argList);
//在环绕通知中一定要调用 joinPoint.proceed()来执行目标方法
result = joinPoint.proceed();
//2.相当于返回通知完成的事情
System.out.println("AOP 环绕通知" + methodName + "方法结束了--结果是:"+ result);
} catch (Throwable throwable) {
//3.相当于异常通知完成的事情
System.out.println("AOP 环绕通知" + methodName + "方法抛异常了--异常对象:" + throwable);
} finally {
//4.相当于最终通知完成的事情
System.out.println("AOP 后置通知" + methodName + "方法最终结束了...");
}
return result;
} //=====环绕通知

切入点表达式重用

切入点表达式是可以重用的,对于需要使用同一表达式可以更简化

1
2
3
4
5
6
 @Pointcut(value = "execution(public float  com.mhl.spring.aop.proxy3.Phone.getSum(float, float))" )
public void myPointCut(){}

// @Before(value = "execution(public float com.mhl.spring.aop.proxy3.Phone.getSum(float, float))")
@Before(value = "myPointCut()")
@AfterReturning(value = "myPointCut()",returning = "res")

切面优先级

如果有多个切面在同一个切入点切入,那就要考虑切面优先级

@order(value = n) n越小,优先级越大

前置通知按优先级执行,其余通知如下图所示依次执行(可以理解为括号匹配 {【()】})

以上便是Spring——AOP的基本内容了。