Sau khi tìm hiểu sâu về singleton thì bây giờ là lúc chúng ta đi sâu hơn vào các tính năng của spring core. Trong bài này Dũng sẽ cùng các bạn tìm hiểu về Spring AOP nhé.

Khái niệm

AOP là viết tắt của Aspect Oriented Programming dịch ra tiếng Việt là lập trình hướng khía cạnh. Cá nhân mình không thích dịch thành “khía cạnh” lắm vì tự nhiên nó làm những người tiếp cận cảm thấy một cái gì đó rất vi diệu, tuy nhiên trên thực tế nó cũng không quá khó và cũng không có gì đặc biệt lắm. Tuy nhiên mình cũng không nghĩ ra từ nào đơn giản hơn :D.
Ý tưởng cơ bản là chúng ta sẽ tạo ra một lớp để bao bọc một lớp đã có sẵn để bổ sung thêm các hành động trước và sau khi gọi hàm. Giả sử chúng ta có một lớp HelloWorld với một hàm sayHello, thì AOP sẽ tạo ra một lớp HelloWorldProxy để bọc lại lớp HelloWorld và bổ sung thêm các hàm beforeSayHello và afterSayHello mã nguồn sẽ kiểu thế này:

package vn.techmaster.aop.hello;

public class HelloWorld {

    public void sayHello() {}
}
package vn.techmaster.aop.hello;

public class HelloWorldProxy extends HelloWorld {

    private void beforeSayHello() {
        System.out.println("before sayHello");
    }

    @Override
    public void sayHello() {
        beforeSayHello();
        super.sayHello();
        afterSayHello();
    }

    private void afterSayHello() {
        System.out.println("after sayHello");
    }
}

Hoặc phức tạp hơn một chút chúng ta sẽ tạo ra tạo ra các lớp Aspect để dễ bổ sung, mở rộng hơn về sau này, và mã nguồn sẽ như sau:

package vn.techmaster.aop.hello;

public interface Aspect {

    void beforeMethod(String method);

    void afterMethod(String method);
}
package vn.techmaster.aop.hello;

public class LogAspect implements Aspect {

    @Override
    public void beforeMethod(String method) {
        System.out.println("before " + method);
    }

    @Override
    public void afterMethod(String method) {
        System.out.println("after " + method);
    }
}
package vn.techmaster.aop.hello;

import java.util.List;

public class HelloWorldProxy extends HelloWorld {

    private final List<Aspect> aspects = List.of(
        new LogAspect()
    );

    private void beforeSayHello() {
        for (Aspect aspect : aspects) {
            aspect.beforeMethod("sayHello");
        }
    }

    @Override
    public void sayHello() {
        beforeSayHello();
        super.sayHello();
        afterSayHello();
    }

    private void afterSayHello() {
        for (Aspect aspect : aspects) {
            aspect.afterMethod("sayHello");
        }
    }
}

Như bạn thấy, về bản chất là AOP sử dụng Proxy design pattern vậy nên nó cũng không có gì phức tạp cả.

Tạo module

Cũng tương tự như những bài trước, chúng ta sẽ tạo một module có tên aop cho bài này.

Cấu hình

Khi chúng ta thêm dependency spring-context vào trong tập tim pom.xml thì maven đã tải sẵn cho chúng ta thư viện spring-aop rồi vì spring-aop cũng nằm trong tập tin pom.xml của spring-context, bạn có thể thấy trong hình dưới đây:

Tuy nhiên về bản chất thì spring không tự cài đặt AOP mà thông qua một thư viện của một bên khác, thư viện này có tên là aspectjweaver và chúng ta sẽ cần bổ sung phụ thuộc vào tập tin aop/pom.xml như sau:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>${aspectj.version}</version>
</dependency>

Với aspectj.version là 1.9.22.1
Tiếp theo chúng ta sẽ cần bổ sung EnableAspectJAutoProxy annotation và lớp AopStartUp như sau:

@Configuration
@ComponentScan(basePackages = "vn.techmaster.aop")
@EnableAspectJAutoProxy
public class AopStartUp {

Sử dụng

Đầu tiên giả sử chúng ta đã có sẵn một lớp UserController thế này:

package vn.techmaster.aop.controller;

import org.springframework.stereotype.Controller;

import java.math.BigDecimal;

@Controller
public class UserController {

    public void updateProfile(
        String username,
        String data
    ) {}

