一文带你搞定 AOP 切面

930 阅读16分钟

​​​​​摘要:AOP 在 spring 中又叫“面向切面编程”,是对传统我们面向对象编程的一个补充,主要操作对象就是“切面”,可以简单的理解它是贯穿于方法之中,在方法执行前、执行时、执行后、返回值后、异常后要执行的操作。

本文分享自华为云社区《一篇文搞懂《AOP面向切面编程》是一种什么样的体验?》,作者: 灰小猿。

一、什么是 Spring 的 AOP?

AOP 在 spring 中又叫“面向切面编程”,它可以说是对传统我们面向对象编程的一个补充,从字面上顾名思义就可以知道,它的主要操作对象就是“切面”,所以我们就可以简单的理解它是贯穿于方法之中,在方法执行前、执行时、执行后、返回值后、异常后要执行的操作。相当于是将我们原本一条线执行的程序在中间切开加入了一些其他操作一样。

在应用 AOP 编程时,仍然需要定义公共功能,但可以明确的定义这个功能应用在哪里,以什么方式应用,并且不必修改受影响的类。这样一来横切关注点就被模块化到特殊的类里——这样的类我们通常就称之为“切面”。

例如下面这个图就是一个 AOP 切面的模型图,是在某一个方法执行前后执行的一些操作,并且这些操作不会影响程序本身的运行。

一文带你搞定 AOP 切面

AOP 切面编程中有一个比较专业的术语,我给大家罗切出来了:

一文带你搞定 AOP 切面

现在大概的了解了 AOP 切面编程的基本概念,接下来就是实际操作了。

二、AOP 框架环境搭建

1、导入 jar 包

目前比较流行且常用的 AOP 框架是 AspectJ,我们在做 SSM 开发时用到的也是 AspectJ,使用该框架技术就需要导入它所支持的 jar 包

  • aopalliance.jar
  • aspectj.weaver.jar
  • spring-aspects.jar

2、引入 AOP 名称空间

使用 AOP 切面编程时是需要在容器中引入 AOP 名称空间的

一文带你搞定 AOP 切面

3、写配置

其实在做 AOP 切面编程时,最常使用也必备的一个标签就是,aop:aspectj-autoproxy</aop:aspectj-autoproxy>

我们在容器中需要添加这个元素,当 Spring IOC 容器侦测到 bean 配置文件中的< aop:aspectj-autoproxy>元素时,会自动为与 AspectJ 切面匹配的 bean 创建代理。

同时在现在的 spring 中使用 AOP 切面有两种方式,分别是 AspectJ 注解或基于 XML 配置的 AOP,下面我依次和大家介绍一下这两种方式的使用。

三、基于 AspectJ 注解的 AOP 开发

在上一篇文章中我也和大家将了关于 spring 中注解开发的强大,所以关于 AOP 开发我们同样也可以使用注解的形式来进行编写,下面我来和大家介绍一下如何使用注解方式书写 AOP。

1、五种通知注解

首先要在 Spring 中声明 AspectJ 切面,只需要在 IOC 容器中将切面声明为 bean 实例。

当在 Spring IOC 容器中初始化 AspectJ 切面之后,Spring IOC 容器就会为那些与 AspectJ 切面相匹配的 bean 创建代理。

在 AspectJ 注解中,切面只是一个带有 @Aspect 注解的 Java 类,它往往要包含很多通知。通知是标注有某种注解的简单的 Java 方法。

AspectJ 支持 5 种类型的通知注解:

1. @Before:前置通知,在方法执行之前执行

2. @After:后置通知,在方法执行之后执行

3. @AfterRunning:返回通知,在方法返回结果之后执行

4. @AfterThrowing:异常通知,在方法抛出异常之后执行

5. @Around:环绕通知,围绕着方法执行

2、切入点表达式规范

这五种通知注解后面还可以跟特定的参数,来指定哪一个切面方法在哪一个方法执行时触发。那么具体操作是怎么样的呢?

这里就需要和大家介绍一个名词:“切入点表达式”,通过在注解中加入该表达式参数,我们就可以通过表达式的方式定位一个或多个具体的连接点

切入点表达式的语法格式规范是:

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

其中在表达式中有两个常用的特殊符号:

星号“ * ”代表所有的意思,星号还可以表示任意的数值类型

“.”号:“…”表示任意类型,或任意路径下的文件

在这里举出几个例子:

表达式:

