Spring 之 AOP 的基础使用

大纲

AOP 的常用注解

  • @Before:前置通知,在目标方法执行之前执行
  • @After:后置通知,在目标方法执行之后执行(始终会执行)
  • @AfterReturning:后置返回后通知,在目标方法正常返回后执行(发生异常不会执行)
  • @AfterThrowing:后置异常通知,在目标方法抛出异常后执行
  • @Around: 环绕通知,可以在目标方法执行前后执行自定义逻辑
  • @Pointcut:定义切入点,指定在哪些连接点上应用切面的逻辑
  • @Aspect:定义切面,通常与 @Component 结合使用,将其标记为 Spring 容器中的一个 Bean

Spring 中的 AOP 注解使用

AOP 案例代码

  • 引入核心依赖
1
2
3
4
5
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
  • 定义服务类
1
2
3
4
5
6
7
8
9
10
11
@Slf4j
@Service
public class MyService {

public int div(int x, int y) {
int result = x / y;
log.info("result = {}", result);
return result;
}

}
  • 定义切面类
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
@Slf4j
@Aspect
@Component
public class MyAspect {

@Before("execution(public int com.java.interview.spring.aop.MyService.*(..))")
private void beforeNotify() {
log.info("@Before ...");
}

@After("execution(public int com.java.interview.spring.aop.MyService.*(..))")
private void afterNotify() {
log.info("@After ...");
}

@AfterReturning("execution(public int com.java.interview.spring.aop.MyService.*(..))")
private void afterReturningNotify() {
log.info("@AfterReturning ...");
}

@AfterThrowing(pointcut = "execution(public int com.java.interview.spring.aop.MyService.*(..))", throwing = "exception")
private void afterThrowingNotify(Exception exception) {
log.info("@AfterThrowing ...");
}

@Around("execution(public int com.java.interview.spring.aop.MyService.*(..))")
private Object afterAroundNotify(ProceedingJoinPoint joinPoint) {
Object result = null;
log.info("@Around before ...");
try {
// 执行目标方法
result = joinPoint.proceed();
} catch (Throwable throwable) {
log.error("@Around exception : {}", throwable.getMessage());
// 抛出异常,否则不会触发 @AfterThrowing
throw new RuntimeException(throwable);
}
log.info("@Around after ...");
return result;
}

}
  • 定义测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 这里直接基于 SpringBootTest 进行单元测试
*/
@EnableAspectJAutoProxy
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {MainTestApplication.class})
public class MainTestApplication {

@Autowired
private MyService myService;

@Test
public void test() {
myService.div(9, 1);
}

}

AOP 测试结果

特别注意,Spring 4.x 与 Spring 5.x 的 AOP 通知顺序是不一样的。

Spring 4.x 与 SpringBoot 1.x

注意

这里使用的 Spring 版本是 4.3.13.RELEASE,而且 SpringBoot 的版本是 1.5.9.RELEASE

执行上述代码,输出的结果如下:

1
2
3
4
5
6
2019-05-13 20:48:17.135  INFO 9045 --- [           main] com.java.interview.spring.aop.MyAspect   : @Around before ...
2019-05-13 20:48:17.135 INFO 9045 --- [ main] com.java.interview.spring.aop.MyAspect : @Before ...
2019-05-13 20:48:17.157 INFO 9045 --- [ main] com.java.interview.spring.aop.MyService : result = 9
2019-05-13 20:48:17.159 INFO 9045 --- [ main] com.java.interview.spring.aop.MyAspect : @Around after ...
2019-05-13 20:48:17.160 INFO 9045 --- [ main] com.java.interview.spring.aop.MyAspect : @After ...
2019-05-13 20:48:17.160 INFO 9045 --- [ main] com.java.interview.spring.aop.MyAspect : @AfterReturning ...

此时,如果将上述代码中 myService.div(9, 1) 更改为 myService.div(9, 0),刻意让 Java 代码抛出异常,输出的结果如下:

1
2
3
4
5
2019-05-13 20:59:38.560  INFO 12634 --- [           main] com.java.interview.spring.aop.MyAspect   : @Around before ...
2019-05-13 20:59:38.560 INFO 12634 --- [ main] com.java.interview.spring.aop.MyAspect : @Before ...
2019-05-13 20:59:38.578 ERROR 12634 --- [ main] com.java.interview.spring.aop.MyAspect : @Around exception : / by zero
2019-05-13 20:59:38.580 INFO 12634 --- [ main] com.java.interview.spring.aop.MyAspect : @After ...
2019-05-13 20:59:38.580 INFO 12634 --- [ main] com.java.interview.spring.aop.MyAspect : @AfterThrowing ...

Spring 4.x 的部分 AOP 通知顺序总结

  • 正常执行:@Around(环绕通知 - 前半部分) > @Before(前置通知)> @Around(环绕通知 - 后半部分) > @After(后置通知)> @AfterReturning(后置返回后通知)
  • 异常执行:@Around(环绕通知 - 前半部分) > @Before(前置通知)> @After(后置通知)> @AfterThrowing(后置异常通知)。特别注意,当发生异常时,@Around(环绕通知 - 后半部分) 会不生效。

Spring 5.x 与 SpringBoot 2.x

注意

这里使用的 Spring 版本是 5.2.8.RELEASE,而且 SpringBoot 的版本是 2.3.3.RELEASE

执行上述代码,输出的结果如下:

