SpringBoot AOP 实践与原理

前言

这篇博客先从AOP的代码编写讲起,接着介绍AOP的实现原理,其中包括动态代理的概念,最后介绍Spring中可以直接用的AOP注解。

代码实践

添加依赖包

1
2
3
4
5
<!-- AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

创建切面类

1
2
3
4
5
@Component //声明这是一个组件
@Aspect //声明这是一个切面
public class MainAspect {

}

定义切入点

切入点就是你要在哪个包下的哪个类的哪个方法执行切面逻辑,或者说指定切入哪里。Spring AOP的切入点只能是方法。
可以在一个切面类中定义多个切入点,再给每个切入点指定不同的Advice方法(Advice方法介绍见下一节)。
定义切入点有两种方式:

  1. 用execution表达式定义切入点
  2. 用自定义注解定义切入点

用execution表达式定义切入点

1
2
3
@Pointcut("execution(* com.sample.service.impl..*.*(..))")
public void pointcut() {
}

这个示例表示在com.sample.service.impl包(含子包)下的所有类的所有方法切入。下面把这个示例拆解看看execution表达式的格式。

符号 含义
第一个”*”符号 表示返回值的类型任意
com.sample.service.impl 要切入的类所在包名
包名后面的”..” 表示当前包及子包
第二个”*“符号 表示类名,*即任意类,也可指定具体的类,或带前后缀的类,如 *Service
.*(..) 表示任何方法名,括号表示参数,两个点表示任何参数类型

如何使用这个切入点?示例:

1
2
3
4
@Before("pointcut()")// 这个注解以及它所修饰的方法的介绍见下一节
public void doBefore() {
// 方法逻辑...
}

若要同时指定多个切入点,用这个方法:

1
2
3
4
@Before("pointcut1() || pointcut2()")
public void doBefore() {
// 方法逻辑...
}

用自定义注解定义切入点

假设已有自定义注解:

1
2
3
4
5
6
7
@Documented
@Target(ElementType.METHOD)
@Inherited
@Retention(RetentionPolicy.RUNTIME )
public @interface Log {
String value() default "";
}

定义切入点:

1
2
3
@Pointcut("@annotation(com.example.demo.aop.Log)") // 自定义注解的限定类名
public void pointcut() {
}

这个示例表示切入所有加了@Log属性的方法,比如下面这个方法:

1
2
3
4
5
6
7
8
@RestController
public class UserController {
@Log("添加用户操作")
@RequestMapping("/user/add")
public String add() {
return "add";
}
}

使用这个切入点的方法和用“execution表达式”定义的切入点没有区别:

1
2
3
4
@Before("pointcut()")// 这个注解以及它所修饰的方法的介绍见下一节
public void doBefore() {
// 方法逻辑...
}

但这只是“自定义注解”定义的切入点的其中一种使用方式,这种使用方式的一个缺点是不能访问自定义注解@Log的value属性。
另一种使用方式:

1
2
3
4
5
@Before("@annotation(log)") // 括号里的名字和注解参数名必须相同
public void doBefore(JoinPoint joinPoint, Log log) {
// 方法逻辑...
System.out.println(log.value()); // 可以访问注解属性值
}

这种使用方式的区别就是,切入点直接写在@Before注解中,不需要再用pointcut()方法定义切入点

和execution表达式相比,用自定义注解定义切入点,可以灵活安排切入的方法(想切哪里注解就加在哪里),且通过自定义注解的属性,在切面类中也能了解到被切方法的业务逻辑,若要在切面类中统一打印被切方法的日志,这点很好用。

根据业务逻辑编写Advice方法

Advice,有翻译为“增强处理”,也有翻译为“通知”,本质含义就是要执行的切面逻辑,如要在每个方法开始前打印入参日志,就可以编写Before类型的Advice方法,如要在每个方法抛出异常后统一处理,就可以编写AfterThrowing类型的Advice方法。Advice一共有五种类型:

  • Before
  • After
  • AfterReturning
  • AfterThrowing
  • Around

这些类型的共同特点:

  • 所有类型的注解都有两个属性:value、argNames。value属性用于指定切入点,argNames属性可以用来访问目标方法的入参
  • Around之外,其他四种类型的方法的连接点参数都只能是JoinPoint,不能是ProceedingJoinPoint(ProceedingJoinPoint是JoinPoint的子类)
  • 只有Around方法能改变目标方法的入参和返回值

