Spring Aop官方文档:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop
Spring的AOP理解
在我们日常开发当中都是面向对象(OOP),允许开发者是自上而下调用关系,但并不适用于定义横向的关系,会导致大量代码的重复,而不利于各个模块的重用。
AOP,一般称为面向切面,作为面向对象的一种扩展,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的冗余代码,降低了模块间的耦合度,提高系统的可维护性。可用于权限认证、日志、事务、限流处理。
AOP实现的关键在于代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。
基本使用
依赖配置
jar 说明:aspectjweaver.jar 用于面向方面的编程或AOP,编织器实际上是将用每个实例中要执行的代码定义的方法/切入点/连接点“编织”在一起的部分。
<properties>
<java.version>1.8</java.version>
<spring.version>5.1.5.RELEASE</spring.version>
<aspectjweaver.version>1.9.5</aspectjweaver.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectjweaver.version}</version>
</dependency>
</dependencies>
配置类AopConfig
@Configuration
// 开启AOP支持
// 该注解中会使用Import注解导入后置处理器及注册自定义Bean用来完成AOP功能
@EnableAspectJAutoProxy
@ComponentScan("com.zlp.spring.aop")
public class AopConfig {
}
计算器测试类MathService
@Service
public class MathService {
public int division(int i,int j){
System.out.println(String.format("division.req ===>i=%d,j=%d",i,j));
return i / j;
}
}
切面AopLog
/**
* Aop 日志切面
* @date: 2022/2/25 11:11
*/
@Aspect
@Component
public class AopLog {
@Pointcut("execution(* com.zlp.spring.aop.service..*.*(..) )")
public void pointCut() { }
// 方法执行前通知
@Before("pointCut()")
public void beforeLog(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
System.out.println("运行 Before 日志记录... 参数为:" + Arrays.asList(args));
}
// 方法执行完后通知
@After("pointCut()")
public void afterLog(JoinPoint joinPoint) {
System.out.println("运行 After 日志记录");
}
// 执行成功后通知
@AfterReturning(value = "pointCut()", returning = "result")
public void afterReturningLog(JoinPoint joinPoint,Object result) {
System.out.println("运行 AfterReturning 日志记录,结果为:" + result);
}
// 抛出异常后通知
@AfterThrowing(value = "pointCut()", throwing = "exception")
public void logException(JoinPoint joinPoint, Exception exception) {
System.out.println(joinPoint.getSignature().getName() + "运行AfterThrowing... 异常信息:" + exception);
}
// 环绕通知
@Around("pointCut()")
public Object aroundLog(ProceedingJoinPoint joinpoint) {
// // 获取被增强的目标对象,然后获取目标对象的class
// Class<?> targetClass = joinpoint.getTarget().getClass();
// System.out.println("执行Around,被增强的目标类为:" + targetClass);
// // 方法名称
// String methodName = joinpoint.getSignature().getName();
// System.out.println("执行Around,目标方法名称为:" + methodName);
// // 目标方法的参数类型
// Class[] parameterTypes = ((MethodSignature) joinpoint.getSignature()).getParameterTypes();
// // 目标方法的入参
// Object[] args = joinpoint.getArgs();
// System.out.println("执行Around,方法入参为:" + Arrays.toString(args));
Object result = null;
try {
System.out.println("环绕通知 Before 日志记录");
long start = System.currentTimeMillis();
//有返回参数 则需返回值
result = joinpoint.proceed();
long end = System.currentTimeMillis();
System.out.println("总共执行时长" + (end - start) + " 毫秒");
System.out.println("环绕通知 After 日志记录");
} catch (Throwable t) {
System.out.println("出现错误");
}
return result;
}
}
测试类 AopTest
public class AopTest {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(AopConfig.class);
MathService mathService = ac.getBean("mathService", MathService.class);
mathService.division(1, 1);
}
}
运行成功结果:
环绕通知 Before 日志记录
运行 Before 日志记录... 参数为:[1, 1]
target method division.req ===>i=1,j=1
总共执行时长111 毫秒
环绕通知 After 日志记录
运行 After 日志记录
运行 AfterReturning 日志记录,结果为:1
运行异常结果:
环绕通知 Before 日志记录
运行 Before 日志记录... 参数为:[1, 0]
target method division.req ===>i=1,j=0
运行 After 日志记录
division运行AfterThrowing... 异常信息:java.lang.Exception: 出现错误/ by zero
就这样简单,一个AOP的使用示例就成功运行完成了,那么示例中的 @Aspect,@PointCut,@Before等都是什么鬼玩意呢?请继续向下看。
Spring的AOP中的概念
切面
切面指的是由一系列切点及增强动作组成的模块对象,在使用过程中,如果有多个切面,可以通过Spring提供的Order接口或者 @Order 注解来定义切面的优先级,从而可以按照指定的顺序来对应用功能进行增强。例如上例中的LogAop测试类就是一个切面,使用注解版的切面类,切面需要使用注解 @Aspect 来进行标注。
经常使用的场景:接口性能监控,事务管理,日志记录,限流,读写分离等。
切点
切点也是一个抽象的概念,可以简单理解为数据库中的查询条件。例如在数据库中,某条数据满足一定的查询条件才会被查询出来,而如果某一个类中的方法满足切点中定义的条件时,切点对应的增强动作才会被执行。而这个切点对应的条件,成为切点表达式,切点表达式通常有如下的几种类型:
(1)execution表达式
该表达式的最小粒度为方法,使用语法如下:
execution(modifier returnType package.method(param) exception);
参数说明:
modifier:方法修饰符,例如:public,private等,可以省略
returnType:方法返回值类型,如:int,void
package:方法所在类的包名,可以省略
method:方法名称
param:方法参数
exception:方法抛出的异常,可以省略
示例1:
下面表达式会匹配到使用 public 修饰,返回值为int(如果写为*,则表示匹配任意返回值),类名称为MathService中的所有返回int方法,方法中可以带有参数,也可以不带参数
execution(public int com.zlp.spring.aop.service.MathService.*(..))
示例2:
下面表达式会匹配到返回值为任意类型,在 com.zlp.spring.aop.service.MathService 类中,并且不带参数的方法
execution(* com.zlp.spring.aop.service.MathService.*())
示例3:
下面表达式会匹配到返回值为任意类型,并且在com.zlp.spring包及其子包下的所有类中名称为testService的没有参数的方法:
..通配符值的是匹配0个或者多个。
execution(* com.zlp.spring..*.testService())
示例4:
下面表达式会匹配到返回值为任意类型,并且以Math开头的所有类中的所有第一个参数为String类型的的所有方法
execution(* com.zlp.spring.aop.service.Math*.*(java.lang.String, ..))
(2)within类型
within声明的切点表达式最小粒度为类,匹配到表达式的所有类中的所有方法都会被增强,使用方法如下:
within(match-pattern)
示例1:
如下的表达式会匹配 com.zlp.spring.aop.service.MathService 类下的所有方法
within(com.zlp.spring.aop.service.MathService)
示例2:
如下的表达式会匹配到 com.zlp.spring.aop 包下的所有类下的所有方法,但是不包括子包中的类
within(com.zlp.spring.aop.*)
示例3:
如下的表达式会匹配到com.zlp.spring.aop包及其子包下的所有类中的所有方法
within(com.zlp.spring.aop..*)
(3)args类型
args类型不用关注类名和方法名,只需要关注方法中的参数类型和参数个数,但是如果指定参数类型时,需要指定类型对应的全路径,语法如下:
args(match-pattern)
示例1:
下面表达式可以匹配到只有一个String类型的参数对应的方法
args(java.lang.String)
示例2:
下面表达式会匹配到第一个参数为String类型,最后一个类型为Long类型的所有方法
args(java.lang.String,..,java.lang.Long)
(4)@within类型
within表示匹配指定的类,@within表示匹配带有指定注解的类。语法如下:
@within(annotation-pattern)
示例:
下面示例表示匹配所有标注有com.zlp.spring.aop.aop.MyAnnotation的类
@within(com.zlp.spring.aop.aop.MyAnnotation)
只要切面中使用 @within声明了切入 @MyAnnotation 注解,则只要标注有该注解的类下面的方法,在运行期间都会被切到,一般用于类上面。
(5)@annotation类型
@annotation和@within类似,只是@annotation中指定的注解一般用于某个类中的具体方法上,只要标注了@annotation中指定的注解,那么该方法在运行期间就会被切面拦截到。语法如下:
@annotation(annotation-pattern)
示例1:
下面示例表示匹配所有标注有 com.wb.spring.aop.MyAnnotation 的方法
@annotation(com.wb.spring.aop.MyAnnotation)
示例2:
例如下面的div方法执行时会被环绕增强,因为方法上方标注有@MyAnnotation注解,而切面表达式刚好是匹配到该注解。
注解类 MyAnnotation
@Target({ElementType.TYPE,ElementType.METHOD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
}
测试类MyService
@Component
public class MyService {
@MyAnnotation
public int div(int a, int b) {
return a + b;
}
}
切面类MyAspect
/**
* 自定义切面
* @date: 2022/2/25 14:23
*/
@Aspect
@Component
public class MyAspect {
/**
* <p>
* 1. @within 声明了切入 @MyAnnotation 注解,则只要标注有该注解的类下面的方法,在运行期间都会被切到,一般用于类上面。
* eg: @Around("@within(com.zlp.spring.aop.aop.MyAnnotation)")
* 2. @annotation中指定的注解一般用于某个类中的具体方法上,只要标注了@annotation中指定的注解,那么该方法在运行期间就会被切面拦截到。
* eg: @Around("@@annotation(com.zlp.spring.aop.aop.MyAnnotation)")
* </p>
* @param proceed
* @date: 2022/2/25 14:18
* @return: java.lang.Object
*/
@Around("@annotation(com.zlp.spring.aop.aop.MyAnnotation)")
public Object around(ProceedingJoinPoint proceed) throws Throwable {
System.out.println("around... invoke ...");
return proceed.proceed();
}
}
(6)@args类型
@args表示使用了指定注解的类作为某个方法的入参,在这个方法被调用的时候,方法会被增强。语法如下:
@args(annotation-pattern)
示例1:
下面的示例表示匹配使用MyAnnotation注解标注的类作为参数的方法
@args(com.zlp.spring.aop.aop.MyAnnotation)
示例2:
使用切面功能对方法入参中包括Pen类型参数的方法进行增强。
测试类Pen
@Component
public class Pen {
@MyAnnotation
public String writeText() {
return "Hello world";
}
}
测试类MyClass
@Component
public class MyClass {
public String write(Pen pen) {
return pen.writeText();
}
}
切面类MyAspect
@Aspect
@Component
public class MyAspect {
@Around("@annotation(com.zlp.spring.aop.aop.MyAnnotation)")
public Object around(ProceedingJoinPoint proceed) throws Throwable {
System.out.println("around... invoke ...");
return proceed.proceed();
}
}
测试类AopTest
public class AopTest {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(AopConfig.class);
MyClass myClass = ac.getBean(MyClass.class);
Pen pen = ac.getBean(Pen.class);
myClass.write(pen);
}
}
增强动作
增强动作指的是程序执行过程中,如果所执行的方法满足某一个切点定义的条件之后,与切点对应的增强方法就会被触发而执行。有如下几种:
(1)@Around,环绕通知
这个注解的功能最丰富,使用这个注解标注的方法是用来实现业务逻辑代码的环绕增强,入参为ProceedingJoinPoint,可以用来调用业务模块的代码,调用之前的逻辑和调用之后的逻辑都可以在这个方法中实现,而且这个方法可以阻断业务模块的调用。
(2)@Before,前置通知
使用这个注解标注的方法会在业务代码执行之前先执行,不能阻断业务逻辑的执行,除非抛出异常。
(3)@After,后置通知
类似于finally,在所有的增强方法执行之后才会执行,无论业务逻辑是否有异常。
(4)@AfterReturning,返回通知
该注解标注的方法在业务逻辑代码执行之后执行。
(5)@AfterThrowing,异常通知
该注解标注的方法在业务逻辑抛出指定异常之后会执行。
增强动作的执行顺序
->Around Before[@Before标注的方法方法执行前]
->Before[@Before标注的方法执行]
->目标方法[目标方法执行]
->Around After[目标方法执行之后,@After之前]
->After[@After标注的方法执行]
-> AfterReturning (如果有异常,则AfterThrowing)[返回或者异常]
至此,SpringAOP 相关的基础用法介绍完毕,本篇文章通过一个简单的示例,说明了SpringAOP中常用的内容,例如切点,切面、切点表达式。后续文章将继续剖析SpringAOP的底层执行过程。欢迎评论转发!
代码地址(aop项目)
https://gitee.com/gaibianzlp/spring-study-example
Spring底层代理是哪种方式?
https://www.jianshu.com/p/f19a3504337a
[
AnnotationAwareAspectJAutoProxyCreator
https://segmentfault.com/a/1190000022372094
Spring中Aop底层实现原理?