    public void updateBalance(
        String username,
        BigDecimal balance
    ) {}
}

Lớp này sẽ có hai hàm cho mục tiêu cập nhật hồ sơ và cập nhật số dư. Hai hàm này tương đối quan trọng nên chúng ta cũng sẽ có nhu cầu:

  1. Log trước và sau khi gọi hàm để dễ dàng cho việc gỡ lỗi sau này.
  2. Đo hiệu suất của hàm để cải thiện nếu cần.
    Chúng ta có thể sử dụng spring AOP như sau.

Sử dụng cho log

Chúng ta có thể tạo ra một lớp có tên LoggingAspect với hai hàm logBefore và logAfter như sau:

package vn.techmaster.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* vn.techmaster.aop.controller..*(..))")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println(
            "LoggingAspect: Before method "
                + joinPoint.getSignature().getName()
                + " execution"
        );
    }

    @After("execution(* vn.techmaster.aop.controller..*(..))")
    public void logAfter(JoinPoint joinPoint) {
        System.out.println(
            "LoggingAspect: After method "
                + joinPoint.getSignature().getName()
                + " execution"
        );
    }
}

Trong mã nguồn chúng ta sẽ có:

  1. @Aspect annotation để chỉ cho spring rằng đây là một lớp Aspect (tương tự như như chúng ta đã nhắc đến trong phần khái niệm) để đưa vào danh sách các lớp Aspect tương tự như chúng ta đã đưa vào lớp HelloWorldProxy.
  2. @Before annotation để chỉ ra rằng hàm logBefore sẽ được chạy trước khi chạy hàm chính.
  3. @After annotation để chỉ ra rằng hàm logAfter sẽ được chạy sau khi chạy hàm chính.
    Chúng ta có thể gọi các hàm của UserController thông qua lớp AopStartUp với mã nguồn như sau:
package vn.techmaster.aop;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import vn.techmaster.aop.controller.UserController;

import java.math.BigDecimal;

@Configuration
@ComponentScan(basePackages = "vn.techmaster.aop")
@EnableAspectJAutoProxy
public class AopStartUp {

    public static void main(String[] args) {
        ApplicationContext applicationContext =
            new AnnotationConfigApplicationContext(
                "vn.techmaster.aop"
            );
        UserController userController = applicationContext
            .getBean(UserController.class);
        userController.updateBalance(
            "Dzung",
            BigDecimal.ZERO
        );
        userController.updateProfile("Dzung", "dung@techmaster.vn");
    }
}

Kết quả chúng ta nhận được sẽ là:

LoggingAspect: Before method updateBalance execution
LoggingAspect: After method updateBalance execution
LoggingAspect: Before method updateProfile execution
LoggingAspect: After method updateProfile execution

Sử dụng để đo hiệu năng

Để thống kê hiệu năng của của các hàm chúng ta có thể tạo ra một lớp PerformceManager như sau:

package vn.techmaster.aop.performance;

import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component
public class PerformanceManager {

    private final Map<String, Long> totalCallByMethod =
        new ConcurrentHashMap<>();
    private final Map<String, Long> totalProcessTimeByMethod =
        new ConcurrentHashMap<>();

    public synchronized void record(String method, long processTime) {
        totalCallByMethod.compute(method, (k, v) -> v != null ? v + 1 : 1);
        totalProcessTimeByMethod.compute(
            method,
            (k, v) -> v != null ? v + processTime : processTime
        );
    }

    public String getSummary() {
        return "totalCallByMethod: " + totalCallByMethod
            + "\n"
            + "totalProcessTimeByMethod: " + totalProcessTimeByMethod;
    }
}

Lớp này sẽ lưu lại số lần gọi hàm và tổng thời gian gọi hàm.
Sau đó chúng ta có thể tạo lớp PerformanceAspect để gọi hàm và ghi nhận lại thời gian gọi như sau:

package vn.techmaster.aop;

import lombok.AllArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import vn.techmaster.aop.performance.PerformanceManager;

@Aspect
@Component
@AllArgsConstructor
public class PerformanceAspect {

    private final PerformanceManager performanceManager;

    @Around("execution(* vn.techmaster.aop.controller..*(..))")
    public Object measurePerformance(
        ProceedingJoinPoint joinPoint
    ) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long endTime = System.currentTimeMillis();
        long elapsedTime = endTime - startTime;
        String methodName = joinPoint.getSignature().getName();
        performanceManager.record(methodName, elapsedTime);
        return result;
    }
}

