Cơ chế Interceptor và kiến trúc Plugin trong MyBatis

MyBatis Interceptor là một tính năng mạnh mẽ cho phép nhà phát triển can thiệp vào quá trình thực thi SQL mà không cần thay đổi mã nguồn cốt lõi của thư viện. Dựa trên mô hình Design Pattern "Decorator" và cơ chế Proxy của Java, MyBatis cung cấp khả năng mở rộng linh hoạt cho các tác vụ như ghi log, phân trang tự động, hoặc bảo mật dữ liệu.

1. Nguyên lý hoạt động của Plugin

MyBatis cho phép chúng ta chặn (intercept) các lời gọi hàm thuộc về 4 interface quan trọng sau:

  • Executor: Quản lý việc thực thi các câu lệnh SQL (update, query) và quản lý giao dịch, bộ nhớ đệm.
  • StatementHandler: Xử lý các tương tác trực tiếp với JDBC Statement như tạo câu lệnh, thiết lập tham số.
  • ParameterHandler: Chịu trách nhiệm chuyển đổi các tham số từ Java Object sang kiểu dữ liệu JDBC tương ứng.
  • ResultSetHandler: Chuyển đổi kết quả trả về từ ResultSet của JDBC thành các List hoặc Object trong Java.

Mỗi khi một trong các đối tượng này được tạo ra, MyBatis sẽ kiểm tra xem có Interceptor nào được đăng ký hay không. Nếu có, nó sẽ sử dụng JDK Dynamic Proxy để tạo ra một đối tượng đại diện (proxy) bao bọc đối tượng gốc.

2. Cấu trúc của một Interceptor

Để tạo một plugin, chúng ta cần triển khai interface org.apache.ibatis.plugin.Interceptor với 3 phương thức chính:

  • intercept(Invocation invocation): Nơi chứa logic xử lý chính. Bạn có thể thực hiện các thao tác trước và sau khi gọi invocation.proceed().
  • plugin(Object target): Xác định xem có bao bọc đối tượng mục tiêu bằng proxy hay không. Thông thường ta gọi Plugin.wrap(target, this).
  • setProperties(Properties properties): Nhận các tham số cấu hình được định nghĩa trong file XML của MyBatis.

3. Triển khai Interceptor giám sát hiệu năng SQL

Dưới đây là ví dụ về một Interceptor dùng để đo lường thời gian thực thi của các câu lệnh SQL nhằm phát hiện các truy vấn chậm (slow queries).

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.util.Properties;

@Intercepts({
    @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
    ),
    @Signature(
        type = Executor.class,
        method = "update",
        args = {MappedStatement.class, Object.class}
    )
})
public class PerformanceMonitorInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement statement = (MappedStatement) invocation.getArgs()[0];
        String commandId = statement.getId();

        long start = System.currentTimeMillis();
        
        // Thực thi câu lệnh SQL gốc
        Object result = invocation.proceed();
        
        long duration = System.currentTimeMillis() - start;

        System.out.println(String.format("[MyBatis Audit] Command: %s | Time: %dms", commandId, duration));
        
        return result;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // Tiếp nhận cấu hình từ file settings nếu cần
    }
}

4. Cách đăng ký Interceptor vào hệ thống

Cấu hình qua file XML (mybatis-config.xml)

<plugins>
    <plugin interceptor="com.example.interceptor.PerformanceMonitorInterceptor">
        <property name="threshold" value="1000"/>
    </plugin>
</plugins>

Cấu hình trong Spring Boot

Nếu bạn đang sử dụng Spring Boot, chỉ cần khai báo Interceptor như một Bean, MyBatis-Spring sẽ tự động phát hiện và đăng ký nó.

@Configuration
public class MyBatisConfig {

    @Bean
    public PerformanceMonitorInterceptor performanceMonitorInterceptor() {
        return new PerformanceMonitorInterceptor();
    }
}

5. Thứ tự thực thi của các Plugin

Khi có nhiều Interceptor được đăng ký, MyBatis sẽ áp dụng chúng theo cơ chế "Lồng nhau" (Nested). Interceptor nào được định nghĩa sau cùng trong file cấu hình (hoặc được thêm vào sau cùng trong code) sẽ là lớp bao bọc ngoài cùng (Outer Proxy).

Quy trình thực thi sẽ theo dạng: Interceptor 3 (Bắt đầu) -> Interceptor 2 (Bắt đầu) -> Interceptor 1 (Bắt đầu) -> Thực thi SQL gốc -> Interceptor 1 (Kết thúc) -> Interceptor 2 (Kết thúc) -> Interceptor 3 (Kết thúc).

6. Các kịch bản ứng dụng phổ biến

  • Phân trang (Pagination): Can thiệp vào StatementHandler để sửa đổi chuỗi SQL gốc bằng cách thêm LIMITOFFSET.
  • Đa thuở (Multi-tenancy): Tự động thêm điều kiện tenant_id vào mệnh đề WHERE của mọi câu lệnh SQL.
  • Mã hóa dữ liệu: Tự động mã hóa các trường nhạy cảm trong ParameterHandler và giải mã chúng trong ResultSetHandler.
  • Ghi log Audit: Lưu lại thông tin người dùng đã thực hiện thay đổi dữ liệu vào một bảng nhật ký hệ thống.

7. Một số lưu ý quan trọng

  • Hiệu năng: Vì mỗi Interceptor tạo ra một lớp Proxy, việc lạm dụng quá nhiều plugin hoặc viết logic quá phức tạp trong intercept có thể làm giảm tốc độ xử lý của ứng dụng.
  • Thread-safety: Interceptor được MyBatis khởi tạo dưới dạng Singleton. Tránh sử dụng các biến instance để lưu trữ trạng thái của một request cụ thể nhằm tránh tranh chấp dữ liệu giữa các luồng.
  • Kiến thức về nội bộ: Để viết được Interceptor hiệu quả, bạn cần hiểu rõ cấu trúc của các đối tượng nội bộ như BoundSql, MappedStatement hay MetaObject để truy xuất và sửa đổi dữ liệu đúng cách.

Thẻ: mybatis Java orm Interceptor performance

Đăng vào ngày 22 tháng 8 lúc 18:34