execution(*com.atguigu.spring.ArithmeticCalculator.*(…))

含义:

ArithmeticCalculator 接口中声明的所有方法。第一个“”代表任意修饰符及任意返回值。第二个“”代表任意方法。“…”匹配任意数量、任意类型的参数。若目标类、接口与该切面类在同一个包中可以省略包名。

表达式:

execution(public * ArithmeticCalculator.*(…))

含义:

ArithmeticCalculator 接口的所有公有方法

表达式:

execution(public doubleArithmeticCalculator.*(…))

含义:

ArithmeticCalculator 接口中返回 double 类型数值的方法

表达式:

execution(public doubleArithmeticCalculator.*(double, …))

含义:

第一个参数为 double 类型的方法。“…”匹配任意数量、任意类型的参数。

表达式:

execution(public double ArithmeticCalculator.*(double,double))

含义:

参数类型为 double,double 类型的方法

这里还有一个定位最模糊的表达式:

execution("**(…)")

表示任意包下任意类的任意方法,但是这个表达式千万别写,哈哈,不然你每一个执行的方法都会有通知方法执行的!

同时,在 AspectJ 中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来。

如:

execution(* .add(int,…)) || execution( *.sub(int,…))

表示任意类中第一个参数为 int 类型的 add 方法或 sub 方法

3、注解实践

现在我们已经知道了注解和切入点表达式的使用,那么接下来就是进行实践了

对于切入点表达式,我们可以直接在注解中使用“”写在其中,还可以在 @AfterReturning 注解和 @AfterThrowing 注解中将切入点赋值给 pointcut 属性,但是在其他的注解中没有 pointcut 这个参数。

一文带你搞定 AOP 切面

将切入点表达式应用到实际的切面类中如下:

@Aspect	//切面注解
@Component	//其他业务层
public class LogUtli {
//	方法执行开始,表示目标方法是com.spring.inpl包下的任意类的任意以两个int为参数,返回int类型参数的方法
	@Before("execution(public int com.spring.inpl.*.*(int, int))")
	public static void LogStart(JoinPoint joinPoint) {
		System.out.println("通知记录开始...");
	}
//	方法正常执行完之后
	/**
	 * 在程序正常执行完之后如果有返回值,我们可以对这个返回值进行接收
	 * returning用来接收方法的返回值
	 * */
	@AfterReturning(pointcut="public int com.spring.inpl.*.*(int, int)",returning="result")
	public static void LogReturn(JoinPoint joinPoint,Object result) {
		System.out.println("【" + joinPoint.getSignature().getName() + "】程序方法执行完毕了...结果是:" + result);
	}
}

以上只是一个最简单的通知方法,但是在实际的使用过程中我们可能会将多个通知方法切入到同一个目标方法上去,比如同一个目标方法上既有前置通知、又有异常通知和后置通知。

但是这样我们也只是在目标方法执行时切入了一些通知方法,那么我们能不能在通知方法中获取到执行的目标方法的一些信息呢?当然是可以的。

4、JoinPoint 获取方法信息

在这里我们就可以使用 JoinPoint 接口来获取到目标方法的信息,如方法的返回值、方法名、参数类型等。

一文带你搞定 AOP 切面

如我们在方法执行开始前,获取到该目标方法的方法名和输入的参数并输出。

//	方法执行开始
	@Before("execution(public int com.spring.inpl.*.*(int, int))")
	public static void LogStart(JoinPoint joinPoint) {
		    Object[] args = joinPoint.getArgs();	//获取到参数信息
		    Signature signature = joinPoint.getSignature(); //获取到方法签名
		    String name = signature.getName();	//获取到方法名
		    System.out.println("【" + name + "】记录开始...执行参数:" + Arrays.asList(args));
	}

5、接收方法的返回值和异常信息

对于有些目标方法在执行完之后可能会有返回值,或者方法中途异常抛出,那么对于这些情况,我们应该如何获取到这些信息呢?

首先我们来获取当方法执行完之后获取返回值

在这里我们可以使用 @AfterReturning 注解,该注解表示的通知方法是在目标方法正常执行完之后执行的。

在返回通知中,只要将 returning 属性添加到 @AfterReturning 注解中,就可以访问连接点的返回值。

该属性的值即为用来传入返回值的参数名称,但是注意必须在通知方法的签名中添加一个同名参数。

