kim.zhang

风在前,无惧!


  • 首页

  • 标签42

  • 分类12

  • 归档94

  • 搜索

Spring AOP.md

发表于 2020-09-05 更新于 2021-11-21 分类于 Spring
本文字数: 7.2k 阅读时长 ≈ 7 分钟

通知的几种类型

  • 前置通知(Before)
  • 后置通知(AfterReturning)
  • 异常通知(AfterThrowing)
  • 最后通知(After):无论方法是正常结束,还是发生异常,都会执行。相当于写在finally块里的代码
  • 环绕通知(Around)

Spring AOP使用步骤

  1. 导入aop相关的包

    1
    2
    3
    org.springframework.spring-aop
    org.aspectj.aspectjweaver
    aopalliance.aopalliance
  2. 开启AOP自动代理、包扫描

    1
    2
    3
    4
    5
    <!-- 扫描注解 -->
    <context:component-scan base-package="com.imooc"/>

    <!-- 开启自动代理 -->
    <aop:aspectj-autoproxy/>
  3. 编写切面类,使用@Aspect标注,并加入IOC容器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    @Component
    @Aspect
    public class LogUtils {

    @Before("execution(public Integer com.imooc.demo3.Calculator.*(..))")
    public void before(){
    System.out.println("方法执行前before...");
    }

    @After("execution(public Integer com.imooc.demo3.Calculator.*(..))")
    public void after(){
    System.out.println("方法执行结束after...");
    }

    @AfterReturning("execution(public Integer com.imooc.demo3.Calculator.*(..))")
    public void afterReturning(){
    System.out.println("法正常返回AfterReturning...");
    }

    @AfterThrowing("execution(public Integer com.imooc.demo3.Calculator.*(..))")
    public void afterThrowing(){
    System.out.println("方法发生异常AfterThrowing:"+e.getMessage());
    }

    @Around("execution(public int com.imooc.demo3.Calculator.*(..))")
    public void around(){
    Object[] args = joinPoint.getArgs();

    Object proceed = null;
    try {
    System.out.println("[环绕通知]前置...");
    // 控制主方法的执行
    proceed = joinPoint.proceed(args);
    System.out.println("[环绕通知]后置...");
    } catch (Exception e) {
    System.out.println("[环绕中异常通知]...");
    // 环绕通知内发生的异常,如果捕获,需要再次抛出,方便下一个AfterThrowing捕获
    throw new RuntimeException("环绕内发生了异常");
    } finally {
    System.out.println("[环绕结束通知]...");
    }

    return proceed;
    }
    }

没有环绕通知的执行顺序

1
2
3
4
5
6
7
8
9
try{
@Before
// 方法执行
@AfterReturning
}catch (Exception e){
@AfterThrowing
}finally {
@After
}
  • 方法正常执行:@Before -> 正常方法执行 -> @After -> @AfterReturning
  • 方法发生异常:@Before -> 正常方法执行 -> @After -> @AfterThrowing(@AfterReturning执行)

注意的是先执行@After通知之后,再执行@AfterReturning通知。

环绕通知

环绕通知相当于拥有其他四种类型的通知,因为它可以拦截目标方法执行。

不调用Object obj = proceedingJoinPoint.proceed();即可拦截原有方法执行。

1
proceed = joinPoint.proceed(args);

这一行代码相当于JDK动态代理中的method.invoke(obj,args);在执行方法过后,要把方法返回的对象return。

环绕通知的执行顺序

环绕通知的执行顺序与其他通知的执行顺序有一点不同,下面代码的执行顺序是:

  • 方法正常执行:[环绕通知]前置… -> 正常方法执行 -> [环绕通知]后置… -> [环绕结束通知]…
  • 方法发生异常:[环绕通知]前置… -> 正常方法执行 -> 环绕内发生了异常 -> [环绕结束通知]…
1
2
3
4
5
6
7
8
9
10
11
12
try {
System.out.println("[环绕通知]前置...");
// 控制主方法的执行
proceed = joinPoint.proceed(args);
System.out.println("[环绕通知]后置...");
} catch (Exception e) {
System.out.println("[环绕中异常通知]...");
// 环绕通知内发生的异常,如果捕获,需要再次抛出,方便下一个AfterThrowing捕获
throw new RuntimeException("环绕内发生了异常");
} finally {
System.out.println("[环绕结束通知]...");
}