Trong mã nguồn này chúng ta sử dụng thêm một annotation mới là @Around để bọc lại việc gọi hàm và đo thời gian, hàm thực tế sẽ được gọi thông quan joinPoint.proceed(). Chạy lại lớp AopStartUp với mã nguồn:

package vn.techmaster.aop;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import vn.techmaster.aop.controller.UserController;
import vn.techmaster.aop.performance.PerformanceManager;

import java.math.BigDecimal;

@Configuration
@ComponentScan(basePackages = "vn.techmaster.aop")
@EnableAspectJAutoProxy
public class AopStartUp {

    public static void main(String[] args) {
        ApplicationContext applicationContext =
            new AnnotationConfigApplicationContext(
                "vn.techmaster.aop"
            );
        UserController userController = applicationContext
            .getBean(UserController.class);
        userController.updateBalance(
            "Dzung",
            BigDecimal.ZERO
        );
        userController.updateProfile("Dzung", "dung@techmaster.vn");
        PerformanceManager performanceManager = applicationContext
            .getBean(PerformanceManager.class);
        System.out.println(performanceManager.getSummary());
    }
}

Kết quả chúng ta nhận được sẽ là:

LoggingAspect: Before method updateBalance execution
LoggingAspect: After method updateBalance execution
LoggingAspect: Before method updateProfile execution
LoggingAspect: After method updateProfile execution
totalCallByMethod: {updateProfile=1, updateBalance=1}
totalProcessTimeByMethod: {updateProfile=0, updateBalance=0}

Vì các hàm updateProfile và updateBalance là các hàm rỗng không làm gì cả nên thời gian xử lý gần như bằng 0.

Spring tạo ra lớp proxy cho AOP thế nào?

Cũng tương tự như lớp HelloWordProxy, thì spring cũng cần tạo ra các lớp proxy để bọc lại các singleton đã được tạo. Cụ thể thì trong quá trình tạo singleton thì spring sẽ gọi đến hàm applyBeanPostProcessorsAfterInitialization:

public Object applyBeanPostProcessorsAfterInitialization(
    Object existingBean,
    String beanName
) throws BeansException {

    Object result = existingBean;
    for (BeanPostProcessor processor : getBeanPostProcessors()) {
       Object current = processor.postProcessAfterInitialization(result, beanName);
       if (current == null) {
          return result;
       }
       result = current;
    }
    return result;
}

Hàm này sẽ gọi đến các lớp cài đặt interface BeanPostProcessor. Cụ thể ở đây là lớp AnnotationAwareAspectJAutoProxyCreator thừa kế từ lớp AbstractAutoProxyCreator. Trong lớp AbstractAutoProxyCreator spring sẽ có hàm postProcessAfterInitialization:

public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
    if (bean != null) {
       Object cacheKey = getCacheKey(bean.getClass(), beanName);
       if (this.earlyBeanReferences.remove(cacheKey) != bean) {
          return wrapIfNecessary(bean, beanName, cacheKey);
       }
    }
    return bean;
}

Và đây là cách mà spring tạo ra lớp proxy. Sâu ở bên trong nữa thì spring sẽ gọi đến thư viện aspectjweaver để tạo proxy, và thư viện aspectjweaver lại sử dụng CGLIB, một thư viện java byte code để tạo và biên dịch class trong runtime, tuy nhiên trọng phạm vi bài này, mình sẽ không đề cập sâu hơn nữa.

Tổng kết

Như vậy chúng ta đã cùng nhau:

  1. Tìm hiểu về AOP.
  2. Cấu hình dự án để có thể sử dụng spring-aop.
  3. Sử dụng AOP để log và đo hiệu năng của các hàm.
  4. Các mà spring tạo ra lớp proxy cho AOP.

Sách tham khảo

Các bạn có thể tham khảo proxy design pattern trong cuốn làm chủ các mẫu thiết kế kinh điển trong lập trình và đừng quên nhập mã giảm giá Tech10 để được giảm giá 10% nhé.


Cám ơn bạn đã quan tâm đến bài viết|video này. Để nhận được thêm các kiến thức bổ ích bạn có thể:

  1. Đọc các bài viết của TechMaster trên facebook: https://www.facebook.com/techmastervn
  2. Xem các video của TechMaster qua Youtube: https://www.youtube.com/@TechMasterVietnam nếu bạn thấy video/bài viết hay bạn có thể theo dõi kênh của TechMaster để nhận được thông báo về các video mới nhất nhé.
  3. Chat với techmaster qua Discord: https://discord.gg/yQjRTFXb7a