在运行时 Spring AOP 会通过这个参数传递返回值,由于我们可能不知道返回值的类型,所以一般将返回值的类型设置为 Object 型。

与此同时,原始的切点表达式需要出现在 pointcut 属性中

如下所示:

//	方法正常执行完之后
	/**
	 * 在程序正常执行完之后如果有返回值,我们可以对这个返回值进行接收
	 * returning用来接收方法的返回值
	 * */
	@AfterReturning(pointcut="public int com.spring.inpl.*.*(int, int)",returning="result")
	public static void LogReturn(JoinPoint joinPoint,Object result) {
		    System.out.println("【" + joinPoint.getSignature().getName() + "】程序方法执行完毕了...结果是:" + result);
	}

对于接收异常信息,方法其实是一样的。

我们需要将 throwing 属性添加到 @AfterThrowing 注解中,也可以访问连接点抛出的异常。Throwable 是所有错误和异常类的顶级父类,所以在异常通知方法可以捕获到任何错误和异常。

如果只对某种特殊的异常类型感兴趣,可以将参数声明为其他异常的参数类型。然后通知就只在抛出这个类型及其子类的异常时才被执行。

实例如下:

//	异常抛出时
	/**
	 * 在执行方法想要抛出异常的时候,可以使用throwing在注解中进行接收,
	 * 其中value指明执行的全方法名
	 * throwing指明返回的错误信息
	 * */
	@AfterThrowing(pointcut="public int com.spring.inpl.*.*(int, int)",throwing="e")
	public static void LogThowing(JoinPoint joinPoint,Object e) {
		System.out.println("【" + joinPoint.getSignature().getName() +"】发现异常信息...,异常信息是:" + e);
	}

6、环绕通知

我们在上面介绍通知注解的时候,大家应该也看到了其实还有一个很重要的通知——环绕通知,环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点。

对于环绕通知来说,连接点的参数类型必须是 ProceedingJoinPoint。它是 JoinPoint 的子接口,允许控制何时执行,是否执行连接点。

在环绕通知中需要明确调用 ProceedingJoinPoint 的 proceed()方法来执行被代理的方法。如果忘记这样做就会导致通知被执行了,但目标方法没有被执行。这就意味着我们需要在方法中传入参数 ProceedingJoinPoint 来接收方法的各种信息。

注意:

环绕通知的方法需要返回目标方法执行之后的结果,即调用 joinPoint.proceed();的返回值,否则会出现空指针异常。

具体使用可以看下面这个实例:

/**
	 * 环绕通知方法
	 * 使用注解@Around()
	 * 需要在方法中传入参数proceedingJoinPoint 来接收方法的各种信息
	 * 使用环绕通知时需要使用proceed方法来执行方法
	 * 同时需要将值进行返回,环绕方法会将需要执行的方法进行放行
	 * *********************************************
	 * @throws Throwable 
	 * */
	@Around("public int com.spring.inpl.*.*(int, int)")
	public Object MyAround(ProceedingJoinPoint pjp) throws Throwable {
		
//		获取到目标方法内部的参数
		Object[] args = pjp.getArgs();
		
		System.out.println("【方法执行前】");
//		获取到目标方法的签名
		Signature signature = pjp.getSignature();
		String name = signature.getName();
		Object proceed = null;
		try {
//			进行方法的执行
			proceed = pjp.proceed();
			System.out.println("方法返回时");
		} catch (Exception e) {
			System.out.println("方法异常时" + e);
		}finally{
			System.out.println("后置方法");
		}
		
		//将方法执行的返回值返回
		return proceed;
	}

7、通知注解的执行顺序

那么现在这五种通知注解的使用方法都已经介绍完了,我们来总结一下这几个通知注解都在同一个目标方法中时的一个执行顺序。

在正常情况下执行:

@Before(前置通知)—>@After(后置通知)---->@AfterReturning(返回通知)

在异常情况下执行:

@Before(前置通知)—>@After(后置通知)---->@AfterThrowing(异常通知)

当普通通知和环绕通知同时执行时:

执行顺序是:

环绕前置----普通前置----环绕返回/异常----环绕后置----普通后置----普通返回/异常

8、重用切入点定义

对于上面的通知注解,我们都是在每一个通知注解上都定义了一遍切入点表达式,但是试想一个问题,如果我们不想给这个方法设置通知方法了,或者我们想要将这些通知方法切入到另一个目标方法,那么我们岂不是要一个一个的更改注解中的切入点表达式吗?这样也太麻烦了吧?

