1.什么是AOP?
AOP(Aspect Orient Programming),直译过来就是面向切面编程。AOP是一种编程思想,是面向对象编程(OOP)的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。
比如,在《Spring实战(第4版)》中有如下一张图描述了AOP的大体模型。
可以看出:所谓切面,其实就相当于应用对象间的横切点,我们可以将其单独抽象为单独的模块
总结:AOP是指在程序的运行期间动态地将某段代码切入到指定方法、指定位置进行运行的编程方式。AOP的底层是使用动态代理实现的。
2.实战案例
2.1.导入AOP依赖
在项目的pom.xml文件中引入AOP的依赖,如下所示:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>4.3.12.RELEASE</version>
</dependency>
Spring AOP对面向切面编程做了一些简化操作,只需要加上几个核心注解,AOP就能工作起来
2.2.定义目标类
在com.tianxia.springannotation.aop包下创建一个业务逻辑类,例如MathCalculator,用于处理数学计算上的一些逻辑。比如,我们在MathCalculator类中定义了一个除法操作,返回两个整数类型值相除之后的结果,如下所示
package com.tianxia.springannotation.aop;
/**
* 业务逻辑类
* @author liqb
* @date 2023-05-09 16:30
**/
public class MathCalculator {
/**
* 除法
* @author liqb
* @date 2023-05-09 16:31
* @param i 除数
* @param j 被除数
* @return
*/
public int div(int i, int j) {
System.out.println("MathCalculator...div...");
return i / j;
}
}
需求:希望在业务逻辑运行的时候将日志进行打印,而且是在方法运行之前、方法运行结束、方法出现异常等等位置,都希望会有日志打印出来。
2.3.定义切面类
在com.tianxia.springannotation.aop包下创建一个切面类,例如LogAspects,在该切面类中定义几个打印日志的方法,以这些方法来动态地感知MathCalculator类中的div()方法的运行情况。如果需要切面类来动态地感知目标类方法的运行情况,那么就需要使用Spring AOP中的一系列通知方法了。
AOP中的通知方法及其对应的注解与含义如下:
- 前置通知(对应的注解是@Before):在目标方法运行之前运行
- 后置通知(对应的注解是@After):在目标方法运行结束之后运行,无论目标方法是正常结束还是异常结束都会执行
- 返回通知(对应的注解是@AfterReturning):在目标方法正常返回之后运行
- 异常通知(对应的注解是@AfterThrowing):在目标方法运行出现异常之后运行
- 环绕通知(对应的注解是@Around):动态代理,我们可以直接手动推进目标方法运行(joinPoint.procced())
一开始的写法:
package com.tianxia.springannotation.aop;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Before;
/**
* 日志切面
* @author liqb
* @date 2023-05-09 16:33
**/
public class LogAspects {
// @Before:在目标方法(即div方法)运行之前切入
@Before("public int com.tianxia.springannotation.aop.MathCalculator.*(..)")
public void logStart() {
System.out.println("除法运行......@Before,参数列表是:{}");
}
// 在目标方法(即div方法)结束时被调用
@After("public int com.tianxia.springannotation.aop.MathCalculator.*(..)")
public void logEnd() {
System.out.println("除法结束......@After");
}
// 在目标方法(即div方法)正常返回了,有返回值,被调用
@AfterReturning("public int com.tianxia.springannotation.aop.MathCalculator.*(..)")
public void logReturn() {
System.out.println("除法正常返回......@AfterReturning,运行结果是:{}");
}
// 在目标方法(即div方法)出现异常,被调用
@AfterThrowing("public int com.tianxia.springannotation.aop.MathCalculator.*(..)")
public void logException() {
System.out.println("除法出现异常......异常信息:{}");
}
}
对MathCalculator类中的div()方法进行切入,因此在每一个通知方法上都写了com.tianxia.springannotation.aop.MathCalculator.*(…)"这样一串玩意,其实它就是切入点表达式,即指定在哪个方法切入。
如果切入点表达式都一样的情况下,可以抽取出一个公共的切入点表达式,就像下面这样:
/**
* 日志切面
* @author liqb
* @date 2023-05-09 16:33
**/
public class LogAspects {
// 如果切入点表达式都一样的情况下,那么可以抽取出一个公共的切入点表达式
@Pointcut("execution(public int com.tianxia.springannotation.aop.MathCalculator.*(..))")
public void pointCut() {
}
}
pointCut()方法就是抽取出来的一个公共的切入点表达式,其实该方法的方法名随便写啥都行,但是方法体中啥都别写
如何在每一个通知方法上引用这个公共的切入点表达式呢?
得分两种情况来讨论,第一种情况,如果是本类引用,那么可以像下面这样写:
/**
* 日志切面
* @author liqb
* @date 2023-05-09 16:33
**/
public class LogAspects {
// 如果切入点表达式都一样的情况下,那么可以抽取出一个公共的切入点表达式
@Pointcut("execution(public int com.tianxia.springannotation.aop.MathCalculator.*(..))")
public void pointCut() {
}
// @Before:在目标方法(即div方法)运行之前切入
@Before("pointCut()")
public void logStart() {
System.out.println("除法运行......@Before,参数列表是:{}");
}
}
第二种情况,如果是外部类(即其他的切面类)引用,那么就得在通知注解中写方法的全名了,例如:
package com.tianxia.springannotation.aop;
import org.aspectj.lang.annotation.*;
/**
* 日志切面
* @author liqb
* @date 2023-05-09 16:33
**/
public class LogAspects {
// 如果切入点表达式都一样的情况下,那么可以抽取出一个公共的切入点表达式
@Pointcut("execution(public int com.tianxia.springannotation.aop.MathCalculator.*(..))")
public void pointCut() {
}
// @Before:在目标方法(即div方法)运行之前切入
@Before("com.tianxia.springannotation.aop.LogAspects.pointCut()")
public void logStart() {
System.out.println("除法运行......@Before,参数列表是:{}");
}
// 在目标方法(即div方法)结束时被调用
@After("com.tianxia.springannotation.aop.LogAspects.pointCut()")
public void logEnd() {
System.out.println("除法结束......@After");
}
// 在目标方法(即div方法)正常返回了,有返回值,被调用
@AfterReturning("com.tianxia.springannotation.aop.LogAspects.pointCut()")
public void logReturn() {
System.out.println("除法正常返回......@AfterReturning,运行结果是:{}");
}
// 在目标方法(即div方法)出现异常,被调用
@AfterThrowing("com.tianxia.springannotation.aop.LogAspects.pointCut()")
public void logException() {
System.out.println("除法出现异常......异常信息:{}");
}
}
必须告诉Spring哪个类是切面类,只需要给切面类上加上一个@Aspect注解即可
/**
* 日志切面
* @author liqb
* @date 2023-05-09 16:33
**/
@Aspect
public class LogAspects {
}
2.4.将目标类和切面类加入到IOC容器
在com.tianxia.springannotation.config包中,新建配置类MainConfigOfAOP,并使用@Configuration注解标注这是一个Spring的配置类,同时使用@EnableAspectJAutoProxy注解开启基于注解的AOP模式。在MainConfigOfAOP配置类中,使用@Bean注解将业务逻辑类(目标方法所在类)和切面类都加入到IOC容器中,如下所示:
package com.tianxia.springannotation.config;
import com.tianxia.springannotation.aop.LogAspects;
import com.tianxia.springannotation.aop.MathCalculator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* AOP配置类
* @author liqb
* @date 2023-05-09 16:57
**/
@Configuration
@EnableAspectJAutoProxy
public class MainConfigOfAOP {
/**
* 将业务逻辑类(目标方法所在类)加入到容器中
* @author liqb
* @date 2023-05-09 16:58
* @return
*/
@Bean
public MathCalculator calculator() {
return new MathCalculator();
}
/**
* 将切面类加入到容器中
* @author liqb
* @date 2023-05-09 16:58
* @return
*/
@Bean
public LogAspects logAspects() {
return new LogAspects();
}
}
2.5.测试
运行测试方法方法,输出的结果信息如下所示:
package com.tianxia.springannotation;
import com.tianxia.springannotation.aop.MathCalculator;
import com.tianxia.springannotation.config.MainConfigOfAOP;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* aop测试类
* @author liqb
* @date 2023-05-09 17:03
**/
@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class AopTest {
/**
* 测试类
* @author liqb
* @date 2023-05-09 17:04
*/
@Test
public void test01() {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfigOfAOP.class);
// 我们要使用Spring容器中的组件
MathCalculator mathCalculator = applicationContext.getBean(MathCalculator.class);
mathCalculator.div(1, 1);
// 关闭容器
applicationContext.close();
}
}
并打印出了相关信息,但是并没有打印参数列表和运行结果
要想打印出参数列表和运行结果,就需要对LogAspects切面类中的方法进行优化,优化后的结果如下所示:
package com.tianxia.springannotation.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import java.util.Arrays;
/**
* 日志切面
* @author liqb
* @date 2023-05-09 16:33
**/
@Aspect
public class LogAspects {
// 如果切入点表达式都一样的情况下,那么可以抽取出一个公共的切入点表达式
@Pointcut("execution(public int com.tianxia.springannotation.aop.MathCalculator.*(..))")
public void pointCut() {
}
// @Before:在目标方法(即div方法)运行之前切入
@Before("pointCut()")
public void logStart(JoinPoint joinPoint) {
// System.out.println("除法运行......@Before,参数列表是:{}");
Object[] args = joinPoint.getArgs(); // 拿到参数列表,即目标方法运行需要的参数列表
System.out.println(joinPoint.getSignature().getName() + "运行......@Before,参数列表是:{" + Arrays.asList(args) + "}");
}
// 在目标方法(即div方法)结束时被调用
@After("com.tianxia.springannotation.aop.LogAspects.pointCut()")
public void logEnd(JoinPoint joinPoint) {
// System.out.println("除法结束......@After");
System.out.println(joinPoint.getSignature().getName() + "结束......@After");
}
// 在目标方法(即div方法)正常返回了,有返回值,被调用
@AfterReturning(value = "com.tianxia.springannotation.aop.LogAspects.pointCut()", returning="result") // returning来指定我们这个方法的参数谁来封装返回值
public void logReturn(JoinPoint joinPoint, Object result) { // 一定要注意:JoinPoint这个参数要写,一定不能写到后面,它必须出现在参数列表的第一位,否则Spring也是无法识别的,就会报错
// System.out.println("除法正常返回......@AfterReturning,运行结果是:{}");
System.out.println(joinPoint.getSignature().getName() + "正常返回......@AfterReturning,运行结果是:{" + result + "}");
}
// 在目标方法(即div方法)出现异常,被调用
@AfterThrowing(value = "com.tianxia.springannotation.aop.LogAspects.pointCut()", throwing = "exception")
public void logException(JoinPoint joinPoint, Exception exception) {
System.out.println(joinPoint.getSignature().getName() + "出现异常......异常信息:{" + exception + "}");
}
}
需要注意的是,JoinPoint参数一定要放在参数列表的第一位,否则Spring是无法识别的,那自然就会报错了
运行测试方法,输出结果如下所示:
3.小结
搭建AOP测试环境时,虽然步骤繁多,但是我们只要牢牢记住以下三点,就会无往而不利了
- 将切面类和业务逻辑组件(目标方法所在类)都加入到容器中,并且要告诉Spring哪个类是切面类(标注了@Aspect注解的那个类)
- 在切面类上的每个通知方法上标注通知注解,告诉Spring何时何地运行,当然最主要的是要写好切入点表达式,这个切入点表达式可以参照官方文档来写
- 开启基于注解的AOP模式,即加上@EnableAspectJAutoProxy注解,这是最关键的一点