Trước hết, kể từ phiên bản SpringBoot 2 - 888bets
Chủ đề này xuất phát từ một buổi chia sẻ của đồng nghiệp trong nhóm. Trước hết, kể từ phiên bản SpringBoot 2.x, cglib đã trở thành mặc định để thực hiện AOP. Dưới đây là cách cấu hình AopAutoConfiguration
trong dự án cũ chạy trên phiên bản 1.x:
1@Configuration
2@ConditionalOnClass({ EnableAspectJAutoProxy.class, Aspect.class, Advice.class })
3@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
4public class AopAutoConfiguration {
5
6 @Configuration
7 @EnableAspectJAutoProxy(proxyTargetClass = false)
8 @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false", matchIfMissing = true)
9 public static class JdkDynamicAutoProxyConfiguration {}
10
11 @Configuration
12 @EnableAspectJAutoProxy(proxyTargetClass = true)
13 @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = false)
14 public static class CglibAutoProxyConfiguration {}
15}
Trong khi đó, ở phiên bản 2.x, cấu hình này đã thay đổi như sau:
1@Configuration(proxyBeanMethods = false)
2@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
3public class AopAutoConfiguration {
4
5 @Configuration(proxyBeanMethods [mua the game w88 o dau](/post/fd1e1ad222e42158.html) = false)
6 @ConditionalOnClass(Advice.class)
7 static class AspectJAutoProxyingConfiguration {
8
9 @Configuration(proxyBeanMethods = false)
10 @EnableAspectJAutoProxy(proxyTargetClass [keo toi nay](/post/44b0ec022b156fed.html) = false)
11 @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
12 static class JdkDynamicAutoProxyConfiguration {}
13
14 @Configuration(proxyBeanMethods = false)
15 @EnableAspectJAutoProxy(proxyTargetClass = true)
16 @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = true)
17 static class CglibAutoProxyConfiguration {}
18 }
19}
Tại sao AopAutoConfiguration
được tải vào? Điều này đã được giải thích chi tiết trong bài viết trước về cơ chế tự động cấu hình của SpringBoot. Bạn có thể thấy rằng kể từ phiên bản 2.x, SpringBoot đã chuyển sang sử dụng cglib làm công cụ thực hiện dynamic proxy mặc định.
Vấn đề nổi lên
Giả sử chúng ta có đoạn mã đơn giản sau với SpringBoot và cơ sở dữ liệu, bao gồm cả annotation giao dịch (@Transactional
):
1@Mapper
2public interface StudentMapper {
3 @Insert("insert into student(name, age) values ('nick', '18')")
4 Long insert();
5}
6
7@Component
8public class StudentManager {
9 @Resource
10 private StudentMapper studentMapper;
11
12 public Long createStudent() {
13 return studentMapper.insert();
14 }
15}
16
17@Component
18public class StudentServiceImpl implements StudentService {
19 @Resource
20 private StudentManager studentManager;
21
22 @Resource
23 private StudentServiceImpl studentService;
24
25 @Override
26 @Transactional
27 public Long createStudent() {
28 Long id = studentManager.createStudent();
29 Long id2 = studentService.createStudent2();
30 return 1L;
31 }
32
33 @Transactional
34 private Long createStudent2() {
35 return studentManager.createStudent();
36 }
37}
Phương thức công khai createStudent
gọi phương thức tạo sinh viên ở lớp manager
, sau đó lại gọi tiếp đến createStudent2
thông qua tham chiếu tới chính nó (studentService
). Khi chạy thử, chương trình báo lỗi NullPointerException tại vị trí studentManager
là null trong phương thức createStudent2
.
Lý do gây ra lỗi
Cglib thực hiện dynamic proxy bằng cách tạo một lớp con kế thừa từ đối tượng mục tiêu. Các logic cắt ngang (aspect) sẽ được xử lý trong lớp con này trước khi gọi đến phương thức mục tiêu gốc. Tuy nhiên, vấn đề xảy ra khi áp dụng cho các phương thức final hoặc private vì Java không cho phép override những phương thức này. Mặc dù vậy, trong trường hợp này, lớp con vẫn có thể truy cập vào phương thức riêng tư của lớp cha thông qua mechanism internal của JVM.
Do createStudent2
là phương thức private, nó không bị proxy bởi cglib mà chỉ đơn thuần được gọi trực tiếp như bình thường. Vì vậy, khi gọi phương thức này, nó không sử dụng bean được quản lý bởi Spring mà thay vào đó sử dụng chính lớp con được tạo ra bởi cglib. Kết quả là các dependency injection (như studentManager
) không được áp dụng, dẫn đến lỗi NullPointerException.
Giải thích sâu hơn về cơ chế hoạt động
Xem xét đoạn mã sau từ lớp org.springframework.aop.framework.CglibAopProxy.CglibMethodInvocation
:
1@Override
2protected Object invokeJoinpoint() throws Throwable {
3 if (this.methodProxy != null) {
4 return this.methodProxy.invoke(this.target, this.arguments);
5 } else {
6 return super.invokeJoinpoint();
7 }
8}
Đây là nơi methodProxy được sử dụng để gọi phương thức trên đối tượng đích (target object), tức là bean được quản lý bởi Spring. Nếu không có cơ chế này, các phương thức không được proxy sẽ không thể hoạt động đúng như mong đợi.
Quá trình khởi tạo proxy
Spring thiết lập đối tượng đích trong quá trình khởi tạo bean thông qua giao diện org.springframework.beans.factory.config.BeanPostProcessor
. Phương thức postProcessAfterInitialization
được gọi sau khi bean được khởi tạo:
1@Override
2public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
3 if (bean != null) {
4 Object cacheKey = getCacheKey(bean.getClass(), beanName);
5 if (this.earlyProxyReferences.remove(cacheKey) != bean) {
6 return wrapIfNecessary(bean, beanName, cacheKey);
7 }
8 }
9 return bean;
10}
Logic cụ thể nằm trong phương thức wrapIfNecessary
của lớp AbstractAutoProxyCreator
:
1protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
2 if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
3 return bean;
4 }
5 if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
6 return bean;
7 }
8 if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
9 this.advisedBeans.put(cacheKey, Boolean.FALSE);
10 return bean;
11 }
12 Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
13 if (specificInterceptors != DO_NOT_PROXY) {
14 this.advisedBeans.put(cacheKey, Boolean.TRUE);
15 Object proxy = createProxy(
16 bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
17 this.proxyTypes.put(cacheKey, proxy.getClass());
18 return proxy;
19 }
20 this.advisedBeans.put(cacheKey, Boolean.FALSE);
21 return bean;
22}
Cuối cùng, proxy được tạo ra trong phương thức createProxy
của AbstractAutoProxyCreator
.
Kết luận: Để tránh lỗi tương tự, cần đảm bảo rằng các phương thức yêu cầu proxy phải là public và không được đánh dấu là final.