AOP
1、环境搭建
AOP是指在程序的运行期间动态地将某段代码切入到指定方法、指定位置进行运行的编程方式。AOP的底层是使用动态代理实现的。
在讲解AOP原理之前,我们先来搭建一个模拟AOP的开发环境
1.1、导入AOP支持的依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>4.3.12.RELEASE</version>
</dependency>
1.2、创建一个业务逻辑类(目标类)
public class MathCalculator {
public int div(int i, int j) {
System.out.println("MathCalculator...div...正在进行除法运算....");
return i / j;
}
}
现在,我们希望在以上这个业务逻辑类中的除法运算之前,记录一下日志,例如记录一下哪个方法运行了,用的参数是什么,运行结束之后它的返回值又是什么,顺便可以将其打印出来,还有如果运行出异常了,那么就捕获一下异常信息。
或者,你会有这样一个需求,即希望在业务逻辑运行的时候将日志进行打印,而且是在方法运行之前、方法运行结束、方法出现异常等等位置,都希望会有日志打印出来。
1.3、创建一个业务切面类
AOP中的通知方法及其对应的注解与含义如下:
前置通知(对应的注解是@Before):在目标方法运行之前运行
后置通知(对应的注解是@After):在目标方法运行结束之后运行,无论目标方法是正常结束还是异常结束都会执行
返回通知(对应的注解是@AfterReturning):在目标方法正常返回之后运行
异常通知(对应的注解是@AfterThrowing):在目标方法运行出现异常之后运行
环绕通知(对应的注解是@Around):动态代理,我们可以直接手动推进目标方法运行(joinPoint.procced())
切面类代码
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Before;
/**
* 切面类
*
*/
public class LogAspects {
// @Before:在目标方法(即div方法)运行之前切入,public int com.meimeixia.aop.MathCalculator.div(int, int)这一串就是切入点表达式,指定在哪个方法切入
@Before("public int lsp.mdy.aop.MathCalculator.*(..)")
public void logStart() {
System.out.println("除法运行之前......@Before,参数列表是:{}");
}
// 在目标方法(即div方法)结束时被调用
@After("public int lsp.mdy.aop.MathCalculator.*(..)")
public void logEnd() {
System.out.println("除法结束......@After");
}
// 在目标方法(即div方法)正常返回了,有返回值,被调用
@AfterReturning("public int lsp.mdy.aop.MathCalculator.*(..)")
public void logReturn() {
System.out.println("除法正常返回......@AfterReturning,运行结果是:{}");
}
// 在目标方法(即div方法)出现异常,被调用
@AfterThrowing("public int lsp.mdy.aop.MathCalculator.*(..)")
public void logException() {
System.out.println("除法出现异常......异常信息:{}");
}
}
在该切面类中定义几个打印日志的方法,以这些方法来动态地感知MathCalculator类中的div()方法的运行情况。如果需要切面类来动态地感知目标类方法的运行情况,那么就需要使用Spring AOP中的一系列通知方法了。
可以看到每一个通知方法上都写上(“public int mdy.lsp.aop.MathCalculator.*(…)”) ,太麻烦了。 如果切入点表达式都一样的情况下,那么我们可以抽取出一个公共的切入点表达式,就像下面这样。
// 如果切入点表达式都一样的情况下,那么我们可以抽取出一个公共的切入点表达式
@Pointcut("execution(public int lsp.mdy.aop.MathCalculator.*(..))")
public void pointCut() {}
若果是在本切面类引用该公共切入点表达式的话,可以用如下形式
@Before("pointCut()")
如果是外部类(即其他的切面类)引用,那么就得在通知注解中写方法的全名了
@After("lsp.mdy.aop.LogAspects.pointCut()")
由于我们只是在本切面类中引用该切入点表达式,所以最后我们的切面类代码如下
import org.aspectj.lang.annotation.*;
/**
* 切面类
* @Aspect:告诉Spring当前类是一个切面类,而不是一些其他普通的类
*/
@Aspect
public class LogAspects {
// 如果切入点表达式都一样的情况下,那么我们可以抽取出一个公共的切入点表达式
@Pointcut("execution(public int lsp.mdy.aop.MathCalculator.*(..))")
public void pointCut() {}
// @Before:在目标方法(即div方法)运行之前切入,public int com.meimeixia.aop.MathCalculator.div(int, int)这一串就是切入点表达式,指定在哪个方法切入
// @Before("public int com.meimeixia.aop.MathCalculator.*(..)")
@Before("pointCut()")
public void logStart() {
System.out.println("除法运行......@Before,参数列表是:{}");
}
// 在目标方法(即div方法)结束时被调用
// @After("pointCut()")
@After("lsp.mdy.aop.LogAspects.pointCut()")
public void logEnd() {
System.out.println("除法结束......@After");
}
// 在目标方法(即div方法)正常返回了,有返回值,被调用
@AfterReturning("pointCut()")
public void logReturn() {
System.out.println("除法正常返回......@AfterReturning,运行结果是:{}");
}
// 在目标方法(即div方法)出现异常,被调用
@AfterThrowing("pointCut()")
public void logException() {
System.out.println("除法出现异常......异常信息:{}");
}
}
1.4、将目标类与切面类加入ioc容器
写一个配置类来将这个目标类与切面类加入ioc容器
@Configuration
public class MathCalculatorConfig {
@Bean
public MathCalculator mathCalculator(){
return new MathCalculator();
}
@Bean
public LogAspects logAspects(){
return new LogAspects();
}
}
1.5、使用 @Aspect注解
告诉Spring当前类是一个切面类,而不是一些其他普通的类。也就是在切面类LogAspects上加一个@Aspect注解。
1.6、使用 EnableAspectJAutoProxy
在配置类中使用 EnableAspectJAutoProxy 注解开启AOP切面自动代理功能
1.7、测试
注意,在进行测试的时候不要自己去new一个MathCalculator对象去进行测试,而是从容器中去获取我们放进去的MathCalculator对象,要不然不在容器中不能使用spring提供的功能。
@Test
public void test01(){
AnnotationConfigApplicationContext ioc = new AnnotationConfigApplicationContext(MathCalculatorConfig.class);
MathCalculator mathCalculator = (MathCalculator) ioc.getBean("mathCalculator");
int div = mathCalculator.div(5, 5);
System.out.println("结果是: "+div);
}
基本的环境搭建已经完成了,而如果我们想要获取运行方法的某些参数,比如方法名,方法参数等,我们可以在通知方法的参数列表加上一个 JoinPoint 类型的形参。 这里,需要注意的是,JoinPoint参数一定要放在参数列表的第一位,否则Spring是无法识别的,那自然就会报错了。
切面类改进如下
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import java.util.Arrays;
/**
* 切面类
* @Aspect:告诉Spring当前类是一个切面类,而不是一些其他普通的类
* @author liayun
*/
@Aspect
public class LogAspects {
// 如果切入点表达式都一样的情况下,那么我们可以抽取出一个公共的切入点表达式
// 1. 本类引用
// 2. 如果是外部类,即其他的切面类引用,那就在这@After("...")写的是方法的全名,而我们就要把切入点写在这儿@Pointcut("...")
@Pointcut("execution(public int lsp.mdy.aop.MathCalculator.*(..))")
public void pointCut() {}
// @Before:在目标方法(即div方法)运行之前切入,public int mdy.lsp.aop.MathCalculator.div(int, int)这一串就是切入点表达式,指定在哪个方法切入
// @Before("public int mdy.lsp.aop.MathCalculator.*(..)")
@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("pointCut()")
@After("lsp.mdy.aop.LogAspects.pointCut()")
public void logEnd(JoinPoint joinPoint) {
// System.out.println("除法结束......@After");
System.out.println(joinPoint.getSignature().getName() + "结束......@After");
}
// 在目标方法(即div方法)正常返回了,有返回值,被调用
// @AfterReturning("pointCut()")
@AfterReturning(value="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("pointCut()")
public void logException() {
System.out.println("除法出现异常......异常信息:{}");
}
}
测试
如果目标方法运行时出现了异常,而我们又想拿到这个异常信息,那么该怎么办呢?只须对LogAspects切面类中的logException()方法进行优化即可,优化后的结果如下所示。
// 在目标方法(即div方法)出现异常,被调用
// @AfterThrowing("pointCut()")
@AfterThrowing(value="pointCut()", throwing="exception")
public void logException(JoinPoint joinPoint, Exception exception) {
// System.out.println("除法出现异常......异常信息:{}");
System.out.println(joinPoint.getSignature().getName() + "出现异常......异常信息:{" + exception + "}");
}
接下来我们来模拟一个除0异常:
@Test
public void test01(){
AnnotationConfigApplicationContext ioc = new AnnotationConfigApplicationContext(MathCalculatorConfig.class);
MathCalculator mathCalculator = (MathCalculator) ioc.getBean("mathCalculator");
int div = mathCalculator.div(5, 0);
System.out.println("结果是: "+div);
}
小结:
搭建AOP测试环境时 ,有三步是最关键的:
1. 将切面类和业务逻辑组件(目标方法所在类)都加入到容器中,并且要告诉Spring哪个类是切面类(标注了@Aspect注解的那个类)。
2. 在切面类上的每个通知方法上标注通知注解,告诉Spring何时何地运行,当然最主要的是要写好切入点表达式,这个切入点表达式可以参照官方文档来写。
3. 开启基于注解的AOP模式,即加上@EnableAspectJAutoProxy注解,这是最关键的一点。