MyBatis Plus 源码解析:动态SQL生成、插件机制与拦截器高级应用

MyBatis Plus 源码解析:动态SQL生成、插件机制与拦截器高级应用

各位同学,大家好!今天我们来深入探讨 MyBatis Plus 的源码,重点关注其动态 SQL 生成、插件机制以及拦截器的高级应用。MyBatis Plus (简称 MP) 在 MyBatis 的基础上做了增强,极大地简化了开发,但同时也隐藏了一些底层实现细节。理解这些细节对于更好地使用 MP,甚至进行定制化开发至关重要。

一、动态SQL生成:抽象与扩展

动态 SQL 是 MyBatis 的核心特性之一。MP 在 MyBatis 的基础上,进一步封装了动态 SQL 的生成过程,使其更加简洁易用。

1.1 核心接口:AbstractWrapperSqlHelper

MP 动态 SQL 生成的核心是 AbstractWrapper 抽象类及其子类,如 QueryWrapperUpdateWrapper。这些 Wrapper 类负责构建 SQL 的 WHERESET 等部分。

SqlHelper 类则提供了一些静态方法,用于处理 SQL 相关的通用逻辑,如安全字段检查、SQL 片段的拼接等。

1.2 动态 SQL 生成流程

QueryWrapper 为例,当我们调用 eq() 方法时,实际上是构建了一个 SqlSegment 对象,该对象包含了字段名、运算符和值。这些 SqlSegment 对象会被添加到 QueryWrapper 的内部列表中。

最终,在执行 SQL 查询时,MP 会将这些 SqlSegment 对象转换为实际的 SQL 片段,并拼接到完整的 SQL 语句中。

1.3 源码分析:QueryWrapper.eq()

// QueryWrapper.java
public QueryWrapper<T> eq(R column, Object val) {
    return addCondition(true, column, SqlKeyword.EQ, val);
}

protected QueryWrapper<T> addCondition(boolean condition, R column, SqlKeyword sqlKeyword, Object val) {
    if (condition) {
        final String sqlColumn = columnToString(column);
        sqlSegment.add(new NormalSqlSegment(sqlKeyword.getSqlSegment() + " " + sqlColumn + " " + Constants.QUESTION, val));
    }
    return typedThis;
}

这段代码展示了 eq() 方法的核心逻辑:

  1. 判断条件是否成立 (condition)。
  2. 将字段名转换为 SQL 字段名 (columnToString())。
  3. 创建一个 NormalSqlSegment 对象,该对象包含了 SQL 关键字 (=)、字段名和占位符 (?),以及对应的值 (val)。
  4. NormalSqlSegment 对象添加到 sqlSegment 列表中。

1.4 自定义 SQL 片段

MP 允许我们自定义 SQL 片段,以满足更复杂的需求。我们可以通过实现 ISqlSegment 接口来创建自定义的 SQL 片段。

// 自定义 SQL 片段
public class MySqlSegment implements ISqlSegment {

    private final String sql;

    public MySqlSegment(String sql) {
        this.sql = sql;
    }

    @Override
    public String getSqlSegment() {
        return sql;
    }
}

// 使用自定义 SQL 片段
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.add(new MySqlSegment("AND age > 18"));

二、插件机制:扩展与定制

MP 的插件机制允许我们在不修改 MP 源码的情况下,扩展其功能。

2.1 核心接口:Interceptor

Interceptor 接口是 MP 插件机制的核心。我们只需要实现该接口,并将其注册到 MP 的配置中,就可以在特定的时机执行自定义的逻辑。

2.2 拦截点

MP 提供了多个拦截点,例如:

  • StatementHandler:拦截 SQL 语句的执行过程。
  • ParameterHandler:拦截 SQL 参数的设置过程。
  • ResultSetHandler:拦截结果集的处理过程。
  • Executor:拦截 SQL 的执行过程。

2.3 源码分析:MybatisPlusInterceptor

MybatisPlusInterceptor 是 MP 提供的默认拦截器,它负责加载并执行所有配置的插件。

// MybatisPlusInterceptor.java
public class MybatisPlusInterceptor implements Interceptor {

    private final List<InnerInterceptor> interceptors;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        if (target instanceof StatementHandler) {
            return this.processStatementHandler(invocation, (StatementHandler) target);
        } else if (target instanceof ResultSetHandler) {
            return this.processResultSetHandler(invocation, (ResultSetHandler) target);
        } else if (target instanceof ParameterHandler) {
            return this.processParameterHandler(invocation, (ParameterHandler) target);
        } else if (target instanceof Executor) {
            return this.processExecutor(invocation, (Executor) target);
        } else {
            return invocation.proceed();
        }
    }

    // ...
}

这段代码展示了 MybatisPlusInterceptor 的核心逻辑:

  1. 获取被拦截的目标对象 (target)。
  2. 判断目标对象的类型,并根据类型调用不同的处理方法。
  3. 如果目标对象不是 MP 支持的类型,则直接调用 invocation.proceed(),继续执行原有的逻辑。

2.4 自定义插件

我们可以通过实现 Interceptor 接口来创建自定义的插件。例如,我们可以创建一个插件,用于自动填充创建时间和更新时间。

