28 tháng 9, 2017

Thực hiện tùy biến Spring AOP Annotation

1. Annotation AOP là gì?

Giới thiệu nhanh thì AOP là viết tắt của lập trình hướng khía cạnh (Aspect Oriented Programming), đây là kĩ thuật chèn thêm hành vi vào đoạn code đã tồn tại mà không cần phải sửa code trực tiếp.

Bài viết giả định bạn đọc đã có những kiến thức cơ bản về lập trình hướng khía cạnh. Nếu bạn chưa có kiến thức về AOP, hãy tìm hiểu qua về khái niệm của pointcutadvice trong lập trình hướng khía cạnh.

Loại AOP mà chúng ta sẽ thực hiện tùy biến trong bài viết này là annotation driven. Chắc hẳn mọi người đều cảm thấy rất quen thuộc nếu đã từng sử dụng qua Spring @Transaction annotaion.

@Transaction
public void saveOrder(Order order) {
    // Gọi một loạt các thao tác với database trong transaction
}

Điểm mấu chốt ở đây đó là không tác động trực tiếp vào code. Bằng cách sử dụng annotation meta-data, những logic nghiệp vụ cơ bản của ứng dụng của bạn sẽ không bị trộn lẫn với những đoạn code quản lí transaction. Việc này sẽ làm cho code của bạn dễ dàng được giải thích, tái cấu trúc và kiểm thử trong transaction isolation.

Đôi khi, những lập trình viên phát triển các ứng dụng của hệ sinh thái Spring lại coi đây là điều "vi diệu" của Framework này, mà không suy nghĩ nhiều về cách thức hoạt động chi tiết của nó. Thực tế, những gì đang xảy ra không thực sự quá phức tạp. Một khi hoàn thành các bước của bài viết này, bạn sẽ có thể tự tạo ra các annotation tùy biến của riêng mình, hiểu được cách thức hoạt động và tận dụng các lợi ích của AOP mang lại.

2. Maven Dependency

Đầu tiên, hãy thêm các Maven dependency cần thiết.

Trong ví dụ này, chúng ta sẽ sử dụng Spring Boot, vì cách tiếp cận cấu hình của module này sẽ giúp chúng ta bắt đầu ví dụ một cách nhanh nhất có thể:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.7.RELEASE</version>
</parent>
 
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
</dependencies>

Cần lưu ý là phải thêm AOP starter, nó sẽ kéo các thư viện mà ta cần để có thể triển khai AOP.

3. Tạo annotation tùy biến

Annotation sau đây mà tôi sẽ tạo ra là một annotation được sử dụng với mục đinh log khoảng thời gian thực thi của một phương thức:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {

}

Mặc dù cách thực hiện khá là đơn giản, nhưng bạn hãy chú ý đến hai meta-annotation được sử dụng.

Annotation @Target sẽ cho bạn biết nơi mà annotation tùy biến có thể áp dụng. Ở đây ta đang sử dụng ElementType.Method, điều này có nghĩa là annotation tùy biến sẽ chỉ làm việc trên các phương thức. Nếu bạn cố gắng thử sử dụng annotation đã tạo ra ở những nơi khác, thì đoạn code của chúng ta sẽ báo lỗi trong quá trình biên dịch.

Và annotation @Retention sẽ chỉ rõ liệu annotation tùy biến của chúng ta có sẵn dùng trong quá trình runtime hay không. Mặc định điều này sẽ là không, vì vậy Spring AOP sẽ không thể nhận biết được annotation mà chúng ta đã tạo ra. Đó là lí do tại sao nó được cấu hình lại.

4. Tạo Aspect

Bây giờ, chúng ta đã có một annotation, hãy tạo thêm aspect. Đây chính là điều chúng ta cần quan tâm nhất, tất cả những gì chúng ta cần là một class được đánh dấu bởi @Aspect annotation:

@Aspect
@Component
public class ExampleAspect {

}

Bạn cũng sẽ cần phải sử dụng đến annotation @Component vì class này cũng cần phải là một spring bean trong Spring container. Về cơ bản, thì đây là class mà ta sẽ thực hiện những logic mong muốn.

5. Tạo Pointcut và Advice

Bây giờ, hãy tạo ra các pointcutadvice. Đây sẽ là một phương thức chú thích được đặt bên trong class Aspect của chúng ta:

@Around("@annotation(LogExecutionTime)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
    return joinPoint.proceed();
}

Về mặt kĩ thuật, điều này sẽ không thay đổi hành vi của bất cứ điều gì, nhưng vẫn còn rất nhiều điều đang cần phân tích.

Đầu tiên, ta đánh dấu phương thức bằng annotation @Around. Đây là advice của chúng ta, và around advice nghĩa là chúng ta đang thêm code bổ sung trước và sau khi thực thi phương thức. Cũng có một số loại advice khác, ví dụ như before, after nhưng trong phạm vi bài viết này, ta sẽ không sử dụng đến chúng.

Tiếp theo, hãy chú ý đến annotation @Around của chúng ta có một đối số point cut. Pointcut của chúng ta nói rằng: 'Áp dụng advice này lên bất kì phương thức nào được đánh dấu với annotation @LogExecutionTime.'. Cũng có rất nhiều loại pointcut khác nữa, nhưng một lần nữa chúng ta sẽ không đề cập đến chúng trong phạm vi bài viết này.

Chính phương thức logExecutionTime() sẽ là một advice, phương thức này có một đối số là ProceedingJoinPoint. Trong trường hợp của chúng ta, thì đây sẽ là một phương thức thực thi mà đã được đánh dấu bằng annotation @LogExecutionTime.

Cuối cùng, bất cứ khi nào một phương thức đã được đánh dấu được gọi đến, thì advice của chúng ta sẽ được gọi đầu tiên. Sau đó, tùy thuộc vào advice của chúng ta quyết định làm gì tiếp theo. Trong trường hợp này, advice của chúng ta không làm gì khác ngoài việc gọi đến phương thức proceed(), điều này sẽ chỉ gọi đến phướng thức gốc đã được đánh dấu.

6. Log thời gian thực thi

Bây giờ chúng ta đã có một bộ khung, tất cả những gì chúng ta cần làm là thêm một vài logic vào trong advice. Chúng ta sẽ ghi lại thời gian thực thi của phương thức ban đầu.

@Around("@annotation(LogExecutionTime)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
    long start = System.currentTimeMillis();
 
    Object proceed = joinPoint.proceed();
 
    long executionTime = System.currentTimeMillis() - start;
 
    System.out.println(joinPoint.getSignature() + " executed in " + executionTime + "ms");
    return proceed;
}

Một lần nữa, chúng ta không làm bất cứ điều gì quá phức tạp ở đây cả. Chúng ta chỉ lưu lại thời gian hiện lại, thực thi phương thức, sau đó in ra console khoảng thời gian đã tính toán được. Chúng ra cũng log cả tên của phương thức, api này được cung cấp bởi joinpoint instance. Ta cũng có thể truy cập được một tá các thông tin khác nếu muốn, ví dụ như các đối số của phương thức.

Bây giờ, hãy thử đánh dấu một phương thức bằng @LogExecutionTime, và sau đó thực thi nó để xem điều gì sẽ xảy ra. Chú ý rằng, phương thức mà chúng ta muốn đánh dấu phải thuộc về một Spring bean:

@LogExecutionTime
public void serve() throws InterruptedException {
    Thread.sleep(2000);
}

Sau khi thực thi, ta sẽ thấy kết quả trên console như sau:

void com.example.Service.serve() executed in 2030ms

Không có nhận xét nào:

Đăng nhận xét