需要注意的一点是,如果环绕通知外还有异常通知,需要将异常再次抛出,否则外边的异常通知会失效。

有环绕通知的执行顺序

以上面的栗子为说明,如果具有环绕通知和其他通知,执行结果如下:

对比没有环绕通知的执行顺序,其实就是把【正常方法执行】这一步骤换成【环绕通知方法执行】,并且环绕通知的优先级高于其他通知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
=========方法正常结束===========
[环绕通知]前置...
方法执行前before...
[环绕通知]后置...
[环绕结束通知]...
方法执行结束after...
方法正常返回AfterReturning...
=========方法发生异常===========
[环绕通知]前置...
方法执行前before...
[环绕中异常通知]...
[环绕结束通知]...
方法执行结束after...
方法发生异常AfterThrowing:环绕内发生了异常

多个切面的通知执行顺序

如果有多个切面切一个方法,切面类是有执行顺序的,其就像一个同心圆,由外到里,再由里到外地执行。

一般来说,会按照切面类类命的首字母来决定执行顺序。但是,可以通过@Order注解来指定优先级。值越小的优先级越大。如果是xml的配置方式,可以通过在xml文件中配置切面类的顺序来控制切面类的执行顺序,当然也可以通过order属性来指定。

JoinPoint获取目标方法的信息

  • 除了环绕通知之外的其他通知,可以在切面方法中使用JoinPoint来获取目标方法的信息。环绕通知可以使用ProceedingJoinPoint来获取目标方法的信息。JointPoint是Spring内置的,无需在注解中指定。

  • 1
    2
    3
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    Object[] args = joinPoint.getArgs();
    }
  • 对于@AfterReturning,还可以在注解中使用returning指定一个变量,并将变量绑定到方法参数,即可接收目标方法返回值

  • 1
    2
    3
    4
    5
    @AfterReturning(value = "execution(public int com.imooc.demo3.Calculator.*(..))",returning = "result")
    public void afterReturning(JoinPoint joinPoint,Object result){
    // 获取方法名
    Signature signature = joinPoint.getSignature();
    }
  • 对于@AfterThrowing,还可以在注解中使用throwing指定一个变量,并将变量绑定到方法参数,即可接收目标方法异常信息

  • 1
    2
    3
    4
    @AfterThrowing(value = "execution(public int com.imooc.demo3.Calculator.*(..))",throwing = "e")
    public void afterThrowing(Exception e){
    System.out.println("方法发生异常AfterThrowing:"+e.getMessage());
    }
  • 直接输出JoinPoint可以获得切入点表达式的信息

PointCut抽取共同的连接点

随便定义一个空方法,使用@PointCut来定义切入点表达式

1
2
@Pointcut(value = "execution(public int com.imooc.demo3.Calculator.*(..))")
private void pointCut(){};

使用这个共用的切入点表达式如下:

1
2
3
4
5
6
7
8
9
10
@Before("pointCut()")
public void before(){
System.out.println("方法执行前before...");
}
@AfterReturning(value = "pointCut()",returning = "result")
public void afterReturning(JoinPoint joinPoint,Object result){
// 获取方法名
Signature signature = joinPoint.getSignature();
System.out.println("方法正常返回AfterReturning...");
}

注意,如果想在其他类使用这个切入点表达式,需要写全限定类名。

1
2
3
4
@Before("com.imooc.demo3.Calculator.pointCut()")
public void before(){
System.out.println("方法执行前before...");
}

xml配置AOP

  • 将被代理的对象和切面类加入到IOC容器中
  • 配置AOP切面类,切入点等信息

需要注意的是配置通知方法时,不需要加()。比如before而不是before()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!--被代理对象加入容器-->
<bean id="calculator" class="com.imooc.demo3.Calculator"/>

<!--切面类加入容器-->
<bean id="logUtils" class="com.imooc.demo3.LogUtils"/>

