后端:Aop 面向切面编程
文章目录
- 1. Aop 初步学习面向切面编程,@EnableAspectJAutoProxy
- 2. AOP的核心概念
- 3. 前置通知(@Before)
- 4. 后置通知(@After)
- 5. 返回通知(@AfterReturning)
- 6. 异常通知(@AfterThrowing)
- 7. 通知的执行顺序
- 8. 切点表达式的抽取
- 9. 切点表达式的书写
- 9.1 execution 实现切点表达式
- 9.2 within 实现切点表达式
- 9.3 @annotation 实现切点表达式
1. Aop 初步学习面向切面编程,@EnableAspectJAutoProxy
添加依赖,在新建的Spring Boot项目下的pom.xml文件添加aop对应的依赖,如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
通过切面,在不改变原有代码的前提下,增强源代码的业务能力。下面是一段演示aop的代码。
package com.lize.demo.aop;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
@Service
public class UserService {
public void add(){
System.out.println("增加");
}
public void del(){
System.out.println("删除");
}
public void query(){
System.out.println("查询");
}
public void update(){
System.out.println("修改");
}
}
切面类,除了需要添加@Aspect注解表示这是一个切面类之外,还需要添加注解@Component表明这是一个Bean。通过注解@Around里边写上具体需要切入的方法,最终实现切面功能。
package com.lize.demo.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
// 标记为切面类
@Component
// 必须设置为切面的Bean
public class MyAspect {
// 实现计时方法
@Around("execution(* com.lize.demo.aop.UserService.*(..) )")
// 切点表达式
// * 表示方法的访问权限 可以为public等
public void logTime(ProceedingJoinPoint point){
long begin = System.currentTimeMillis();
// 执行具体的方法
try {
point.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("用时为:"+(end-begin));
}
}
单元测试类
package com.lize.demo.aop;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@SpringBootTest(classes = AopDemo.class)
@ComponentScan
public class AopDemo {
@Test
public void test(@Autowired UserService us){
us.add();
}
}
运行结果如下:
只运行了add方法,原本只是输出“增加”,但是添加切面之后,除了输出“增加”这个字符串之外,还额外输出执行这个add方法的用时。在这个过程中,出现问题有,运行上述代码之后,切面没有起作用,也就是打印结果依旧为“增加”,但是我的项目是一个Spring Boot项目,Spring Boot项目会自动通过启动类帮我们加上@EnableAspectJAutoProxy,从而使切面起作用。解决方法可以是在这个单元测试类上加上这个注解@EnableAspectJAutoProxy,或者去掉@ComponentScan注解以及把@SpringBootTest(classes = AopDemo.class)这个注解修改为@SpringBootTest。出现这个问题的原因是因为我的单元测试所在的目录与Spring Boot项目启动类所在的目录不在同一个目录下,如果在同一个目录下,可以省略这个注解 @EnableAspectJAutoProxy,不过,建议加上。
2. AOP的核心概念
目标对象(target):目标对象指将要被增强的对象。即包含主业务逻辑的类的对象,要增强的对象通常会有很多个。
切面(aspect):指放存放增强代码的类。
通知(advice):用来放增强的代码的那个方法,通知方式可以有环绕通知(@Around)【代码增强在目标方法的任意位置,更加通用】、前置通知(@Before)【目标方法之前执行】、后置通知(@After)【目标方法之后执行】、异常通知(@AfterThrowing)【目标方法出现了异常执行】、返回通知(@AfterReturning)【目标方法返回值执行】。
切点(pointcut):增强代码要切入到哪些方法中,在代码中通常写的是切点表达式。
连接点(Join point):通知和目标方法的一个桥梁,要获取目标方法的信息,就得通过JoinPoint。
关于连接点JoinPoint,在上面的代码中使用的是这个ProceedingJoinPoint,查看源码可知,ProceedingJoinPoint继承JoinPoint,并且还添加了如下方法的。
3. 前置通知(@Before)
现在我想要在执行上述代码中的add方法之前获取add这个方法名,此时可以考虑使用前置通知,在上述代码的基础之上,修改对应的注解即可,参考代码如下:
package com.lize.demo.aop.advice;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
// 标记为切面类
@Component
// 必须设置为切面的Bean
public class MyAspect {
@Before("execution(* com.lize.demo.aop.advice.UserService.*(..) )")
// 切点表达式
public void before(JoinPoint joinPoint){
String name = joinPoint.getSignature().getName();
// 获取当前执行的方法名
System.out.println("当前执行的方法是:"+name);
}
}
运行结果:
4. 后置通知(@After)
在目标方法执行之后再执行的代码,如下:
@After("execution(* com.lize.demo.aop.advice.UserService.*(..) )")
// 切点表达式
public void after(JoinPoint joinPoint){
System.out.println("后置通知");
}
5. 返回通知(@AfterReturning)
在try\catch\finally这个防止异常代码结构中,如果在try中有返回值,依旧会执行finally里边的代码
获取目标方法的返回值:
@AfterReturning(value = "execution(* com.lize.demo.aop.advice.UserService.*(..) )",returning = "ans")
public void afterReturning(JoinPoint joinPoint,Object ans){
System.out.println("返回通知。。目标方法的返回值为:"+ans);
}
上述代码中为了获取返回值对象,在注解中使用returning进行接收,查看@AfterReturning的源码就可以知道了。
运行结果如下:
因为在add方面里边没有返回值,因此上述返回值为null。
6. 异常通知(@AfterThrowing)
当目标方法出现异常时执行的方法,参考代码如下:
@AfterThrowing(value = "execution(* com.lize.demo.aop.advice.UserService.*(..) )",throwing = "ans")
public void afterThrowing(JoinPoint joinPoint,Exception ans){
System.out.println("异常通知。。目标方法的报错信息为:"+ans);
}
通过throwing 获取异常信息,查看一下@AfterThrowing的源码就知道了。
运行结果如下:
7. 通知的执行顺序
- 正常情况下:先执行前置通知、再执行目标方法,然后执行返回通知、最后执行后置通知;
- 异常情况下:先执行前置通知、再执行目标方法、然后执行异常通知、最后执行后置通知。
在环绕通知中可以包含其他四种通知,因此环绕通知也是通用的。
8. 切点表达式的抽取
看一下上面的切点表达式,如果要进行修改,那么这些都需要进行修改,为此,可以利用切点表达式的抽取,把切点表达式抽取出来,这样只需要修改一处地方,在需要引入切点表达式的地方,引入方法即可,如下:
package com.lize.demo.aop.advice;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
// 标记为切面类
@Component
// 必须设置为切面的Bean
public class MyAspect {
@Pointcut("execution(* com.lize.demo.aop.advice.UserService.*(..) )")
public void pointCut(){}
@Before("pointCut()")
// 切点表达式
public void before(JoinPoint joinPoint){
String name = joinPoint.getSignature().getName();
// 获取当前执行的方法名
System.out.println("当前执行的方法是:"+name);
}
@After("pointCut()")
// 切点表达式
public void after(JoinPoint joinPoint){
System.out.println("后置通知");
}
@AfterReturning(value = "pointCut()",returning = "ans")
public void afterReturning(JoinPoint joinPoint,Object ans){
System.out.println("返回通知。。目标方法的返回值为:"+ans);
}
@AfterThrowing(value = "pointCut()",throwing = "ans")
public void afterThrowing(JoinPoint joinPoint,Exception ans){
System.out.println("异常通知。。目标方法的报错信息为:"+ans);
}
}
9. 切点表达式的书写
9.1 execution 实现切点表达式
execution 能匹配到方法级别
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
- execution 表示切点标识符
- modifiers-pattern 表示访问修饰符,即目标方法的访问权限,不写代表所有;
- ret-type-pattern 表示目标方法的返回值类型,可以为void,*代表所有;
- declaring-type-pattern 表示完整限定名,包含包名和类名;
- 包名
- 可以写完整的,如com.xx.service
- 也可以这样写,如com.xx.* 等价于 com.xx.dao、com.xx.service等,反正”*“只能代表一个层级吧!比如com.xx.service.inter是不能匹配到的
- 如果想任意层级 ,可以这样写com.xx…,这样就可以匹配到com.xx.service.inter、com.xx.service.inter.until等
- 类名
- ”*“代表所有类
- com.xx…*可以匹配到com.xx.service.utils.inter.任意包.任意类
- 包名
- name-pattern 表示方法,"*"代表匹配所有
- param-pattern 表示方法的参数,”…“表示任意参数,不写表示匹配无参数
9.2 within 实现切点表达式
within 只能匹配到类级别,表示类下面的所有方法都能匹配到。
@Before("within(com.lize.demo.aop.advice.UserService)")
// 切点表达式
public void before(JoinPoint joinPoint){
String name = joinPoint.getSignature().getName();
// 获取当前执行的方法名
System.out.println("当前执行的方法是:"+name);
}
9.3 @annotation 实现切点表达式
@annotation 用于匹配特定注解的方法
这里定义了一个自定义的注解
package com.lize.demo.aop.advice;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface MyLog {
String value();
}
@Before("@annotation(log)")
// 切点表达式
public void before(JoinPoint joinPoint,MyLog log){
String name = joinPoint.getSignature().getName();
// 获取当前执行的方法名
System.out.println("当前执行的方法是:"+name+" "+log);
}
运行结果: