AOP:Aspect Oriented Programming面向切面编程
下面两点是同一件事的两面,一枚硬币的两面:
从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
封装通知方法的类。
被代理的目标对象。
向目标对象应用通知之后创建的代理对象。
这也是一个纯逻辑概念,不是语法定义的。
把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点。
定位连接点的方式。
每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。
如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。
Spring 的 AOP 技术可以通过切入点定位到特定的连接点。
切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
- <dependencies>
-
- <dependency>
- <groupId>org.springframeworkgroupId>
- <artifactId>spring-contextartifactId>
- <version>5.3.1version>
- dependency>
-
- <dependency>
- <groupId>org.springframeworkgroupId>
- <artifactId>spring-aspectsartifactId>
- <version>5.3.1version>
- dependency>
-
- <dependency>
- <groupId>junitgroupId>
- <artifactId>junitartifactId>
- <version>4.12version>
- <scope>testscope>
- dependency>
- dependencies>
接口:
- public interface Calculator {
- int add(int i, int j);
-
- int sub(int i, int j);
-
- int mul(int i, int j);
-
- int div(int i, int j);
- }
实现类:
-
- import com.chenyixin.ssm.annotation.Calculator;
- import org.springframework.stereotype.Component;
-
- @Component
- public class CalculatorPureImpl implements Calculator {
- @Override
- public int add(int i, int j) {
-
- int result = i + j;
-
- System.out.println("方法内部 result = " + result);
-
- return result;
- }
-
- @Override
- public int sub(int i, int j) {
-
- int result = i - j;
-
- System.out.println("方法内部 result = " + result);
-
- return result;
- }
-
- @Override
- public int mul(int i, int j) {
-
- int result = i * j;
-
- System.out.println("方法内部 result = " + result);
-
- return result;
- }
-
- @Override
- public int div(int i, int j) {
-
- int result = i / j;
-
- System.out.println("方法内部 result = " + result);
-
- return result;
- }
- }
-
- import org.aspectj.lang.annotation.*;
- import org.springframework.stereotype.Component;
-
- @Component
- @Aspect // @Aspect表示这个类是一个切面类
- public class LogAspect {
- // @Before注解:声明当前方法是前置通知方法
- // value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上
- @Before(value="execution(public int com.chenyixin.ssm.annotation.imp.CalculatorPureImpl.add(..))")
- public void add() {
- System.out.println("[LogAspect --> 前置通知] 方法执行了");
- }
-
- @AfterReturning(value = "execution(public int com.chenyixin.ssm.annotation.imp.CalculatorPureImpl.add(int,int))")
- public void printLogAfterSuccess() {
- System.out.println("[LogAspect --> 返回通知] 方法成功返回了");
- }
-
- @AfterThrowing(value = "execution(public int com.chenyixin.ssm.annotation.imp.CalculatorPureImpl.add(int,int))")
- public void printLogAfterException() {
- System.out.println("[LogAspect --> 异常通知] 方法抛异常了");
- }
-
- @After(value = "execution(public int com.chenyixin.ssm.annotation.imp.CalculatorPureImpl.add(int,int))")
- public void printLogFinallyEnd() {
- System.out.println("[LogAspect --> 后置通知] 方法最终结束了");
- }
- }
- @Test
- public void testAnnotationAOP() {
- ApplicationContext ioc =
- new ClassPathXmlApplicationContext("aop-annotation.xml");
- Calculator calculator = ioc.getBean(Calculator.class);
- calculator.add(1, 2);
- // [LogAspect --> 前置通知] 方法执行了
- // 方法内部 result = 3
- // [LogAspect --> 返回通知] 方法成功返回了
- // [LogAspect --> 后置通知] 方法最终结束了
- }
各种通知的执行顺序:Spring版本 5.3.x 以前:前置通知目标操作后置通知返回通知或异常通知Spring版本5.3.x以后:前置通知目标操作返回通知或异常通知后置通知
*Service
上面例子表示匹配所有名称以Service结尾的类或接口
*Operation
上面例子表示匹配所有方法名以Operation结尾的方法
execution(public int *..*Service.*(.., int))
上面例子是对的,下面例子是错的:
execution(* int *..*Service.*(.., int))
但是public *表示权限修饰符明确,返回值任意是可以的。
- 对于execution()表达式整体可以使用三个逻辑运算符号
- execution() || execution()表示满足两个execution()中的任何一个即可
- execution() && execution()表示两个execution()表达式必须都满足
- !execution()表示不满足表达式的其他方法
TIP
虽然我们上面介绍过的切入点表达式语法细节很多,有很多变化,但是实际上具体在项目中应用时有比较固定的写法。
典型场景:在基于 XML 的声明式事务配置中需要指定切入点表达式。这个切入点表达式通常都会套用到所有 Service 类(接口)的所有方法。那么切入点表达式将如下所示:
execution(* *..*Service.*(..))
在一处声明切入点表达式之后,其他有需要的地方引用这个切入点表达式。易于维护,一处修改,处处生效。
声明方式如下:
- @Pointcut(value = "execution(* com.chenyixin.ssm.annotation.imp.CalculatorPureImpl.*(..))")
- public void pointCut() {}
- @Component
- @Aspect // @Aspect表示这个类是一个切面类
- public class LogAspect {
-
- @Pointcut(value = "execution(* com.chenyixin.ssm.annotation.imp.CalculatorPureImpl.*(..))")
- public void pointCut() {}
-
- // @Before注解:声明当前方法是前置通知方法
- // value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上
- @Before(value="pointCut()")
- public void add() {
- System.out.println("[LogAspect --> 前置通知] 方法执行了");
- }
-
- @AfterReturning("pointCut()")
- public void printLogAfterSuccess() {
- System.out.println("[LogAspect --> 返回通知] 方法成功返回了");
- }
-
- @AfterThrowing("pointCut()")
- public void printLogAfterException() {
- System.out.println("[LogAspect --> 异常通知] 方法抛异常了");
- }
-
- @After("pointCut()")
- public void printLogFinallyEnd() {
- System.out.println("[LogAspect --> 后置通知] 方法最终结束了");
- }
- }
- @Component
- @Aspect
- public class ValidateAspect {
-
- // 相当于调用 :全类名.方法名
- @Before(value="com.chenyixin.ssm.annotation.LogAspect.pointCut()")
- public void beforeMethod() {
-
- }
- }
而作为存放切入点表达式的类,可以把整个项目中所有切入点表达式全部集中过来,便于统一管理
- @Component
- public class AtguiguPointCut {
-
- @Pointcut(value = "execution(public int *..Calculator.sub(int,int))")
- public void atguiguGlobalPointCut(){}
-
- @Pointcut(value = "execution(public int *..Calculator.add(int,int))")
- public void atguiguSecondPointCut(){}
-
- @Pointcut(value = "execution(* *..*Service.*(..))")
- public void transactionPointCut(){}
- }
org.aspectj.lang.JoinPoint
- // @Before注解:声明当前方法是前置通知方法
- // value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上
- // 在前置通知方法形参位置声明一个JoinPoint类型的参数,Spring就会将这个对象传入
- // 根据JoinPoint对象就可以获取目标方法名称、实际参数列表
- @Before(value = "pointCut()")
- public void add(JoinPoint joinPoint) {
- // 1.通过JoinPoint对象获取目标方法签名对象
- // 方法的签名:一个方法的全部声明信息
- Signature signature = joinPoint.getSignature();
-
- // 2.通过方法的签名对象获取目标方法的详细信息
- // 获取方法名
- String name = signature.getName();
-
- int modifiers = signature.getModifiers();
- // System.out.println(modifiers);
- String declaringTypeName = signature.getDeclaringTypeName();
- // System.out.println(declaringTypeName);
- Class declaringType = signature.getDeclaringType();
- // System.out.println(declaringType);
-
- // 3.通过JoinPoint对象获取外界调用目标方法时传入的实参列表
- Object[] args = joinPoint.getArgs();
-
- System.out.println("[LogAspect --> 前置通知] 方法" + name + "执行了,参数为:" + Arrays.toString(args));
- }
在返回通知中,通过@AfterReturning注解的returning属性获取目标方法的返回值
- // @AfterReturning注解标记返回通知方法
- // 在返回通知中获取目标方法返回值分两步:
- // 第一步:在@AfterReturning注解中通过returning属性设置一个名称
- // 第二步:使用returning属性设置的名称在通知方法中声明一个对应的形参
- @AfterReturning(value = "pointCut()", returning = "result")
- public void printLogAfterSuccess(JoinPoint joinPoint, Object result) {
- String name = joinPoint.getSignature().getName();
- System.out.println("[LogAspect --> 返回通知] 方法" + name + "成功返回了,返回值为:" + result);
- }
在异常通知中,通过@AfterThrowing注解的throwing属性获取目标方法抛出的异常对象
- // @AfterThrowing注解标记异常通知方法
- // 在异常通知中获取目标方法抛出的异常分两步:
- // 第一步:在@AfterThrowing注解中声明一个throwing属性设定形参名称
- // 第二步:使用throwing属性指定的名称在通知方法声明形参,Spring会将目标方法抛出的异常对象从这里传给我们
- @AfterThrowing(value = "pointCut()", throwing = "exception")
- public void printLogAfterException(JoinPoint joinPoint, Exception exception) {
- String name = joinPoint.getSignature().getName();
- System.out.println("[LogAspect --> 异常通知] 方法" + name + "抛异常了,异常为:" + exception);
- }
- @Test
- public void testAnnotationAOP() {
- ApplicationContext ioc =
- new ClassPathXmlApplicationContext("aop-annotation.xml");
- Calculator calculator = ioc.getBean(Calculator.class);
- System.out.println("测试1:");
- calculator.add(1, 2);
- System.out.println();
- System.out.println("测试2:");
- calculator.div(1, 0);
- }
结果:
- //使用@Around注解标明环绕通知方法
- @Around("pointCut()")
- public Object aroundAdviceMethod(ProceedingJoinPoint joinPoint) {
- // 声明变量用来存储目标方法的返回值
- Object result = null;
- // 获取参数列表
- Object[] args = joinPoint.getArgs();
- try {
- // 在目标方法执行前执行
- System.out.println("环绕通知 --> 前置位置");
-
- // 过ProceedingJoinPoint对象调用目标方法
- // 目标方法的返回值一定要返回给外界调用者
- result = joinPoint.proceed(args);
-
- // 在目标方法成功返回后执行
- System.out.println("环绕通知 --> 返回位置");
-
- } catch (Throwable e) {
- // e.printStackTrace();
- // 在目标方法抛异常后执行
- System.out.println("环绕通知 --> 异常位置");
-
- }finally {
- // 在目标方法最终结束后执行
- System.out.println("环绕通知 --> 后置位置");
-
- }
- return result;
- }
- public class AOPTest {
-
- @Test
- public void testAnnotationAOP() {
- ApplicationContext ioc =
- new ClassPathXmlApplicationContext("aop-annotation.xml");
- Calculator calculator = ioc.getBean(Calculator.class);
- System.out.println("测试1:");
- calculator.add(1, 2);
- System.out.println();
- System.out.println("测试2:");
- calculator.div(1, 0);
- }
- }
相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。
使用 @Order 注解可以控制切面的优先级:
实际开发时,如果有多个切面嵌套的情况,要慎重考虑。例如:如果事务切面优先级高,那么在缓存中命中数据的情况下,事务切面的操作都浪费了。
此时应该将缓存切面的优先级提高,在事务操作之前先检查缓存中是否存在目标数据。
- @Component
- @Aspect
- @Order(1)
- public class ValidateAspect {
-
- // 相当于调用 :全类名.方法名
- @Before(value = "com.chenyixin.ssm.annotation.LogAspect.pointCut()")
- public void beforeMethod(JoinPoint joinPoint) {
- String name = joinPoint.getSignature().getName();
- List
- System.out.println("[LogAspect --> 前置通知] 方法" + name + "执行了,参数为:" + args);
-
- }
- }
- @Component
- @Aspect // @Aspect表示这个类是一个切面类
- @Order(2)
- public class LogAspect {...}
测试:
- @Test
- public void testAnnotationAOP() {
- ApplicationContext ioc =
- new ClassPathXmlApplicationContext("aop-annotation.xml");
- Calculator calculator = ioc.getBean(Calculator.class);
- calculator.add(1, 2);
-
- }
结果:
1、在切面中,需要通过指定的注解将方法标识为通知方法 * @Before:前置通知,在目标对象方法执行之前执行 * @After:后置通知,在目标对象方法的finally字句中执行 * @AfterReturning:返回通知,在目标对象方法返回值之后执行 * @AfterThrowing:异常通知,在目标对象方法的catch字句中执行 * * * 2、切入点表达式:设置在标识通知的注解的value属性中 * execution(public int com.atguigu.spring.aop.annotation.CalculatorImpl.add(int, int) * execution(* com.atguigu.spring.aop.annotation.CalculatorImpl.*(..) * 第一个*表示任意的访问修饰符和返回值类型 * 第二个*表示类中任意的方法 * ..表示任意的参数列表 * 类的地方也可以使用*,表示包下所有的类 * 3、重用切入点表达式 * //@Pointcut声明一个公共的切入点表达式 * @Pointcut("execution(* com.atguigu.spring.aop.annotation.CalculatorImpl.*(..))") * public void pointCut(){} * 使用方式:@Before("pointCut()") * * 4、获取连接点的信息 * 在通知方法的参数位置,设置JoinPoint类型的参数,就可以获取连接点所对应方法的信息 * //获取连接点所对应方法的签名信息 * Signature signature = joinPoint.getSignature(); * //获取连接点所对应方法的参数 * Object[] args = joinPoint.getArgs(); * * 5、切面的优先级 * 可以通过@Order注解的value属性设置优先级,默认值Integer的最大值 * @Order注解的value属性值越小,优先级越高注:在目标类没有实现任何接口的情况下,Spring会自动使用cglib技术实现代理。
接口:
- public interface Calculator {
- int add(int i, int j);
-
- int sub(int i, int j);
-
- int mul(int i, int j);
-
- int div(int i, int j);
- }
接口实现类:
- @Component
- public class CalculatorPureImpl implements Calculator {
- @Override
- public int add(int i, int j) {
-
- int result = i + j;
-
- System.out.println("方法内部 result = " + result);
-
- return result;
- }
-
- @Override
- public int sub(int i, int j) {
-
- int result = i - j;
-
- System.out.println("方法内部 result = " + result);
-
- return result;
- }
-
- @Override
- public int mul(int i, int j) {
-
- int result = i * j;
-
- System.out.println("方法内部 result = " + result);
-
- return result;
- }
-
- @Override
- public int div(int i, int j) {
-
- int result = i / j;
-
- System.out.println("方法内部 result = " + result);
-
- return result;
- }
- }
-
- @Component
- public class LogAspect {
-
- public void add(JoinPoint joinPoint) {
- // 1.通过JoinPoint对象获取目标方法签名对象
- // 方法的签名:一个方法的全部声明信息
- Signature signature = joinPoint.getSignature();
-
- // 2.通过方法的签名对象获取目标方法的详细信息
- // 获取方法名
- String name = signature.getName();
-
- int modifiers = signature.getModifiers();
- // System.out.println(modifiers);
- String declaringTypeName = signature.getDeclaringTypeName();
- // System.out.println(declaringTypeName);
- Class declaringType = signature.getDeclaringType();
- // System.out.println(declaringType);
-
- // 3.通过JoinPoint对象获取外界调用目标方法时传入的实参列表
- Object[] args = joinPoint.getArgs();
-
- System.out.println("[LogAspect --> 前置通知] 方法" + name + "执行了,参数为:" + Arrays.toString(args));
- }
-
- public void printLogAfterSuccess(JoinPoint joinPoint, Object result) {
- String name = joinPoint.getSignature().getName();
- System.out.println("[LogAspect --> 返回通知] 方法" + name + "成功返回了,返回值为:" + result);
- }
-
- public void printLogAfterException(JoinPoint joinPoint, Exception exception) {
- String name = joinPoint.getSignature().getName();
- System.out.println("[LogAspect --> 异常通知] 方法" + name + "抛异常了,异常为:" + exception);
- }
-
- public void printLogFinallyEnd(JoinPoint joinPoint) {
- String name = joinPoint.getSignature().getName();
- System.out.println("[LogAspect --> 后置通知] 方法" + name + "最终结束了");
- }
-
- public Object aroundAdviceMethod(ProceedingJoinPoint joinPoint) {
- // 声明变量用来存储目标方法的返回值
- Object result = null;
- // 获取参数列表
- Object[] args = joinPoint.getArgs();
- try {
- // 在目标方法执行前执行
- System.out.println("环绕通知 --> 前置位置");
-
- // 过ProceedingJoinPoint对象调用目标方法
- // 目标方法的返回值一定要返回给外界调用者
- result = joinPoint.proceed(args);
-
- // 在目标方法成功返回后执行
- System.out.println("环绕通知 --> 返回位置");
-
- } catch (Throwable e) {
- // e.printStackTrace();
- // 在目标方法抛异常后执行
- System.out.println("环绕通知 --> 异常位置");
-
- }finally {
- // 在目标方法最终结束后执行
- System.out.println("环绕通知 --> 后置位置");
-
- }
- return result;
- }
- }
- @Component
- public class ValidateAspect {
-
- public void beforeMethod(JoinPoint joinPoint) {
- String name = joinPoint.getSignature().getName();
- List
- System.out.println("[LogAspect --> 前置通知] 方法" + name + "执行了,参数为:" + args);
-
- }
- }
-
- <context:component-scan base-package="com.chenyixin.ssm.xml"/>
-
- <aop:config>
-
- <aop:pointcut id="pointcut" expression="execution(* com.chenyixin.ssm.xml.imp.CalculatorPureImpl.*(..))"/>
-
-
-
- <aop:aspect ref="logAspect" order="1">
-
-
-
- <aop:before method="add" pointcut-ref="pointcut"/>
-
-
-
- <aop:after-returning method="printLogAfterSuccess" pointcut-ref="pointcut"
- returning="result"/>
-
-
-
- <aop:after-throwing method="printLogAfterException" pointcut-ref="pointcut"
- throwing="exception"/>
-
-
- <aop:after method="printLogFinallyEnd" pointcut-ref="pointcut"/>
-
-
- <aop:around method="aroundAdviceMethod" pointcut-ref="pointcut"/>
- aop:aspect>
-
- <aop:aspect ref="validateAspect" order="2">
- <aop:before method="beforeMethod" pointcut-ref="pointcut"/>
- aop:aspect>
-
- aop:config>
测试:
- @Test
- public void testXmlAOP() {
- ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-xml.xml");
- Calculator calculator = ioc.getBean(Calculator.class);
- calculator.add(10, 30);
- }
结果:
测试:IOC容器中同类型的 bean 只有一个
正常获取到 IOC 容器中的那个 bean 对象
测试:IOC 容器中同类型的 bean 有多个
会抛出 NoUniqueBeanDefinitionException 异常,表示 IOC 容器中这个类型的 bean 有多个
测试:根据接口类型获取 bean
会抛出 NoUniqueBeanDefinitionException 异常,表示 IOC 容器中这个类型的 bean 有多个
测试:根据类获取bean
正常
原因分析:
从内存分析的角度来说,IOC容器中引用的是代理对象,代理对象引用的是目标对象。IOC容器并没有直接引用目标对象,所以根据目标类本身在IOC容器范围内查找不到。
debug查看代理类的类型:
debug查看实际类型:
自动装配需先从 IOC 容器中获取到唯一的一个 bean 才能够执行装配。所以装配能否成功和装配底层的原理,和前面测试的获取 bean 的机制是一致的。
测试:IOC容器中同类型的bean只有一个
正常装配
测试:IOC容器中同类型的bean有多个
会抛出NoUniqueBeanDefinitionException异常,表示IOC容器中这个类型的bean有多个
测试:根据接口类型装配bean
正常
测试:根据类装配bean
正常
测试:根据接口类型装配bean
@Autowired注解会先根据类型查找,此时会找到多个符合的bean,然后根据成员变量名作为bean的id进一步筛选,如果没有id匹配的,则会抛出NoUniqueBeanDefinitionException异常,表示IOC容器中这个类型的bean有多个
测试:根据类装配bean
正常
测试:根据接口类型装配bean
正常
测试:根据类装配bean
此时获取不到对应的bean,所以无法装配,抛出下面的异常:
Caused by: org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'fruitApple' is expected to be of type 'com.atguigu.bean.impl.FruitAppleImpl' but was actually of type 'com.sun.proxy.$Proxy15'
测试:根据类装配bean
正常