所以 spring 就想到了一个办法,重用切入点表达式

也就是说将这些会重复使用的切入点表达式用一个方法来表示,那么我们的通知注解只需要调用这个使用了该切入点表达式的方法即可实现和之前一样的效果,这样的话,我们即使想要更改切入点表达式的接入方法,也不用一个一个的去通知注解上修改了。

获取可重用的切入点表达式的方法是:

1. 随便定义一个 void 的无实现的方法

2. 为方法添加注解 @Pointcut()

3. 在注解中加入抽取出来的可重用的切入点表达式

4. 使用 value 属性将方法加入到对应的切面函数的注解中

一文带你搞定 AOP 切面

完整实例如下:

@Aspect	//切面注解
@Component	//其他业务层
public class LogUtli {
	/**
	 * 定义切入点表达式的可重用方法
	 * */
	@Pointcut("execution(public int com.spring.inpl.MyMathCalculator.*(int, int))")
	public void MyCanChongYong() {}
	
//	方法执行开始
	@Before("MyCanChongYong()")
	public static void LogStart(JoinPoint joinPoint) {
		Object[] args = joinPoint.getArgs();	//获取到参数信息
		Signature signature = joinPoint.getSignature(); //获取到方法签名
		String name = signature.getName();	//获取到方法名
		System.out.println("【" + name + "】记录开始...执行参数:" + Arrays.asList(args));
	}
//	方法正常执行完之后
	/**
	 * 在程序正常执行完之后如果有返回值,我们可以对这个返回值进行接收
	 * returning用来接收方法的返回值
	 * */
	@AfterReturning(value="MyCanChongYong()",returning="result")
	public static void LogReturn(JoinPoint joinPoint,Object result) {
		System.out.println("【" + joinPoint.getSignature().getName() + "】程序方法执行完毕了...结果是:" + result);
	}
	
//	异常抛出时
	/**
	 * 在执行方法想要抛出异常的时候,可以使用throwing在注解中进行接收,
	 * 其中value指明执行的全方法名
	 * throwing指明返回的错误信息
	 * */
	@AfterThrowing(value="MyCanChongYong()",throwing="e")
	public static void LogThowing(JoinPoint joinPoint,Object e) {
		System.out.println("【" + joinPoint.getSignature().getName() +"】发现异常信息...,异常信息是:" + e);
	}
	
//	结束得出结果
	@After(value = "execution(public int com.spring.inpl.MyMathCalculator.add(int, int))")
	public static void LogEnd(JoinPoint joinPoint) {
		System.out.println("【" + joinPoint.getSignature().getName() +"】执行结束");
	}
	
	/**
	 * 环绕通知方法
	 * @throws Throwable 
	 * */
	@Around("MyCanChongYong()")
	public Object MyAround(ProceedingJoinPoint pjp) throws Throwable {
		
//		获取到目标方法内部的参数
		Object[] args = pjp.getArgs();
		
		System.out.println("【方法执行前】");
//		获取到目标方法的签名
		Signature signature = pjp.getSignature();
		String name = signature.getName();
		Object proceed = null;
		try {
//			进行方法的执行
			proceed = pjp.proceed();
			System.out.println("方法返回时");
		} catch (Exception e) {
			System.out.println("方法异常时" + e);
		}finally{
			System.out.println("后置方法");
		}
		
		//将方法执行的返回值返回
		return proceed;
	}
}

以上就是使用 AspectJ 注解实现 AOP 切面的全部过程了,在这里还有一点特别有意思的规定提醒大家,就是当你有多个切面类时,切面类的执行顺序是按照类名的首字符先后来执行的(不区分大小写)。

接下来我来和大家讲解一下实现 AOP 切面编程的另一种方法——基于 XML 配置的 AOP 实现

四、基于 XML 配置的 AOP 实现

基于 XML 配置的 AOP 切面顾名思义就是摒弃了注解的使用,转而在 IOC 容器中配置切面类,这种声明是基于 aop 名称空间中的 XML 元素来完成的

在 bean 配置文件中,所有的 Spring AOP 配置都必须定义在< aop:config>元素内部。对于每个切面而言,都要创建一个 aop:aspect 元素来为具体的切面实现引用后端 bean 实例。

切面 bean 必须有一个标识符,供< aop:aspect>元素引用。