下面分别介绍这五种类型的特点和使用方法。

Before

在目标方法执行前织入,不能访问目标方法的返回值,可选参数有JoinPoint。
@Before注解属性:value、argNames

JoinPoint简单介绍:
JoinPoint参数不必须,但需要时必须作为第一个参数!
常用方法有:
Object[] getArgs:返回目标方法的参数
Signature getSignature:返回目标方法的签名(含方法名和参数表)

示例:

1
2
3
4
5
6
7
8
9
10
11
@Before("pointcut()")
public void doBefore(JoinPoint joinPoint) {
System.out.println("============before============");
System.out.println("目标方法名为:" + joinPoint.getSignature().getName()); // 打印方法名add
System.out.println("目标方法所属类的类名:" + joinPoint.getSignature().getDeclaringTypeName());// 打印全限定类名com.example.demo.controller.UserController
//获取传入目标方法的参数
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
System.out.println("第" + (i+1) + "个参数为:" + args[i]);
}
}

After

在目标方法结束后织入,不管目标方法如何结束(正常还是异常),它都会被织入,可选参数有JoinPoint。
@After注解和@Before注解属性相同:value、argNames
示例:

1
2
3
4
5
@After("pointcut()")
public void doAfter(JoinPoint joinPoint) {
System.out.println("============after============");
// joinPoint参数的使用和Before一样,不再赘述
}

AfterReturning

在目标方法正常完成后被织入,抛出异常了不织入,可选参数有JoinPoint和Object(目标方法的返回值)。
@AfterReturning注解除了value、argNames这两个属性外,还有一个属性:

  • returning:指定一个返回值形参名,可以通过该形参名来访问目标方法的返回值,但不可修改目标方法的返回值

示例:

1
2
3
4
5
6
7
@AfterReturning(pointcut = "pointcut()", returning = "returnObject")
public void doAfterReturning(JoinPoint joinPoint, Object returnObject) { // 返回值的形参名与注解中的保持一致
System.out.println("============afterReturning============");
// joinPoint参数的使用和Before一样,不再赘述
// 访问目标方法的返回值
System.out.println("返回值:" + returnObject);
}

AfterThrowing

在目标方法抛出异常时织入,正常完成不织入,可选参数有JoinPoint和Throwable(目标方法抛出的异常)。
@AfterThrowing注解除了value、argNames这两个属性外,还有一个属性:

  • throwing:指定一个异常形参名,形参可用于访问目标方法抛出的异常

示例:

1
2
3
4
5
6
7
@AfterThrowing(throwing = "e", pointcut = "pointcut()")
public void doAfterThrowing(JoinPoint joinPoint, Throwable e) { // 异常的形参名与注解中的保持一致
System.out.println("============afterThrowing============");
// joinPoint参数的使用和Before一样,不再赘述
// 打印异常
System.out.println(e.getMessage());
}

需要注意的是,这个AfterThrowing只能用在打印异常信息,不能对抛出的异常做更多处理,也不能针对异常来改变目标方法的返回值。
想要根据异常信息修改目标方法返回值,只能用下面讲的Around。

Around

  • 可以在执行目标方法前织入,也可以在执行后织入
  • 可以决定目标方法在什么时候执行,如何执行,可以完全阻止目标方法的执行
  • 可以修改目标方法的参数值,也可以修改目标方法的返回值
  • 至少包含一个参数,且第一个参数必须是ProceedingJoinPoint
  • 在方法体内,调用ProceedingJoinPoint的proceed()方法才会执行目标方法。如果方法体内没有调用这个proceed()方法,则目标方法不会执行
  • 最后必须把获得的目标方法的返回值,作为@Around方法的返回值return回去(因为如果无返回值的话,将不会继续执行目标方法)

@Around注解属性:value、argNames

第一个示例,没修改目标方法入参:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  @Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
String result = "success"; // 返回值
try {
System.out.println("============around start============");
//获取传入目标方法的参数
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
System.out.println("第" + (i+1) + "个参数为:" + args[i]);
}

// 返回值
Object re = joinPoint.proceed();
if(re instanceof String) result = (String) re;
return result;
} catch (Throwable e) { // 目标方法抛出异常
e.printStackTrace();
result = "fail"; // 返回fail响应
return result;
} finally {
System.out.println("============around end============");
}
}