<aop:config>
<!--配置在<aop:aspect>标签外,可以全局使用-->
<aop:pointcut id="pointCut" expression="execution(public int com.imooc.demo3.Calculator.*(..))"/>
<!--指定切面类-->
<aop:aspect ref="logUtils">
<aop:before method="before" pointcut-ref="pointCut"/>
<aop:after method="after" pointcut-ref="pointCut"/>
<aop:after-returning method="afterReturning" pointcut-ref="pointCut" returning="result"/>
<aop:after-throwing method="afterThrowing" pointcut-ref="pointCut" throwing="e"/>
<aop:around method="around" pointcut-ref="pointCut"/>
</aop:aspect>
</aop:config>

踩坑1:AOP不生效?

1
2
3
4
5
6
7
8
9
10
11
@ContextConfiguration("classpath:applicationContext.xml")
@RunWith(SpringJUnit4ClassRunner.class)
public class CalculatorTest {
@Test
public void calculatorTest(){
// 踩坑1:必须从ioc容器中取出对象才可以
Calculator calculator = new Calculator();
System.out.println(calculator.add(1, 2));
System.out.println(calculator.div(2, 0));
}
}

执行以上单元测试代码,AOP好像不生效。原因在于第7行代码,Calculator不是从容器中获取的,自然不能使用Spring提供的AOP功能。

踩坑2:IOC容器中保存的是代理对象

从坑1爬出来之后,自然想到注入Calculator。

1
2
3
4
5
6
7
8
9
10
11
12
13
@ContextConfiguration("classpath:applicationContext.xml")
@RunWith(SpringJUnit4ClassRunner.class)
public class CalculatorTest {

@Autowired
private Calculator calculator;

@Test
public void calculatorTest(){
System.out.println(calculator.add(1, 2));
System.out.println(calculator.div(2, 0));
}
}

此时执行会报错:

1
org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'calculator' is expected to be of type 'com.imooc.demo3.Calculator' but was actually of type 'com.sun.proxy.$Proxy26'

可以看到,AOP的底层是代理,Calculator经过AOP代理之后,真正保存的是com.sun.proxy.$Proxy26代理对象。可以通过以下代码验证:

1
2
System.out.println(calculator);// com.imooc.demo3.Calculator@69c79f09
System.out.println(calculator.getClass());// class com.sun.proxy.$Proxy26

而代理对象和被代理对象,唯一的共同点就是实现了同一个接口Calculete。我们使用接口接收注入的对象:

1
2
@Autowired
private Calculate calculator;

踩坑3:基本类型引发的错误

在注入Calculate接口后,本以为会一帆风顺,但是结果往往很现实:

1
Null return value from advice does not match primitive return type for: public abstract int com.imooc.demo3.Calculate.add(int,int)

发生这个错误的原因在于,Calculate接口规定的add方法返回值是int的基本类型,而切面在执行before方法时,返回值类型是void,切面会返回null类型。这就导致了基本类型和null不可兼容的错误。

1
2
3
4
@Before("execution(public Integer com.imooc.demo3.Calculator.*(..))")
public void before(){
System.out.println("方法开始");
}

一种简单的解决方案是将Calculate接口的返回值类型修改为包装类,因为包装类与null是兼容的。同时要注意,修改了接口的返回值,要检查切入点表达式是否满足切入条件。

一毛也是爱~
Kim.Zhang 微信支付

微信支付

# AOP
JDK动态代理.md
声明性事务.md
  • 文章目录
  • 站点概览
Kim.Zhang

Kim.Zhang

且行且珍惜
94 日志
12 分类
42 标签
E-Mail Weibo
  1. 1. 通知的几种类型
  2. 2. Spring AOP使用步骤
  3. 3. 没有环绕通知的执行顺序
  4. 4. 环绕通知
  5. 5. 环绕通知的执行顺序
  6. 6. 有环绕通知的执行顺序
  7. 7. 多个切面的通知执行顺序
  8. 8. JoinPoint获取目标方法的信息
  9. 9. PointCut抽取共同的连接点
  10. 10. xml配置AOP
  11. 11. 踩坑1:AOP不生效?
  12. 12. 踩坑2:IOC容器中保存的是代理对象
  13. 13. 踩坑3:基本类型引发的错误
粵ICP备19091267号 © 2019 – 2022 Kim.Zhang | 629k | 9:32
本站总访问量 4 次 | 有 309 人看我的博客啦 |
博客全站共176.7k字
载入天数...载入时分秒...
0%