// 自动填充插件
@Intercepts({@Signature(
        type = Executor.class,
        method = "update",
        args = {MappedStatement.class, Object.class}
)})
public class AutoFillInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        Object parameter = invocation.getArgs()[1];

        // 获取实体类
        Class<?> entityClass = parameter.getClass();

        // 获取当前时间
        LocalDateTime now = LocalDateTime.now();

        // 填充创建时间
        try {
            Field createTimeField = entityClass.getDeclaredField("createTime");
            createTimeField.setAccessible(true);
            if (createTimeField.get(parameter) == null) {
                createTimeField.set(parameter, now);
            }
        } catch (NoSuchFieldException e) {
            // Ignore
        }

        // 填充更新时间
        try {
            Field updateTimeField = entityClass.getDeclaredField("updateTime");
            updateTimeField.setAccessible(true);
            updateTimeField.set(parameter, now);
        } catch (NoSuchFieldException e) {
            // Ignore
        }

        return invocation.proceed();
    }

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

三、拦截器高级应用

拦截器不仅可以用于扩展功能,还可以用于实现一些高级应用,例如:

3.1 SQL 性能监控

我们可以通过拦截 StatementHandler,记录 SQL 语句的执行时间,从而实现 SQL 性能监控。

// SQL 性能监控插件
@Intercepts({@Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
)})
public class PerformanceInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        long startTime = System.currentTimeMillis();
        try {
            return invocation.proceed();
        } finally {
            long endTime = System.currentTimeMillis();
            long elapsedTime = endTime - startTime;
            BoundSql boundSql = statementHandler.getBoundSql();
            String sql = boundSql.getSql();
            System.out.println("SQL: " + sql);
            System.out.println("Elapsed time: " + elapsedTime + "ms");
        }
    }

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

3.2 数据权限控制

我们可以通过拦截 StatementHandler,修改 SQL 语句,添加数据权限的过滤条件,从而实现数据权限控制。

// 数据权限控制插件
@Intercepts({@Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
)})
public class DataPermissionInterceptor implements Interceptor {

    private final String userId;

    public DataPermissionInterceptor(String userId) {
        this.userId = userId;
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = statementHandler.getBoundSql();
        String sql = boundSql.getSql();

        // 添加数据权限过滤条件
        String modifiedSql = sql + " AND user_id = '" + userId + "'";
        ReflectUtil.setFieldValue(boundSql, "sql", modifiedSql);

        return invocation.proceed();
    }

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

3.3 敏感数据加密

我们可以通过拦截 ParameterHandler,对敏感数据进行加密,从而保护数据的安全性。

// 敏感数据加密插件
@Intercepts({@Signature(
        type = ParameterHandler.class,
        method = "setParameters",
        args = {PreparedStatement.class}
)})
public class EncryptionInterceptor implements Interceptor {

    private final String encryptionKey;

    public EncryptionInterceptor(String encryptionKey) {
        this.encryptionKey = encryptionKey;
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
        PreparedStatement preparedStatement = (PreparedStatement) invocation.getArgs()[0];
        Object parameterObject = parameterHandler.getParameterObject();

        // 加密敏感数据
        if (parameterObject instanceof User) {
            User user = (User) parameterObject;
            String phone = user.getPhone();
            if (phone != null) {
                String encryptedPhone = encrypt(phone, encryptionKey);
                user.setPhone(encryptedPhone);
                // 通过反射修改PreparedStatement中的参数值
                // ... (具体实现需要根据MyBatis版本和PreparedStatement的内部结构进行调整)
            }
        }

        return invocation.proceed();
    }

    private String encrypt(String data, String key) {
        // 实现加密逻辑 (例如使用AES)
        // ...
        return "encrypted_" + data; // 示例,实际需要使用真实的加密算法
    }

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

四、表格总结

特性 核心类/接口 功能描述 应用场景
动态SQL生成 AbstractWrapper, SqlHelper 封装动态 SQL 的生成过程,提供简洁易用的 API。 复杂的查询条件、动态更新字段等。
插件机制 Interceptor 允许在不修改 MP 源码的情况下,扩展其功能。 自动填充、SQL 性能监控、数据权限控制、敏感数据加密等。
拦截点 StatementHandler, ParameterHandler, ResultSetHandler, Executor 定义了可以拦截的目标对象和方法。 可以根据不同的拦截点,实现不同的功能。例如,拦截 StatementHandler 可以修改 SQL 语句,拦截 ParameterHandler 可以修改 SQL 参数。
拦截器高级应用 自定义 Interceptor 实现更高级的功能,例如 SQL 性能监控、数据权限控制、敏感数据加密等。 可以根据实际需求,定制各种各样的拦截器,以满足不同的业务需求。

五、深入理解,灵活应用

通过今天的讲解,我们深入了解了 MyBatis Plus 的动态 SQL 生成、插件机制以及拦截器的高级应用。希望大家能够掌握这些知识,并在实际项目中灵活应用,提升开发效率和代码质量。理解这些底层机制,能够帮助我们更好地使用MP,甚至可以针对特定需求进行定制化开发,发挥MP的最大价值。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注