MyBatis Plus 源码解析:动态SQL生成、插件机制与拦截器高级应用
各位同学,大家好!今天我们来深入探讨 MyBatis Plus 的源码,重点关注其动态 SQL 生成、插件机制以及拦截器的高级应用。MyBatis Plus (简称 MP) 在 MyBatis 的基础上做了增强,极大地简化了开发,但同时也隐藏了一些底层实现细节。理解这些细节对于更好地使用 MP,甚至进行定制化开发至关重要。
一、动态SQL生成:抽象与扩展
动态 SQL 是 MyBatis 的核心特性之一。MP 在 MyBatis 的基础上,进一步封装了动态 SQL 的生成过程,使其更加简洁易用。
1.1 核心接口:AbstractWrapper
与 SqlHelper
MP 动态 SQL 生成的核心是 AbstractWrapper
抽象类及其子类,如 QueryWrapper
和 UpdateWrapper
。这些 Wrapper 类负责构建 SQL 的 WHERE
、SET
等部分。
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()
方法的核心逻辑:
- 判断条件是否成立 (
condition
)。 - 将字段名转换为 SQL 字段名 (
columnToString()
)。 - 创建一个
NormalSqlSegment
对象,该对象包含了 SQL 关键字 (=
)、字段名和占位符 (?
),以及对应的值 (val
)。 - 将
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
的核心逻辑:
- 获取被拦截的目标对象 (
target
)。 - 判断目标对象的类型,并根据类型调用不同的处理方法。
- 如果目标对象不是 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的最大价值。