Spring-AOP(面向切面编程)
面向切面编程(Aspect Oriented Programming-AOP)是面向对象编程(Object Oriented Programming-OOP)的一种补充,二者是互补的编程范式。在OOP中,关键单元是类,而在AOP中关键单元则是横切关注点。面向对象编程关注于将现实世界中的实体抽象为对象,并通过对象之间的交互来模拟现实世界的复杂关系。面向切面关注于将横切关注点(如日志、安全、事务管理等)从核心业务逻辑中分离出来。
AOP框架是Spring的关键组件之一,虽然Spring IoC容器不依赖于AOP(这意味着如果不想使用AOP则不需要AOP相关依赖),但是AOP补充了Spring IoC,提供了一个非常强大的中间件解决方案。
问题引入
当前业务需要实现一个计算器功能的接口,并且在此接口的基础上具备打印计算前后的日志功能。
计算机接口
/*** 计算器接口*/
public interface Calculator {/*** 加** @param i 整数* @param j 整数* @return 计算结果*/int add(int i, int j);/*** 减** @param i 整数* @param j 整数* @return 计算结果*/int sub(int i, int j);/*** 乘** @param i 整数* @param j 整数* @return 计算结果*/int mul(int i, int j);/*** 除** @param i 整数* @param j 整数* @return 计算结果*/int div(int i, int j);
}
计算机核心功能(计算功能)实现类
/*** 计算器接口实现类*/
public class CalculatorImpl implements Calculator {@Overridepublic int add(int i, int j) {int result = i + j;System.out.println("方法内部 result = " + result);return result;}@Overridepublic int sub(int i, int j) {int result = i - j;System.out.println("方法内部 result = " + result);return result;}@Overridepublic int mul(int i, int j) {int result = i * j;System.out.println("方法内部 result = " + result);return result;}@Overridepublic int div(int i, int j) {int result = i / j;System.out.println("方法内部 result = " + result);return result;}
}
计算器核心功能附加日志功能实现类
/*** 带日志的计算器接口实现*/
public class CalculatorLogImpl implements Calculator{@Overridepublic int add(int i, int j) {System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);int result = i + j;System.out.println("方法内部 result = " + result);System.out.println("[日志] add 方法结束了,结果是:" + result);return result;}@Overridepublic int sub(int i, int j) {System.out.println("[日志] sub 方法开始了,参数是:" + i + "," + j);int result = i - j;System.out.println("方法内部 result = " + result);System.out.println("[日志] sub 方法结束了,结果是:" + result);return result;}@Overridepublic int mul(int i, int j) {System.out.println("[日志] mul 方法开始了,参数是:" + i + "," + j);int result = i * j;System.out.println("方法内部 result = " + result);System.out.println("[日志] mul 方法结束了,结果是:" + result);return result;}@Overridepublic int div(int i, int j) {System.out.println("[日志] div 方法开始了,参数是:" + i + "," + j);int result = i / j;System.out.println("方法内部 result = " + result);System.out.println("[日志] div 方法结束了,结果是:" + result);return result;}
}
分析问题
针对附加日志功能的实现类中,能够发现以下问题:
1.日志功能对核心业务功能有干扰,会导致程员在开发核心业务功能需要考虑日志功能的开发而分散了精力。
2.附加的日志功能分散在每个业务功能方法中,不利于统一维护。针对这两个问题,最主要的解决方式就是解耦,将附加日志功能从核心业务功能代码中抽离出来。面向切面编程(AOP)正式解决这种问题而诞生的,在OOP(面向对象编程)中开发者常常会遇到一些横切关注点(Cross Cutting Concerns),比如日志、安全、事务管理等,这些关注点分布在多个类和方法中,导致代码重复、耦合度高,且难以维护。为了解决这些问题AOP应运而生。AOP的核心思想就是将这些横切关注点从核心业务逻辑中分离出来,形成独立的模块,然后在需要的时候通过"编织"(weaving)的方式动态地注入到核心业务逻辑中。这样就key实现关注点的分离,提高代码可读性、可维护性、可重用性。
代理模式(Proxy)
由于Spring-AOP是基于动态代理实现的,这里先讲解一下代理模式的基本概念。
概念
代理模式是一种设计模式,它提供了对目标对象的代理,以控制对目标对象的方法。代理对象通常具有与目标对象相同的接口,以便可以在任何需要目标对象的地方使用代理对象。
代理模式的主要角色:
-
主体(Subject):定义了实际主体与代理人的共同接口,以便在任何需要实际主体时使用代理人。
-
实际主体(Real Subject): 实现了主体中定义的接口,相当于目标对象。
-
代理人(Proxy): 实现了主体中定义的接口,内部会有实际主体的引用,在代理人不能处理时,使用实际主体进行处理,其余均有代理人处理。
代理模式的主要种类:
- 静态代理: 代理类在编译时就已经确定,通常需要为每一个实际主体类编写代理类。
- 动态代理: 代理类在运行时动态生成,不需要为每个实际主体类编写代理类。动态代理提供了更高的灵活度。常见的动态代理的实现方式有JDK动态代理和CGLIB。
静态代理示例
主体类
/*** 计算器接口*/
public interface Calculator {/*** 加** @param i 整数* @param j 整数* @return 计算结果*/int add(int i, int j);/*** 减** @param i 整数* @param j 整数* @return 计算结果*/int sub(int i, int j);/*** 乘** @param i 整数* @param j 整数* @return 计算结果*/int mul(int i, int j);/*** 除** @param i 整数* @param j 整数* @return 计算结果*/int div(int i, int j);
}
实际主体类
/*** 计算器接口实现类*/
public class CalculatorImpl implements Calculator {@Overridepublic int add(int i, int j) {int result = i + j;System.out.println("方法内部 result = " + result);return result;}@Overridepublic int sub(int i, int j) {int result = i - j;System.out.println("方法内部 result = " + result);return result;}@Overridepublic int mul(int i, int j) {int result = i * j;System.out.println("方法内部 result = " + result);return result;}@Overridepublic int div(int i, int j) {int result = i / j;System.out.println("方法内部 result = " + result);return result;}
}
代理类
/*** 计算器静态代理类*/
public class CalculatorStaticProxy implements Calculator {private static final Logger logger = LoggerFactory.getLogger(CalculatorStaticProxy.class);//被代理的目标传进来private Calculator calculator;public CalculatorStaticProxy(Calculator calculator) {this.calculator = calculator;}@Overridepublic int add(int i, int j) {//输出日志logger.info("[日志]: add方法开始了,参数为[{}]和[{}]", i, j);int addResult = calculator.add(i, j);logger.info("[日志]: add方法结束了,结果为:[{}]", addResult);return addResult;}@Overridepublic int sub(int i, int j) {return 0;}@Overridepublic int mul(int i, int j) {return 0;}@Overridepublic int div(int i, int j) {return 0;}
}
测试类
/*** 测试静态代理*/
public class TestStaticProxy {public static void main(String[] args) {Calculator calculator = new CalculatorImpl();CalculatorStaticProxy calculatorStaticProxy = new CalculatorStaticProxy(calculator);calculatorStaticProxy.add(1, 2);}
}
动态代理示例
主体类与实际主体类与静态代理示例中的类一致,这里就只展示代理类和测试类
代理类
/*** 动态代理类*/
public class ProxyFactory {private static final Logger logger = LoggerFactory.getLogger(ProxyFactory.class);//传入目标对象private Object target;public ProxyFactory(Object target) {this.target = target;}//返回代理对象public Object getProxy() {/***Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)* 获取代理对象的方法* 三个传参* loader: 类加载器* interfaces: 目标类对象实现的所有接口* h: 是一个接口,需要实现其方法。设置代理对象实现目标对象方法的过程* Object invoke(Object proxy, Method method, Object[] args)** proxy 代理对象* method 需要重写目标对象的方法* args 对应目标对象方法的参数*/ClassLoader classLoader = target.getClass().getClassLoader();Class<?>[] interfaces = target.getClass().getInterfaces();//匿名内部类InvocationHandler invocationHandler = new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {logger.info("[动态代理:日志]: {} 方法,参数为:{}", method.getName(), Arrays.toString(args));//调用目标方法Object result = method.invoke(target, args);logger.info("[动态代理:日志]: {} 方法,执行结果为:{}", method.getName(), result.toString());return result;}};return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);}
}
测试类
/*** 测试动态代理类*/
public class TestProxyFactory {public static void main(String[] args) {Calculator calculator = new CalculatorImpl();ProxyFactory proxyFactory = new ProxyFactory(calculator);Calculator calculatorProxy = (Calculator) proxyFactory.getProxy();calculatorProxy.add(1, 2);calculatorProxy.mul(2, 5);}
}
AOP的概念
AOP(面向切面编程)是一种编程范式,旨在通过分离横切关注点来增加模块化。它允许开发者定义跨多个对象的横切行为(或称为切面),从而避免将这些行为重复编写到每个对象中。
Aspect(切面)
定义横切关注点的模块。包含切入点(Pointcut)和通知(Advice)。
在Spring AOP中切面是用@Aspect注释(AspectJ风格)的常规类实现的。
Join point(连接点)
程序执行过程中的一个点,如执行方法或处理异常。
在Spring AOP中,连接点总是表示一个方法的执行。
Advice(通知)
切面在特定连接点采取的操作。类型包括:前置通知(Before)、后置通知(After)、返回通知(After Returning)、异常通知(After Throwing)、环绕通知(Around)。
很多AOP框架(包括Spring)将通知建模为拦截器,并在连接点周围维护一系列拦截器。
Pointcut(切入点)
指定在哪些连接点上执行切面逻辑。通知与切入点表达式相关联,并在切入点匹配的任何连接点上运行。
切入点表达式匹配连接点的概念是AOP的核心。
Spring默认使用AspectJ切入点表达式语言。
Target object(目标对象)
被切面逻辑所增强的对象。被一个或多个切面通知(Advice)的对象,也成为通知对象。
因为Spring AOP是通过动态代理实现,所以这个对象总是一个被代理的对象。
Proxy object(代理对象)
包含目标对象和切面逻辑的对象。AOP框架为了实现切面概念(通知方法执行等)而创建的对象。在Spring框架中,AOP代理是JDK动态代理或CGLIB代理。
Weaving(织入)
将切面逻辑插入到目标对象的过程。可以在编译时、加载时、运行时进行。
Spring AOP是在运行时执行织入操作。
基于注解实现AOP
Spring AOP使用的两种动态代理-JDK动态代理与CGLIB动态代理
Spring框架默认情况下优先使用JDK动态代理,但在代理类没有实现接口时,会自动切换到CGLIB动态代理。
JDK动态代理:
- 基于Java的反射机制实现。
- 只能代理实现了接口的类,适用于被代理类有实现接口的情况。
- 生成的代理类实现了被代理类所实现的接口。
- 性能相对较低,因为基于反射调用方法。
CGLIB动态代理:
- 基于ASM字节码生成库实现。
- 可以代理没有接口的类,适用于被代理类没有实现接口与有实现接口的情况。
- 生成的代理类是继承自被代理类的子类。
- 性能相对较高,因为直接操作字节码生成代理类。
引入Spring AOP的相关依赖
<!--spring context依赖--><!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了--><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>6.0.11</version></dependency><!--log4j2的依赖--><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-core</artifactId><version>2.19.0</version></dependency><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-slf4j2-impl</artifactId><version>2.19.0</version></dependency><!--spring aop依赖--><dependency><groupId>org.springframework</groupId><artifactId>spring-aop</artifactId><version>6.0.11</version></dependency><!--spring aspects依赖--><dependency><groupId>org.springframework</groupId><artifactId>spring-aspects</artifactId><version>6.0.11</version></dependency>
开启AspectJ自动代理(为目标对象自动生成代理)
有两种开启AspectJ自动代理的方式:Java配置类和XML配置文件,开启后就可以通过@Aspect注解声明切面类了。
XML配置文件方式开启
<?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/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop.xsd"><!-- 开启组件扫描--><context:component-scan base-package="com.shen.springaop.annoaop"></context:component-scan><!-- 开启aspectj自动代理,为目标对象生成代理--><aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
Java配置类方式开启
@Configuration
@ComponentScan("com.shen.springaop.annoaop")
@EnableAspectJAutoProxy
public class Spring6Config {
}
切面优先级
在同一个目标方法上可能出现多个切面的情况,对于这种情况,切面优先级为外高内低,外面的切面优先级高,里面的切面优先级低。除此之外也可以通过@Order注解控制切面优先级,@Order的value值越小优先级越高。
通知与切入点表达式的使用
由于注解形式,通常是通知注解中包含切入点表达式,因此这两个一起讲。
通知注解
作用于方法上,value存放切入点表达式
通知注解源码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Before {/*** @return the pointcut expression where to bind the advice*/String value();/*** When compiling without debug info, or when interpreting pointcuts at runtime,* the names of any arguments used in the advice declaration are not available.* Under these circumstances only, it is necessary to provide the arg names in* the annotation - these MUST duplicate the names used in the annotated method.* Format is a simple comma-separated list.** @return the argument names (should match the annotated method parameter names)*/String argNames() default "";}
五种通知注解
通知类型 | 通知注解 | 说明 |
---|---|---|
前置通知 | @Before | 在目标方法执行之前执行 |
返回通知 | @AfterReturning | 在目标方法正常执行并返回之后执行,此注解可以指定一个参数用于接收返回值 |
异常通知 | @AfterThrowing | 在目标方法抛出异常,此注解可以指定一个参数用于接收异常 |
后置通知 | @After | 在目标方法执行之后执行,无论目标方法是否抛出异常 |
环绕通知 | @Around | 在目标方法执行前后都可以执行,可以控制目标方法的执行 |
切入点表达式
切入点表达式是在通知注解中,下图是对于切入点表达式的格式讲解。
复用切入点表达式
被@Pointcut注解声明的公共无参无返回值的方法可以实现切入表达式的复用
@Pointcut是用于定义切点(Pointcut)的注解,用于指定在哪些连接点(Join point)上应用通知(Advice),即定义哪些方法执行时会被拦截。被@Pointcut声明的方法配合通知注解就可以实现切入点表达式的复用,需要注解的时@Pointcut声明的方法必须是公共的,无参数的,无返回值的。
@Before("com.shen.springaop.annoaop.LogAspect.pointCut()")public void beforeMethod() {logger.info("前置通知==============");}@Pointcut(value = "execution(* com.shen.springaop.annoaop.CalculatorImpl.div(..))")public void pointCut() {}
基于注解实现AOP的示例
主体类
/*** 计算器接口*/
public interface Calculator {/*** 加** @param i 整数* @param j 整数* @return 计算结果*/int add(int i, int j);/*** 减** @param i 整数* @param j 整数* @return 计算结果*/int sub(int i, int j);/*** 乘** @param i 整数* @param j 整数* @return 计算结果*/int mul(int i, int j);/*** 除** @param i 整数* @param j 整数* @return 计算结果*/int div(int i, int j);
}
实际主体类(被代理类)
/*** 计算器接口实现类*/
@Service
public class CalculatorImpl implements Calculator {@Overridepublic int add(int i, int j) {int result = i + j;System.out.println("方法内部 result = " + result);return result;}@Overridepublic int sub(int i, int j) {int result = i - j;System.out.println("方法内部 result = " + result);return result;}@Overridepublic int mul(int i, int j) {int result = i * j;System.out.println("方法内部 result = " + result);//为了测试模拟异常int a = 1 / 0;return result;}@Overridepublic int div(int i, int j) {int result = i / j;System.out.println("方法内部 result = " + result);//为了测试模拟异常
// int a = 1 / 0;return result;}
}
开启AspectJ自动代理的XML配置文件
<?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/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop.xsd"><!-- 开启组件扫描--><context:component-scan base-package="com.shen.springaop.annoaop"></context:component-scan><!-- 开启aspectj自动代理,为目标对象生成代理--><aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
代理类(切面类)
/*** 切面类*/
@Aspect //声明为切面类
@Order
@Component //ioc容器管理
public class LogAspect {private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);//通知类型: 前置,返回,异常,后置,环绕//前置 @Before()//返回 @AfterReturning//异常 @AfterThrowing//后置 @After()//环绕 @Around()//设置切入点与通知类型//切入点表达式: execution(访问修饰符 增强方法返回类型 增强方法所在类的全类名.方法名称(参数列表))//前置 @Before(value = "切入点表达式配置切入点")@Before(value = "execution(public int com.shen.springaop.annoaop.CalculatorImpl.add(int,int))")public void beforeMethod() {logger.info("前置通知==============");}@After(value = "execution(* com.shen.springaop.annoaop.*.sub(..))")public void afterMethod(JoinPoint joinPoint) {String methodName = joinPoint.getSignature().getName();Object[] args = joinPoint.getArgs();logger.info("后置通知========,方法名为:{},传参为:{}", methodName, Arrays.asList(args));}@AfterReturning(value = "execution(public int com.shen.springaop.annoaop.CalculatorImpl.*(int,int)))", returning = "result")public void afterReturnMethod(JoinPoint joinPoint, Object result) {String methodName = joinPoint.getSignature().getName();logger.info("返回通知========,方法名为:{},返回结果为:{}", methodName, result.toString());}@AfterThrowing(value = "execution(public int com.shen.springaop.annoaop.CalculatorImpl.mul(..))", throwing = "throwable")public void afterThrowingMethod(JoinPoint joinPoint, Throwable throwable) {String methodName = joinPoint.getSignature().getName();logger.info("异常通知========,方法名为:{},异常为:{}", methodName, throwable);}@Around("com.shen.springaop.annoaop.LogAspect.pointCut()")public Object aroundMethod(ProceedingJoinPoint joinPoint) {String methodName = joinPoint.getSignature().getName();String args = Arrays.toString(joinPoint.getArgs());Object object = null;try {logger.info("环绕通知===,目标方法执行前");object = joinPoint.proceed();logger.info("环绕通知===,目标方法返回值之后");} catch (Throwable throwable) {logger.info("环绕通知===,目标方法出现异常:", throwable);} finally {logger.info("环绕通知===,目标方法执行完毕");}return object;}@Pointcut(value = "execution(* com.shen.springaop.annoaop.CalculatorImpl.div(..))")public void pointCut() {}
}
测试类
/*** 测试配置切面类后的方法执行结果*/
public class TestLogAspect {public static void main(String[] args) {ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");Calculator calculator = context.getBean(Calculator.class);
// calculator.add(1, 2);
// calculator.sub(2,3);
// calculator.mul(1,2);calculator.div(2, 1);}
}
基于XML实现AOP(不常用)
基于注解形式的AOP在应用性、可维护性、灵活性和开发效率等方面都优于XML形式,因此在现代Java开发中基于注解形式的AOP更为常用。在某些特定场景或遗留系统中,XML形式的AOP仍然有其应用价值,这里只显示XML形式实现AOP的示例。
示例
由于主体类、实际主体类与注解形式实现AOP的示例一致,这里就只展示代理类、测试类、XML配置文件
XML配置文件
<?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/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop.xsd"><!-- 开启组件扫描--><context:component-scan base-package="com.shen.springaop.xmlaop"></context:component-scan><!-- 配置aop五种通知类型--><aop:config><!-- 配置切面类--><aop:aspect ref="logAspect"><!--配置切入点--><aop:pointcut id="pointcut" expression="execution(* com.shen.springaop.xmlaop.CalculatorImpl.div(..))"/><!-- 前置通知--><aop:before method="beforeMethod" pointcut-ref="pointcut"></aop:before><!-- 后置通知--><aop:after method="afterMethod" pointcut-ref="pointcut"></aop:after><!-- 返回通知--><aop:after-returning method="afterReturnMethod" pointcut-ref="pointcut"returning="result"></aop:after-returning><!-- 异常通知--><aop:after-throwing method="afterThrowingMethod" pointcut-ref="pointcut"throwing="throwable"></aop:after-throwing><!-- 环绕通知--><aop:around method="aroundMethod" pointcut-ref="pointcut"></aop:around></aop:aspect></aop:config></beans>
代理类(切面类)
/*** 切面类*/
@Component //ioc容器管理
public class LogAspect {private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);//通知类型: 前置,返回,异常,后置,环绕//前置 @Before()//返回 @AfterReturning//异常 @AfterThrowing//后置 @After()//环绕 @Around()//设置切入点与通知类型//切入点表达式: execution(访问修饰符 增强方法返回类型 增强方法所在类的全类名.方法名称(参数列表))//前置 @Before(value = "切入点表达式配置切入点")public void beforeMethod() {logger.info("前置通知==============");}public void afterMethod(JoinPoint joinPoint) {String methodName = joinPoint.getSignature().getName();Object[] args = joinPoint.getArgs();logger.info("后置通知========,方法名为:{},传参为:{}", methodName, Arrays.asList(args));}public void afterReturnMethod(JoinPoint joinPoint, Object result) {String methodName = joinPoint.getSignature().getName();logger.info("返回通知========,方法名为:{},返回结果为:{}", methodName, result.toString());}public void afterThrowingMethod(JoinPoint joinPoint, Throwable throwable) {String methodName = joinPoint.getSignature().getName();logger.info("异常通知========,方法名为:{},异常为:{}", methodName, throwable);}public Object aroundMethod(ProceedingJoinPoint joinPoint) {String methodName = joinPoint.getSignature().getName();Object object = null;try {logger.info("环绕通知===,目标方法执行前");object = joinPoint.proceed();logger.info("环绕通知===,目标方法返回值之后");} catch (Throwable throwable) {logger.info("环绕通知===,目标方法出现异常:", throwable);} finally {logger.info("环绕通知===,目标方法执行完毕");}return object;}
}
测试类
/*** 测试配置切面类后的方法执行结果*/
public class TestLogAspect {public static void main(String[] args) {ApplicationContext context = new ClassPathXmlApplicationContext("beanaop.xml");Calculator calculator = context.getBean(Calculator.class);
// calculator.add(1, 2);
// calculator.sub(2,3);
// calculator.mul(1,2);calculator.div(2, 1);}
}
参考资料
https://docs.spring.io/spring-framework/reference/core/aop.html
https://docs.spring.io/spring-framework/reference/core/aop-api.html
https://www.bilibili.com/video/BV1kR4y1b7Qc/