第二个示例,修改了目标方法入参(入参为基本类型和String):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
String result = "success"; // 返回值
try {
System.out.println("============around start============");
//获取传入目标方法的参数
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
// 修改参数值
args[i] += "updated";
}

// 返回值
Object re = joinPoint.proceed(args); // 这里要把修改后的args作为proceed方法的参数
if(re instanceof String) result = (String) re;
return result;
} catch (Throwable e) { // 目标方法抛出异常
e.printStackTrace();
result = "fail"; // 返回fail响应
return result;
} finally {
System.out.println("============around end============");
}
}

第三个示例,修改了目标方法入参(入参为对象):

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
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
String result = "success"; // 返回值
try {
System.out.println("============around start============");
//获取传入目标方法的参数
Object[] args = joinPoint.getArgs();
Object param = args[0];
Person person = new Person();
if(param instanceof Person)
person = (Person) param;
person.setAddress("address"); // 修改参数对象属性

// 返回值
Object re = joinPoint.proceed(); // 这里不用再把修改后的args作为proceed方法的参数
if(re instanceof String) result = (String) re;
return result;
} catch (Throwable e) { // 目标方法抛出异常
e.printStackTrace();
result = "fail"; // 返回fail响应
return result;
} finally {
System.out.println("============around end============");
}
}

注意,关于Around中的异常捕获:

  1. 只有从目标方法中抛出的异常才会被捕获,若目标方法内自己try-catch异常了没有抛出,就不会触发Around的异常捕获
  2. Around中处理异常,返回的响应类型必须和目标方法声明的一致,即必须是目标方法的返回类及其子类,否则会出现强制转换报错

AOP原理

AOP的全称是Aspect Oriented Programming,面向切面编程,它是通过动态代理技术,将Aspect方法中的逻辑完整织入到切入点中。下面开始先介绍静态代理(即设计模式中的代理模式),再介绍动态代理,最后再说说Spring是怎么用动态代理实现AOP的。

静态代理

在设计模式中,常用到代理模式,它一般由一个接口和这个接口的两个实现类组成。
接口:

1
2
3
public interface Service {
public void printMessage(String msg);
}

被代理类:

1
2
3
4
5
6
public class ServiceImpl implements Service {
@Override
public void printMessage(String msg) {
System.out.println("message: " + msg);
}
}

代理类:

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
public class ServiceProxy implements Service {
private Service service; // 被代理对象

public ServiceProxy(Service service) {
super();
this.service = service;
}

@Override
public void printMessage(String msg) {
doBefore();
this.service.printMessage(msg);
doAfter();
}

/**
* 执行代理方法前的处理(前置处理)
*/
private void doBefore() {
System.out.println("========proxy start========");
}
/**
* 执行完代理方法后的处理(后置处理)
*/
private void doAfter() {
System.out.println("========proxy end========");
}

}

使用代理类是这样的:

1
2
3
4
5
6
7
public class MainTest {
public static void main(String[] args) {
Service service = new ServiceImpl();
ServiceProxy proxy = new ServiceProxy(service);
proxy.printMessage("hello");
}
}

可以看出,静态代理需要我们自己编写代理类ServiceProxy,在它的printMessage方法中加上前置处理和后置处理。而动态代理,就不用我们自己写代理类,就能把我们指定的前置后置处理方法加到被代理方法的前后流程中。下面介绍动态代理的实现。

动态代理

动态代理,简单地说,就是不用自己写代理类,而是在运行时自动生成代理类和代理类对象。
接口Service和实现类ServiceImpl代码同上,不再赘述。