1
2
3
4
5
6
2019-05-13 21:35:33.890  INFO 31351 --- [           main] com.java.interview.spring.aop.MyAspect   : @Around before ...
2019-05-13 21:35:33.891 INFO 31351 --- [ main] com.java.interview.spring.aop.MyAspect : @Before ...
2019-05-13 21:35:33.906 INFO 31351 --- [ main] com.java.interview.spring.aop.MyService : result = 9
2019-05-13 21:35:33.907 INFO 31351 --- [ main] com.java.interview.spring.aop.MyAspect : @AfterReturning ...
2019-05-13 21:35:33.907 INFO 31351 --- [ main] com.java.interview.spring.aop.MyAspect : @After ...
2019-05-13 21:35:33.907 INFO 31351 --- [ main] com.java.interview.spring.aop.MyAspect : @Around after ...

此时,如果将上述代码中 myService.div(9, 1) 更改为 myService.div(9, 0),刻意让 Java 代码抛出异常,输出的结果如下:

1
2
3
4
5
2019-05-13 21:36:38.212  INFO 31712 --- [           main] com.java.interview.spring.aop.MyAspect   : @Around before ...
2019-05-13 21:36:38.212 INFO 31712 --- [ main] com.java.interview.spring.aop.MyAspect : @Before ...
2019-05-13 21:36:38.231 INFO 31712 --- [ main] com.java.interview.spring.aop.MyAspect : @AfterThrowing ...
2019-05-13 21:36:38.233 INFO 31712 --- [ main] com.java.interview.spring.aop.MyAspect : @After ...
2019-05-13 21:36:38.233 ERROR 31712 --- [ main] com.java.interview.spring.aop.MyAspect : @Around exception : / by zero

Spring 5.x 的部分 AOP 通知顺序总结

  • 正常执行:@Around(环绕通知 - 前半部分) > @Before(前置通知)> @AfterReturning(后置返回后通知)> @After(后置通知) > @Around(环绕通知 - 后半部分)
  • 异常执行:@Around(环绕通知 - 前半部分) > @Before(前置通知)> @AfterThrowing(后置异常通知)> @After(后置通知)。特别注意,当发生异常时,@Around(环绕通知 - 后半部分) 会不生效。

AOP 通知顺序总结

SpringBoot 1 使用的 Spring 版本是 4.x,在这个版本中的 AOP 通知顺序为:

  • (1) 环绕通知(Around advice)
  • (2) 前置通知(Before advice)
  • (3) 后置通知(After advice)
  • (4) 后置返回通知(After-returning advice)
  • (5) 后置异常通知(After-throwing advice)

SpringBoot 2 使用的 Spring 版本是 5.x,在这个版本中的 AOP 通知顺序为:

  • (1) 环绕通知(Around advice)
  • (2) 前置通知(Before advice)
  • (3) 后置返回通知(After-returning advice)
  • (4) 后置异常通知(After-throwing advice)
  • (5) 后置通知(After advice)

特别注意

Spring 4 和 Spring 5 的 AOP 通知顺序是不一样的,这一点必须记住。

Spring 中调用内部方法 AOP 不生效

在 Spring 中,类自身调用内部方法时,会出现 AOP 不生效的问题。基于上述案例,更改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Slf4j
@Service
public class MyService {

public int div(int x, int y) {
int result = x / y;
log.info("div result is {}", result);

// 当 @EnableAspectJAutoProxy 注解的 exposeProxy 属性为 false 时(默认),这里直接调用 multiply() 内部方法,并不会发生代理调用
this.multiply(x, y);

return result;
}

public int multiply(int x, int y) {
int result = x * y;
log.info("multiply result is {}", result);
return result;
}

}

可以发现,在调用 div() 方法时,可以发生代理调用,但是在 div() 方法中调用内部方法 multiply() 时,multiply() 方法并不会发生代理调用,因为 this 并非是代理对象(去掉 this 代理也不会生效)。解决方法是将代理对象暴露出去,然后在代码中通过 AopContext 获取当前的代理对象(底层使用了 ThreadLocal 来实现),最后再使用代理对象调用内部方法。基于上述案例,更改后的代码如下:

1
2
3
4
5
6
7
// 通过 @EnableAspectJAutoProxy 注解 exposeProxy 属性将代理对象暴露出去
@EnableAspectJAutoProxy(exposeProxy = true)
public class MainTestApplication {

......

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Slf4j
@Service
public class MyService {

public int div(int x, int y) {
int result = x / y;
log.info("div result is {}", result);

// 当 @EnableAspectJAutoProxy 注解的 exposeProxy 属性为 true 时,可以通过 AopContext 获取当前的代理对象,然后再调用内部方法
MyService proxyInstance = (MyService) AopContext.currentProxy();
proxyInstance.multiply(x, y);

return result;
}

public int multiply(int x, int y) {
int result = x * y;
log.info("multiply result is {}", result);
return result;
}

}

Spring 事务不能回滚的深层次原因

Spring 中 AOP 默认使用哪种代理方式

在 Spring 5.x 中,如果目标对象实现了接口,默认会使用 JDK 动态代理;如果目标对象没有实现接口,则会使用 Cglib 代理。底层源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}

值得一提的是,从 SpringBoot 2.x 开始,为了解决使用 JDK 动态代理可能导致的类型转换异常,默认使用的是 Cglib 代理。如果需要让 SpringBoot 2.x 默认使用 JDK 动态代理,可以通过配置项 spring.aop.proxy-target-class = false 来进行指定。