所以我们在 bean 的配置文件中首先应该先将所需切面类加入到 IOC 容器中去,之后在 aop 的元素标签中进行配置。我们在使用注解进行开发的时候,五种通知注解以及切入点表达式这些在 xml 文件中同样是可以配置出来的。

1、声明切入点

切入点使用

< aop:pointcut>元素声明。

切入点必须定义在< aop:aspect>元素下,或者直接定义在< aop:config>元素下。

定义在< aop:aspect>元素下:只对当前切面有效

定义在< aop:config>元素下:对所有切面都有效

基于 XML 的 AOP 配置不允许在切入点表达式中用名称引用其他切入点。

2、声明通知

在 aop 名称空间中,每种通知类型都对应一个特定的 XML 元素。

通知元素需要使用来引用切入点,或用< pointcut>直接嵌入切入点表达式。

method 属性指定切面类中通知方法的名称

具体使用可以看下面这里实例:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">


	<!-- 通过配置文件实现切面 
		1、将目标类和切面类加入到容器中 @component
		2、声明哪个类是切面类,@Aspect
		3、在配置文件中配置五个通知方法,告诉切面类中的方法都何时运行
		4、开启基于注解的AOP功能
	-->
	
	<!-- 将所需类加入到容器中 -->
	<bean id="myCalculator" class="com.spring.inpl.MyMathCalculator"></bean>
	<bean id="logUtil" class="com.spring.utils.LogUtli"></bean>
	<bean id="SecondUtli" class="com.spring.utils.SecondUtli"></bean>
	
	<!-- 进行基于AOP的配置 -->
	<!-- 当有两个切面类和一个环绕方法时,方法的执行是按照配置文件中配置的先后顺序执行的
		配置在前的就会先执行,配置在后的就会后执行,但同时环绕方法进入之后就会先执行环绕方法
	 -->
	<aop:config>
		<!-- 配置一个通用类 -->
		<aop:pointcut expression="execution(public int com.spring.inpl.MyMathCalculator.*(int, int)))" id="myPoint"/>
		<!-- 配置某一个指定的切面类 -->
		<aop:aspect id="logUtil_Aspect" ref="logUtil">
			<!-- 为具体的方法进行指定
			method指定具体的方法名
			pointcut指定具体要对应的方法
			 -->
			<aop:before method="LogStart" pointcut="execution(public int com.spring.inpl.MyMathCalculator.add(int, int))"/>
			<aop:after-throwing method="LogThowing" pointcut="execution(public int com.spring.inpl.MyMathCalculator.*(int, int)))" throwing="e"/>
			<aop:after-returning method="LogReturn" pointcut-ref="myPoint" returning="result"/>
			<aop:after method="LogEnd" pointcut-ref="myPoint"/>
			<!-- 定义一个环绕方法 -->
			<aop:around method="MyAround" pointcut-ref="myPoint"/>
		</aop:aspect>
		
		<!-- 定义第二个切面类 -->
		<aop:aspect ref="SecondUtli">
			<aop:before method="LogStart" pointcut="execution(public int com.spring.inpl.MyMathCalculator.*(..))"/>
			<aop:after-throwing method="LogThowing" pointcut-ref="myPoint" throwing="e"/>
			<aop:after method="LogEnd" pointcut-ref="myPoint"/>
		</aop:aspect>
		
	</aop:config>
</beans>

总结一下通过 XML 配置实现 AOP 切面编程的过程:

通过配置文件实现切面

1. 将目标类和切面类加入到容器中 相当于注解 @component

2. 声明哪个类是切面类,相当于注解 @Aspect

3. 在配置文件中配置五个通知方法,告诉切面类中的方法都何时运行

4. 开启基于注解的 AOP 功能

这里有一点还需要注意:

当有两个切面类和一个环绕方法时,方法的执行是按照配置文件中配置的先后顺序执行的,配置在前的就会先执行,配置在后的就会后执行,但同时环绕方法进入之后就会先执行环绕方法。

最后总结

至此通过 AspectJ 注解和 XML 配置两种方式来实现 AOP 切面编程的过程就和大家分享完了,总体来说基于注解的声明要优先于基于 XML 的声明。通过 AspectJ 注解,切面可以与 AspectJ 兼容,而基于 XML 的配置则是 Spring 专有的。由于 AspectJ 得到越来越多的 AOP 框架支持,所以以注解风格编写的切面将会有更多重用的机会。

点击关注,第一时间了解华为云新鲜技术~