动态代理类:
作用:在运行时生成被代理类对象,规定执行被代理对象的目标方法的流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DynamicProxy implements InvocationHandler {

private Object _obj = null; // 被代理对象

public DynamicProxy(Object _obj) {
super();
this._obj = _obj;
}

/**
* @param proxy 代理对象
* @param method 被代理对象的目标方法
* @param args 被代理对象的目标方法的参数
* @return 被代理对象的目标方法的执行结果
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 此处可以编写前置处理...
return method.invoke(_obj, args); // 反射执行被代理对象的目标方法
}

}

使用动态代理类是这样的:

1
2
3
4
5
6
7
8
9
10
public class MainTest {
public static void main(String[] args) {
Service service = new ServiceImpl();
DynamicProxy dynamicProxy = new DynamicProxy(service); // 放入被代理对象
// 使用JDK中的Proxy类生成Service类的代理对象
Service proxy = (Service) Proxy.newProxyInstance(service.getClass().getClassLoader(),
service.getClass().getInterfaces(), dynamicProxy);
proxy.printMessage("hello"); // 这一步其实就是执行dynamicProxy.invoke方法
}
}

以上代码很简单地实现了动态代理:自己不用写Service类的代理类,运行时才生成Service类的代理类和代理对象。

Proxy.newProxyInstance 生成代理类的原理在这篇博客: https://blog.csdn.net/weixin_45505313/article/details/106399906
大致的原理就是,代理类由代理类工厂 ProxyClassFactory 调用 native 方法生成代理类的 .class 文件,放入内存,这样不需要重复生成。生成的代理类的名字是‘包名+$Proxy+id’

当然AOP的实现没有这么简单,它还需要把切面类织入到切入点中。

AOP的动态代理

以下代码可以简单实现AOP逻辑。
接口Service和实现类ServiceImpl代码同上,不再赘述。

切面接口:

1
2
3
4
5
6
7
8
9
public interface Aspect {
public void doBefore();
public void doAfter();
public void doAfterReturning();
public void doAfterThrowing();
public Object doAround(Object target, Method method, Object[] args)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;
public boolean useAround();
}

切面类:

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
public class AspectImpl implements Aspect{
@Override
public void doBefore() {
System.out.println("========doBefore========");
}

@Override
public void doAfter() {
System.out.println("========doAfter========");
}

@Override
public void doAfterReturning() {
System.out.println("========doAfterReturning========");
}

@Override
public void doAfterThrowing() {
System.out.println("========doAfterThrowing========");
}

@Override
public Object doAround(Object target, Method method, Object[] args)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
System.out.println("========doAround Start========");
Object result = method.invoke(target, args);
System.out.println("========doAround End========");
return result;
}

@Override
public boolean useAround() {
return true;
}

}

动态代理类:

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
46
47
48
49
public class DynamicProxy implements InvocationHandler {

private Object _obj = null; // 被代理对象

private Aspect _aspect; // 切面类

public Object getProxy(Object _obj, Aspect _aspect) {
this._obj = _obj;
this._aspect = _aspect;
Object proxy = Proxy.newProxyInstance(_obj.getClass().getClassLoader(), _obj.getClass().getInterfaces(), this); // 代理对象
return proxy;
}

/**
* @param proxy
* 代理对象
* @param method
* 被代理对象的目标方法
* @param args
* 被代理对象的目标方法的参数
* @return 被代理对象的目标方法的执行结果
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
boolean hasException = false;
Object result = null;
try {
_aspect.doBefore();
if (_aspect.useAround()) {
result = _aspect.doAround(_obj, method, args);
} else {
result = method.invoke(_obj, args);
}

} catch (Exception e) {
hasException = true;
}

_aspect.doAfter();
if (hasException) {
_aspect.doAfterThrowing();
} else {
_aspect.doAfterReturning();
}

return result;
}

}

使用动态代理类是这样的:

1
2
3
4
5
6
7
8
public class MainTest {
public static void main(String[] args) {
Service service = new ServiceImpl();
DynamicProxy dynamicProxy = new DynamicProxy();
Service proxy = (Service) dynamicProxy.getProxy(service, new AspectImpl());
proxy.printMessage("hello");
}
}

打印结果:

1
2
3
4
5
6
========doBefore========
========doAround Start========
message: hello
========doAround End========
========doAfter========
========doAfterReturning========

可以看出,以上代码成功把切面类AspectImpl的逻辑织入到了被代理对象的目标方法中。因此,Spring AOP的原理就是,Spring中有一个类似于动态代理类DynamicProxy的类,帮我们把切面类织入到切入点了。

Spring中已实现的AOP

Spring中有一些已经写好的切面逻辑,可以直接拿来用。

@ControllerAdvice

@ControllerAdvice注解修饰的类,可以对Controller中用@RequestMapping修饰的方法做切面处理,最常用的是统一处理Controller方法抛出的异常。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
@ControllerAdvice
public class ControllerExceptionHandler {

@ResponseBody // 响应体是json格式就必须加这个注解
@ExceptionHandler(value = Exception.class) // 指定抛出的异常类型
public JSONObject handleException(Exception ex) { // 在这个方法中写抛出异常后的处理
System.out.println("=============ControllerExceptionHandler===============");
JSONObject json = new JSONObject();
json.put("code", "9999");
json.put("msg", "System Error");
return json;
}
}

这个示例是指,当Conrtroller方法中抛出异常时,统一返回{“msg”:”System Error”,”code”:”9999”}响应体。
若需要统一跳到某个页面,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@ControllerAdvice
public class ControllerExceptionHandler {

@ExceptionHandler(value = Exception.class) // 指定抛出的异常类型
public ModelAndView handleException(Exception ex) { // 在这个方法中写抛出异常后的处理
System.out.println("=============ControllerExceptionHandler===============");
ModelAndView mav = new ModelAndView();
//指定错误页面的模板页
mav.setViewName("error");
mav.addObject("code", ex.getCode());
mav.addObject("msg", ex.getMsg());
return mav;
}
}

注意:当AOP切面和@ControllerAdvice同时存在,且AOP切面里也会统一处理Controller抛出的异常,@ControllerAdvice就不一定会执行。请看下面的例子。
AOP切面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
@Aspect
public class MainAspect {

@Around("@annotation(log)")
public Object doAround(ProceedingJoinPoint joinPoint, Log log) throws Throwable {
JSONObject result = new JSONObject();
try {
System.out.println("============around start============");
// 返回值
Object re = joinPoint.proceed();
if(re instanceof JSONObject)
result = (JSONObject) re;
return result;
} catch (Throwable e) { // 目标方法抛出异常
e.printStackTrace();
result.put("code", "9998");
result.put("msg", "error");
return result;
} finally {
System.out.println("============around end============");
}
}
}

@ControllerAdvice的修饰类同上,不再赘述。
运行时发现,目标方法抛出异常时,会统一返回{“msg”:”error”,”code”:”9998”},而不会返回@ControllerAdvice中的{“msg”:”System Error”,”code”:”9999”}。原因就是AOP中已经处理好异常了。
若仍然想把异常交给@ControllerAdvice处理,AOP切面可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
@Aspect
public class MainAspect {

@Around("@annotation(log)")
public Object doAround(ProceedingJoinPoint joinPoint, Log log) throws Throwable {
JSONObject result = new JSONObject();
try {
System.out.println("============around start============");
// 返回值
Object re = joinPoint.proceed();
if(re instanceof JSONObject)
result = (JSONObject) re;
return result;
} catch (Throwable e) { // 目标方法抛出异常
e.printStackTrace();
throw e; // 把异常抛出
} finally {
System.out.println("============around end============");
}
}
}

这样,目标方法抛出异常时,会统一返回@ControllerAdvice中的{“msg”:”System Error”,”code”:”9999”}。原因就是AOP中把异常抛出了。

404异常处理

没有什么特殊配置的情况下,Spring Boot遇到404就会自动跳到Spring Boot的error页面。若想要自己处理404异常,可以使用@ControllerAdvice。顺便一提,由于接口不存在,所以404异常肯定不会被AOP切面处理。
要处理404异常,必须在配置文件中加上:

1
2
3
4
# 出现错误时, 直接抛出异常
spring.mvc.throw-exception-if-no-handler-found: true
# 不要为我们工程中的资源文件建立映射
spring.resources.add-mappings: false

这个配置一定要加,否则Spring Boot总是会帮我们处理404异常,而不会进入我们定义的方法中。
然后就是在@ControllerAdvice中处理404,示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@ControllerAdvice
public class ControllerExceptionHandler {
@ResponseBody
@ExceptionHandler(value = Exception.class)
public JSONObject handleException(Exception ex) {
System.out.println("=============ControllerExceptionHandler===============");
JSONObject json = new JSONObject();
if(ex instanceof NoHandlerFoundException) { // 404异常
json.put("code", "9997");
json.put("msg", "No Found");
return json;
}
json.put("code", "9999");
json.put("msg", "System Error");
return json;
}
}

运行后可以发现,访问不存在的URI时,会统一返回{“msg”:”No Found”,”code”:”9997”}

@Transactional

这个注解帮你实现了数据库事务逻辑,尤其是事务回滚。
举个例子,删除用户时,要把用户关联的权限一起删除,当删除用户成功但删除权限不成功时,应该把删除用户的操作回滚,使得数据一致。
服务层示例:

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class MaintainService implements IMaintainService{
@Resource
private PersonMapper personMapper;

@Override
@Transactional // 事务回滚
public void deletePerson(int personId) {
personMapper.deleteById(personId);
personMapper.deletePersonPrivilege(personId);
}
}

以上代码,当deletePersonPrivilege方法抛出异常时,deleteById方法会被回滚,即数据库中用户和用户权限依然存在,说明回滚成功。另外,回滚成功后,依然会进入AOP切面中的异常捕获。
下面介绍@Transactional注解使用的注意点。

@Transactional只能用在public方法上

Spring在回滚前会检查方法修饰符是不是public,是才回滚。

@Transactional的rollbackFor属性

默认情况下,只有抛出Error类,或RuntimeException类及其子类的异常,Spring才会回滚。其他类型的异常不会回滚。
若需要在抛出其他异常时回滚,可以指定rollbackFor属性,如:

1
2
3
4
5
@Transactional(rollbackFor=Exception.class)
public void deletePerson(int personId) {
personMapper.deleteById(personId);
personMapper.deletePersonPrivilege(personId);
}

这样,只有抛出Exception类及其子类的异常,Spring才会回滚。

自调用问题

上面的示例,@Transactional注解修饰的方法会直接被Controller层接口调用,这种情况下都能回滚成功。但也有一些特殊情况。
情况1:

1
2
3
4
5
6
7
8
9
public void deletePerson(int personId) {
doDeletePerson(personId);
}

@Transactional
public void doDeletePerson(int personId) {
personMapper.deleteById(personId);
personMapper.deletePersonPrivilege(personId);
}

这种情况下,@Transactional不会生效。应改为下面这样才能生效:

1
2
3
4
5
6
7
8
9
@Transactional
public void deletePerson(int personId) {
doDeletePerson(personId);
}

public void doDeletePerson(int personId) {
personMapper.deleteById(personId);
personMapper.deletePersonPrivilege(personId);
}

情况2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void deletePerson(int personId) {
new Thread(new Runnable() { // 异步调用
@Override
public void run() {
doDeletePerson(personId);
}
}).start();
}

@Transactional
public void doDeletePerson(int personId) {
personMapper.deleteById(personId);
personMapper.deletePersonPrivilege(personId);
}

以及

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Transactional
public void deletePerson(int personId) {
new Thread(new Runnable() { // 异步调用
@Override
public void run() {
doDeletePerson(personId);
}
}).start();
}

public void doDeletePerson(int personId) {
personMapper.deleteById(personId);
personMapper.deletePersonPrivilege(personId);
}

这种有异步线程存在的情况下,@Transactional无论加在哪个方法都不会生效。
因此,只有把@Transactional注解加在直接被外部调用的方法才能生效。

Spring AOP和AspectJ的关系

AspectJ是一个独立的AOP框架,它有自己的一套语法用来实现AOP,用它的语法写的代码文件是.aj文件,还有自己的编译器ajc(Java编译器是javac),负责把.aj编译为.class文件。AspectJ在编译时就生成了代理类,所以它是静态代理。Spring的切入点只能是方法,但AspectJ可以用于字段、类等等,它的实现比Spring AOP要复杂的多。

在我们上面的实践中,都是用注解来编写切面类,这些注解是AspectJ的jar包提供的,但对于AOP功能的实现,用的是JDK或CGLIB的动态代理。因此,Spring AOP和AspectJ的关系就是,Spring用到了AspectJ的注解,但没有用它的语法和编译器,也没有用AspectJ的静态代理来实现功能。

JDK和CGLIB

  1. JDK动态代理。只能代理实现了接口的类。上面代码所展现的就是这个机制。它是用Proxy.newProxyInstance()方法生成代理类,用InvocationHandler向代理类织入AOP逻辑
  2. CGLIB动态代理。不要求被代理类必须实现接口,但不能代理final类。它是用Enhancer类创建被代理类的子类作为代理类,底层是用字节码创建子类,用MethodInterceptor向子类织入AOP逻辑

在SpringBoot2.0之前,Spring默认用JDK动态代理,只有当被代理类没有实现接口时,Spring才用CGLIB动态代理。在SpringBoot2.0之后,无论被代理类是否实现接口,Spring默认都用CGLIB动态代理。

想要自行实现CGLIB动态代理,看这篇博客:https://www.jianshu.com/p/13fa41aa18d8

JDK为什么只能代理实现了接口的类?

JDK用Proxy.newProxyInstance()方法生成的代理类类似于:

1
public final class $Proxy0 extends Proxy implements HelloService{...}

因为既要继承Proxy,又要继承接口,所以另一个只能是接口。

SpringBoot2.0之后,为什么默认用CGLIB动态代理?

官方的回答是,We’ve generally found cglib proxies less likely to cause unexpected cast exceptions.他们认为使用cglib更不容易出现转换错误。
如果我们的代码写成了:

1
2
@Autowired
UserServiceImpl userService;

这个时候,如果是JDK动态代理,那在启动时就会报错:因为JDK动态代理是基于接口的,代理生成的对象只能赋值给接口类型。CGLIB就不会报错
如果想设置默认使用JDK动态代理,可以加上配置项spring.aop.proxy-target-class=false。

性能问题

  1. JDK和CGLIB都是在运行期生成代理类的字节码。区别在于,JDK是直接写Class字节码,CGLIB使用ASM框架写Class字节码,CGLIB代理实现更复杂,生成代理类的效率比JDK低。
  2. JDK调用代理方法,是通过反射机制调用。CGLIB是通过FastClass机制直接调用方法,CGLIB执行效率更高。但,在JDK1.8后,Java的反射调用效率有所改善,整体的动态代理速度已经可以和CGLIB媲美了。

什么是FastClass机制?

FastClass机制的原理简单来说就是:为代理类和被代理类各生成一个Class,这个Class会为代理类或被代理类的方法分配一个index(int类型)。这个index当做一个入参,FastClass就可以直接定位要调用的方法直接进行调用,这样省去了反射调用,所以调用效率比JDK动态代理通过反射调用高。

JDK1.8后,Java的反射调用效率有哪些改善?

在1.8之前,JVM把缓存放在Class的属性SoftReference reflectionData。这个属性的类型是SoftReference(软引用),所谓软引用就是在资源紧张的情况下GC会进行回收,这就可能导致缓存丢失。SoftReference的泛型是ReflectionData,ReflectionData就是缓存数据的真正格式。ReflectionData将所有的Constructor、Method、Field对象存储下来供反射进行使用。

在1.8,放弃使用ReflectionData存储,而是在Class中直接将Constructor、Method、Field数组放进软引用中作为缓存。这样就将缓存分散,当资源紧张时,缓存不会全部被GC回收。

SpringBoot使用CGLIB创建代理类的原理

https://blog.csdn.net/weixin_45505313/article/details/103495439

https://blog.csdn.net/weixin_43732955/article/details/99196229

使用CGLIB实现AOP https://www.jianshu.com/p/7cc8ffe4372b

一、注册 AOP 的自动配置类AopAutoConfiguration,该配置类中配置 Bean 时使用@EnableAspectJAutoProxy注解,该注解 import 类AspectJAutoProxyRegistrar,该类中手动注册BeanAnnotationAwareAspectJAutoProxyCreator

二、由于 AnnotationAwareAspectJAutoProxyCreator 实现了 BeanPostProcess 接口,所以在每个 Bean 初始化后,会经过它的postProcessAfterInitialization方法,在这个方法中调用wrapIfNecessary方法。在 wrapIfNecessary 方法中为当前 Bean 创建代理类()。

  1. 扫描切面类的方法最终位于BeanFactoryAspectJAdvisorsBuilder.buildAspectJAdvisors(),它先扫描出项目中的几乎所有Bean,一个一个判断它是否是切面Bean(判断其是否有@Aspect注解),是就调用AspectJAdvisorFactory.getAdvisors(),取这个切面Bean中的所有切面方法,一个切面方法就封装成一个 Advisor。最后的结果就是,BeanFactoryAspectJAdvisorsBuilder 将所有切面Bean的所有切面方法,封装成好几个 Advisor 并返回(并且有做缓存,不会出现找到了一个bean的)
  2. 得到 BeanFactoryAspectJAdvisorsBuilder 返回的好几个 Advisor 后,会筛选出匹配当前 Bean 的几个 Advisor
  3. 调用ProxyFactory.getProxy()创建代理类(ProxyFactory 中已保存了前面得到的 Advisor),使用的是ObjenesisCglibAopProxy(调用其构造函数时,ProxyFactory 把自己作为参数传入了,所以 Advisor 也传入了),它继承自CglibAopProxy,所以创建代理类的方法就是CglibAopProxy.getProxy

CGLIB 代理类原理简述

CGLIB 有个类Enhancer,它可以保存