Mối quan hệ biện chứng giữa AOP và Transactional trong Spring Framework

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 foxcode.site. Hm hm, cũng lâu rồi kể từ khi mình bắt đầu những bài viết đầu tiên trên blog, bắt đầu tìm đến thú vui gõ phím viết bài chém gió, à nhầm, thảo luận, cùng các bạn. 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à:

The 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.

import 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:

import 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, ... được mô tả như sau:

  • Những đoạn code mà bạn muốn thực thi khi gọi một method nào đó, được gọi là aspect

Ví dụ: Đoạn code bạn cần tính toán thời gian thực thi phương thức notify ở trên, chính nó là một aspect

  • Khi nào aspect của bạn được thực thi (ví dụ: trước hay sau method chính được gọi) được gọi là advice.

    Ví dụ: Để tính toán được thời gian thực thi của phương thức notify, bạn cần phải execute aspect trước khi phương thức notify được execute để lấy được thời điểm mà notify bắt đầu execute.

  • Phương thức bạn muốn framework theo dõi để thực thi aspect được gọi là point cut.

    Ví dụ: Nó chính là phương thức notify ở trên đó ạ.

  • Sự kiện làm cho aspect được thực thi được gọi là Join point. Trong Spring, sự kiện đó luôn là việc chúng ta gọi đến phương thức (method call).

    Ví dụ: Chính là khi bạn gọi đến phương thức notify, join point ở đây chính là sự kiện bạn gọi đến phương thức notify.

@SpringBootApplication
@RequiredArgsConstructor
public class CommonQueueTestApplication implements CommandLineRunner {
    private final NotificationService notificationService;

    public static void main(String[] args) {
        SpringApplication.run(CommonQueueTestApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        notificationService.notify("tunk@gmail.com", "Welcome to Foxcode social network!");
    }
}

Để các bạn có thể hình dung được một cách tổng quan, mình xin đưa ra một hình minh họa như sau nhé, 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.

aspect.png

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:

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

Sau đó chúng ta tạo class cho AOP như sau:

@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é

withoutAspect.png

withAspect.png

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

debug1.png

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

debug2.png

Ồ, 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 Transcational 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:

    @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:

debug3.png

Tiếp tục, F7

debug4.png

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.

tandaop.png

Ở 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:

@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.

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é.