
Mối quan hệ biện chứng giữa AOP và Transactional trong Spring Framework
Xin chào tất cả các bạn, chào mừng các bạn đến với blog fox-learning. Hôm nay mình sẽ lên cho các bạn một bài viết về chủ đề đã làm thổn thức bao trái tim của các Spring Developer cũng nói riêng cũng như Java developer nói chung. Đó chính là Spring AOP. AOP là cái chi chi và nó có mối quan hệ như thế nào với Transactional, liệu có phải biện chứng như cái tiêu đề mình đã đặt ở trên hay không? Trong bài viết này, mình sẽ giúp các bạn trả lời những câu hỏi đó nhé. Nào, let's go!
AOP là cái chi chi?
Chắc hẳn mỗi khi tìm kiếm định nghĩa với cụm từ AOP thì các bạn sẽ thường gặp các cách diễn giải đại khái như: AOP là viết tắt của Aspect Oriented Programming, là một mô thức lập trình phân tách chương trình thành các module tách biệt, hoặc là AOP cho phép lập trình viên phân tách các phần code thuộc cross-cutting concern ra khỏi các code thuộc business logic. Chả chà, nghe thì cũng khá là hấp dẫn đúng không ạ? Thế nhưng ta lại phải đi làm rõ cross-cutting concern cụ thể là cái gì. Nếu search cụm từ này trên google, bạn dễ dàng nhận được các kết quả kiểu kiểu như là:
12The cross-cutting concerns are features that span across different parts of the application and affects the entire application. Examples → logging, declarative transactions, security, caching, etc
Để các bạn dễ hình dung, mình có thể lấy một ví dụ đơn giản như sau: Giả sử mình đang ấp ủ xây dựng một mạng xã hội mang tên Foxcode, cũng giống như Facebook, thì notify (core concern) là một tính năng không thể thiếu của một mạng xã hội. Ngoài việc push notification tới người dùng, hệ thống cần phải làm một số việc khác ( cross-cutting) chẳng hạn như: kiểm tra ràng buộc, chứng thực người dùng, ghi log, tương lai khi chúng ta muốn monitor hệ thống thì có thể đặt log để kiểm tra thời gian thực thi của tính năng notification.
12345678910import org.springframework.stereotype.Service; @Service public class NotificationService { public void notify(String target, String message){ //Do something to push notification } }
Với mỗi lần gọi phương thức notify của NotificationService, chúng ta cần tính toán thời gian thực thi của của phương thức này để đánh giá hiệu năng cho hệ thống. Okay, đơn giản đúng không, bạn có thể thêm code tính toán như sau:
1234567891011121314151617import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @Service @Slf4j public class NotificationService { public void notify(String target, String message){ long startTime = System.currentTimeMillis(); //Do something to push notification long endTime = System.currentTimeMillis(); long executedTimeInMiliSec = endTime - startTime; log.info("(notify) - Total time to execute notify method: [{}]", executedTimeInMiliSec); } }
Code chạy log ra thời gian thực thi đúng theo yêu cầu. Thế nhưng, nếu để ý kỹ, các bạn sẽ thấy là code tính toán thời gian thực thi ( cross-cutting concern) nằm lẫn với code thực hiện việc push notification (core concern). Trong tương lai mình muốn thay đổi việc hiển thị thời gian này theo giây, mình sẽ vào trực tiếp phương thức notify để sửa => dễ gây ra lỗi cho method notify này. Hơn thế nữa, chúng ta không chỉ theo dõi thời gian thực thi của phương thức notify, mà rất nhiều phương thức khác cũng cần monitor để đánh giá hiệu năng, chả có nhẽ ta lại đi copy code (vi phạm principle DRY trong lập trình), khi muốn sửa ta lại đi tìm từng chỗ để sửa (vi phạm principle Open/Close của SOLID). Ở phần tiếp theo, chúng ta sẽ cùng xem AOP đã giải quyết vấn đề này như thế nào nhé.
How aspects work in Spring?
Okie, đến đây chắc hẳn các bạn cũng đã có một chút hình dung ban đầu về thằng AOP đúng không ạ, về thực tế thì AOP có thể được sử dụng phức tạp hơn thế rất nhiều, nhưng tạm thời để làm quen với AOP mình nghĩ một vài khái niệm và ví dụ đơn giản như thế sẽ giúp chúng ta có một cái nhìn dễ thở hơn đối với AOP.
Trong phần này, mình và các bạn sẽ cùng nhau đi tìm hiểu AOP đã được đưa vào Spring như thế nào nhé. Cũng như khi bạn làm quen với OOP bạn cũng cần tìm hiểu thế nào là Class, Object, Instance, Interface, Abstract Class, để làm việc với AOP trong Spring, bạn sẽ cần hiểu một vài các thuật ngữ như là: aspect, advice, pointcut, join point, ...
Spring AOP sử dụng runtime proxy để chèn (weave) aspect vào các điểm (join point) trong quá trình chạy ứng dụng
- Aspect: Một module đại diện cho cross-cutting concern. Nó định nghĩa advice và pointcut. Ví dụ: Lớp LoggingTimeExecutionAspect chứa logic đo thời gian.
- Join point: Một điểm trong quá trình thực thi chương trình mà aspect có thể "chen vào". Trong Spring AOP, join point chỉ là method execution. Ví dụ: Gọi notificationService.notify() là một join point.
- Advice: Hành động (một đoạn code) được thực hiện tại một join point, như @Before, @After, @Around. Ví dụ: Đoạn code log.info(...) trong @Around advice.
- Pointcut: Biểu thức định nghĩa join point nào sẽ bị áp dụng advice. Ví dụ: execution(* NotificationService.notify(..)) chọn ra join point ứng với method notify(...).
- Target object: Bean gốc đang bị proxy để áp dụng aspect. Ví dụ: notificationService sau khi được AOP proxy hóa.
- AOP Proxy: Object được tạo ra để bao quanh target object, và xử lý aspect. Spring dùng JDK proxy (nếu có interface) hoặc CGLIB (nếu không). Ví dụ: Spring tạo notificationServiceProxy để chèn aspect.
- Weaving: Quá trình kết hợp aspect với target object tại các join point. Spring AOP thực hiện weaving lúc runtime. Ví dụ: Spring proxy method notify() tại runtime để áp dụng advice đo thời gian.
Ngắn gọn thì: Aspect là một concern cắt ngang, chứa các advice. Advice là đoạn logic thực thi tại các join point (như method execution). Pointcut định nghĩa điều kiện để advice áp dụng lên join point.
Để dễ hình dung hơn, bạn hãy tưởng tượng mình đang đi dự sinh nhật tại một nhà hàng sang trọng, khi nhân viên bồi bàn lên món, họ luôn nói: "Chúc quý khách ngon miệng" trước khi họ đặt món ăn lên bàn và rời đi. Ánh xạ vào khái niệm của AOP thì có thể hiểu rằng:
- Aspect: Bộ quy định ứng xử với khách hàng khi mang đồ ăn lên bàn.
- Advice: Hành động nói: “Chúc quý khách ngon miệng!”, trước khi (@Before) đặt đồ ăn xuống bàn
- Join Point: Thời điểm nhân viên phục vụ đặt món ăn xuống bàn
- Pointcut: Điều kiện: chỉ áp dụng cho hành động bưng món ra bàn
Quay lại bài toán kỹ thuật, chúng ta vẫn tiếp tục đi giải quyết bài toán log execution time cho phương thức notify nha.

Yeah, tiếp đến chúng ta sẽ cùng nhau implement AOP với Spring boot nha. Để sử dụng AOP với Spring boot, mình cần dependency sau:
12345<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
Sau đó chúng ta tạo class cho AOP như sau:
12345678910111213@Aspect @Slf4j @Component public class LoggingTimeExecutionAspect { @Around("execution(* site.foxcode.commonqueuetest.service.NotificationService.notify(..))") public void logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { log.info("(logExecutionTime) - Calculating .... [{}]", joinPoint.getSignature().getName()); joinPoint.proceed(); log.info("(logExecutionTime) - Calculated done!"); } }
Để hiểu rõ và sâu hơn cách viết Aspect thì mình sẽ có các bài viết khác về Spring AOP cũng như AspectJ nhé, tạm thời ở đây các bạn cứ hiểu giúp mình là mình đang khai báo một Aspect để thực hiện việc tính toán execution time cho phương thức notify, tách biệt khỏi logic code trong notify method nha.
Vậy, khi ta áp dụng AOP vào thì sẽ khác gì so với khi chưa áp dụng AOP, mình sẽ cùng so sánh một chút nhé


Nếu mọi người đặt debug thì sẽ thấy rõ hơn nhé.

Sau đó F7 để vào chi tiết xem mình có thực sự nhảy sang method notify không nhé

Ồ, lời gọi method của chúng ta tạm thời đã bị chặn (intercept) bởi AOP proxy!!!
Đến đây chắc mọi người đã thấy được sức mạnh của AOP chưa ạ, ta có thể làm bất cứ điều gì với method được chặn bởi AOP proxy: kiểm tra điều kiện, cho phép thực thi method hay là chặn, điều chỉnh tham số trước khi method được gọi, .... Khi làm việc sâu hơn nữa với Spring AOP, bạn sẽ cảm nhận được sức mạnh của nó.
AOP có mối liên hệ gì với @Transactional trong Spring?
Là một lập trình viên, chúng ta không thể tránh khỏi việc sử dụng Transaction để đảm bảo tính toàn vẹn, nhất quán của dữ liệu. Thật may mắn, với Spring, để có được Transaction, ta chỉ cần đánh dấu method, class với annotation là @Transactional. Liệu có khi nào bạn tự hỏi, điều gì đã làm cho @Transactional vi diệu đến vậy. Vâng, các bạn đã đoán đúng rồi đó ạ, đó chính là nhờ sức mạnh của AOP.
Mình có một ví dụ đơn giản như sau:
1234567@Override @Transactional public void createLead(LeadEntity lead) { leadRepository.save(lead); log.info("(createLead) - Create lead : [{}] successfully!", lead); }
Nếu đặt debug trước khi gọi tới method createLead, ta được như sau:

Tiếp tục, F7

Wow, method của chúng ta tiếp tục bị chặn bởi AOP đúng không ạ? Một cách tổng quan, mình có vẽ một sơ đồ cho các bạn dễ hình dung nha.

Ở trên mình có minh họa giả code đơn giản cho Spring transaction AOP. Nếu một trong các bước ở method createLead throw ra bất kỳ runtime exception nào, thì aspect lập tức rollback toàn bộ transaction. Ngược lại, nếu không có runtime exception trong suốt quá trình thực thi, thì transaction được commit. Vậy nếu như, trong method createLead, bạn đặt các lệnh trong khối try catch như thế này thì sao:
123456789101112@Override @Transactional public void createLead(LeadEntity lead) { try{ leadRepository.save(lead); throw new RuntimeException("Oops! Something went wrong!"); }catch (Exception e){ log.error("(createLead) - Exception with message: [{}]", e.getMessage()); } }
Mặc dù có Exception, nhưng trasaction này của bạn vẫn được commit do ở phần xử lý Exception bạn không throw ra một Exception nào để Spring transaction aspect có thể nhận biết để rollback. Các bạn có thể tự tạo một ví dụ đơn giản như phương thức createLead của mình để kiểm chứng nha
Annotation @Transactional không phải là magic, nó hoạt động nhờ Spring AOP. Khi bạn đánh dấu method là @Transactional, Spring sẽ:
- Tạo ra một proxy quanh bean.
- Khi bạn gọi method đó, Spring chặn (intercept) trước khi gọi thật.
- Proxy kiểm tra có cần bắt đầu transaction không.
- Nếu trong quá trình thực thi có exception (unchecked), nó rollback.
- Nếu không có lỗi – thì commit.
Nhưng…
Gọi method trong cùng class thì sao?
12345public void createUserAndLead(User user, Lead lead) { createUser(user); // method có @Transactional createLead(lead); // method có @Transactional }
Surprise! Transaction không hề được tạo! 😱
Tại sao?
Vì khi bạn gọi method từ cùng một instance, Spring không sử dụng proxy để chặn được lời gọi. Nó chỉ hoạt động khi lời gọi đi qua proxy. Tình huống này gọi là "self-invocation".
@Async cũng tương tự
Khi dùng @Async, bạn cũng kỳ vọng method sẽ chạy ở thread khác
123@Async public void sendEmail() { ... }
Nhưng nếu gọi trong cùng class như sau:
1234public void process() { sendEmail(); // GỌI TRỰC TIẾP }
Không có gì xảy ra – vì lời gọi không đi qua proxy => @Async không được kích hoạt.
Tổng kết
Như vậy qua bài viết này, mình mong muốn truyền tải đến các bạn một số kiến thức cơ bản nhất, có thể là basic background cho các bạn có thể tự tìm hiểu, làm việc sâu và rộng hơn với Spring AOP cũng như AspectJ. Mối liên hệ giữa AOP và transactional trong Spring cũng được dần làm sáng tỏ. Ở đây có một lưu ý nhỏ mà theo mình nghĩ các bạn cần lưu tâm. Spring chỉ rollback transaction khi nó nhận biết được Exception, nếu bạn muốn Spring xử lý transaction cho mình, đừng quên điều này nha. Cảm ơn các bạn đã theo dõi bài viết của mình, hẹn gặp lại các bạn ở các bài viết khác nhé
Bình luận (0)
Vui lòng đăng nhập để tham gia bình luận.