如何使用Mybatis的拦截器?
MyBatis 拦截器是 MyBatis 提供的一个强大特性,它允许你在 MyBatis 执行其核心逻辑的关键节点插入自定义逻辑,从而改变 MyBatis 的默认行为。这类似于面向切面编程(AOP),允许开发者在不改变原有代码的情况下,增强或修改原有功能。
在 MyBatis 中,拦截器可以应用于以下四种类型的对象:
Executor:负责 MyBatis 中的 SQL 执行过程,拦截 Executor 可以在 SQL 执行前后添加自定义逻辑。
Executor 的核心职责
在 MyBatis 中,Executor
接口定义了数据库操作的核心方法,如 update
、query
、commit
、rollback
等。MyBatis 提供了几种 Executor
的实现,例如 SimpleExecutor
、ReuseExecutor
、BatchExecutor
等,它们各自有不同的特点和用途。
以 SimpleExecutor
为例,它是最基本的 Executor
实现,每次操作都会创建一个新的 Statement
对象。
源码解析
以下是 Executor
接口中 query
方法的一个简化版实现,展示了 MyBatis 如何执行一个查询操作:
public class SimpleExecutor extends BaseExecutor {
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 准备 SQL
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
}
这段代码展示了查询操作的基本流程:创建 StatementHandler
,准备和参数化 Statement
,执行查询,最后关闭 Statement
。
拦截器的实现和应用
拦截器允许开发者在 MyBatis 核心操作执行的关键节点插入自定义逻辑。拦截器需要实现 Interceptor
接口,并定义要拦截的目标和方法。
当 MyBatis 初始化时,它会检测配置中的拦截器,并在执行相应操作时调用这些拦截器。拦截器中的 intercept
方法将在目标方法执行时被调用。
public class ExampleExecutorInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 在 SQL 执行前的逻辑
System.out.println("Before executing query");
// 执行原有逻辑
Object returnValue = invocation.proceed();
// 在 SQL 执行后的逻辑
System.out.println("After executing query");
return returnValue;
}
}
在这个例子中,intercept
方法实现了在查询执行前后打印消息的逻辑。invocation.proceed()
是关键,它触发了原本的操作(如查询)。通过这种方式,开发者可以在不修改原有代码的基础上增强或改变 MyBatis 的行为。
ParameterHandler:负责 MyBatis 中的参数处理,通过拦截 ParameterHandler,可以在 SQL 语句绑定参数前后添加自定义逻辑。
ParameterHandler 的工作原理
ParameterHandler
的主要职责是为 SQL 语句绑定正确的参数值。在执行 SQL 之前,MyBatis 会通过 ParameterHandler
接口的实现类来遍历方法传入的参数,并将它们设置到 JDBC 的 PreparedStatement
中。
MyBatis 默认提供了 DefaultParameterHandler
类作为 ParameterHandler
接口的实现,用于处理参数的设置工作。
源码解析
下面是一个简化的 ParameterHandler
使用示例,演示了如何在 MyBatis 中处理参数绑定:
public class DefaultParameterHandler implements ParameterHandler {
private final MappedStatement mappedStatement;
private final Object parameterObject;
private final BoundSql boundSql;
private final TypeHandlerRegistry typeHandlerRegistry;
public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
this.mappedStatement = mappedStatement;
this.parameterObject = parameterObject;
this.boundSql = boundSql;
this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
}
@Override
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = mappedStatement.getConfiguration().newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
typeHandler.setParameter(ps, i + 1, value, jdbcType);
}
}
}
}
}
在上述代码中,setParameters
方法是核心,它负责将参数值绑定到 PreparedStatement
。首先,它会遍历所有的 ParameterMapping
,这些映射信息定义了如何从参数对象中获取具体的值。然后,它使用相应的 TypeHandler
来处理参数值的类型转换,并将转换后的值设置到 PreparedStatement
中。
拦截器的应用
通过拦截 ParameterHandler
的 setParameters
方法,可以在参数绑定前后插入自定义逻辑。例如,可以在参数绑定之前对参数进行日志记录,或者对参数值进行额外的处理。
@Intercepts({
@Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class)
})
public class ExampleParameterHandlerInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
// 在参数绑定前的自定义逻辑
System.out.println("Before parameter binding");
// 继续执行原逻辑
Object result = invocation.proceed();
// 在参数绑定后的自定义逻辑
System.out.println("After parameter binding");
return result;
}
}
在这个例子中,intercept
方法会在 setParameters
被调用时执行,允许开发者在参数被绑定到 SQL 语句之前和之后执行自定义代码。
ResultSetHandler:负责处理 JDBC 返回的 ResultSet 结果集,拦截 ResultSetHandler 可以在结果集处理前后添加自定义逻辑。
ResultSetHandler 的工作原理
当 MyBatis 执行查询操作后,会得到一个 ResultSet
,ResultSetHandler
的职责就是遍历这个 ResultSet
,并将其行转换为 Java 对象。MyBatis 中默认的 ResultSetHandler
实现是 DefaultResultSetHandler
。
源码解析
下面是 DefaultResultSetHandler
处理结果集的一个简化版示例,帮助理解其工作机制:
public class DefaultResultSetHandler implements ResultSetHandler {
private final MappedStatement mappedStatement;
private final RowBounds rowBounds;
public DefaultResultSetHandler(MappedStatement mappedStatement, RowBounds rowBounds) {
this.mappedStatement = mappedStatement;
this.rowBounds = rowBounds;
}
@Override
public <E> List<E> handleResultSets(Statement stmt) throws SQLException {
ResultSet rs = stmt.getResultSet();
List<E> resultList = new ArrayList<>();
while (rs.next()) {
E resultObject = getRowValue(rs);
resultList.add(resultObject);
}
return resultList;
}
private <E> E getRowValue(ResultSet rs) throws SQLException {
// 实际的结果对象映射逻辑
// 这里通常会涉及到 ResultMap 的处理
return ...;
}
}
在这个简化的例子中,handleResultSets
方法遍历 ResultSet
,对每一行调用 getRowValue
方法将其转换为一个 Java 对象,最终返回一个对象列表。
拦截器的应用
通过拦截 ResultSetHandler
的 handleResultSets
方法,开发者可以在结果集被处理成 Java 对象前后插入自定义逻辑。这可以用于额外的结果处理,比如对查询结果的后处理或审计日志记录等。
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = Statement.class)
})
public class ExampleResultSetHandlerInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 在结果集处理前的逻辑
System.out.println("Before handling result sets");
// 执行原有逻辑
Object result = invocation.proceed();
// 在结果集处理后的逻辑
System.out.println("After handling result sets");
return result;
}
}
在这个示例中,intercept
方法在 handleResultSets
被调用时执行,允许在结果集转换为 Java 对象的前后执行自定义代码。
StatementHandler:负责对 JDBC Statement 的操作,通过拦截 StatementHandler,可以在 SQL 语句被执行前后添加自定义逻辑。
StatementHandler 的工作原理
StatementHandler
主要有三个实现类:SimpleStatementHandler
、PreparedStatementHandler
和 CallableStatementHandler
,分别对应于 JDBC 的 Statement
、PreparedStatement
和 CallableStatement
。这些类处理 SQL 的不同执行方式,其中 PreparedStatementHandler
是最常用的,因为它支持参数化的 SQL 语句,有助于提高性能和安全性。
源码解析
以下是 StatementHandler
接口的一个简化版实现(PreparedStatementHandler
),演示了 MyBatis 如何准备和执行 SQL 语句:
public class PreparedStatementHandler implements StatementHandler {
private final MappedStatement mappedStatement;
private final Object parameterObject;
private final BoundSql boundSql;
private final ParameterHandler parameterHandler;
public PreparedStatementHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
this.mappedStatement = mappedStatement;
this.parameterObject = parameterObject;
this.boundSql = boundSql;
this.parameterHandler = mappedStatement.getConfiguration().newParameterHandler(mappedStatement, parameterObject, boundSql);
}
@Override
public PreparedStatement prepare(Connection connection) throws SQLException {
String sql = boundSql.getSql();
PreparedStatement pstmt = connection.prepareStatement(sql);
parameterHandler.setParameters(pstmt);
return pstmt;
}
@Override
public int update(PreparedStatement pstmt) throws SQLException {
pstmt.execute();
return pstmt.getUpdateCount();
}
@Override
public <E> List<E> query(PreparedStatement pstmt, ResultHandler resultHandler) throws SQLException {
pstmt.execute();
return resultSetHandler.handleResultSets(pstmt);
}
}
在这个简化的实现中,prepare
方法负责创建 PreparedStatement
并通过 ParameterHandler
设置参数。update
和 query
方法则用于执行 SQL 语句并处理执行结果。
拦截器的应用
通过拦截 StatementHandler
的方法,可以在 SQL 语句执行的关键节点加入自定义逻辑。例如,可以拦截 prepare
方法,在 SQL 语句被准备之前或之后添加逻辑:
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class ExampleStatementHandlerInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 在 SQL 语句准备前的逻辑
System.out.println("Before preparing the SQL statement");
// 执行原有逻辑
Object result = invocation.proceed();
// 在 SQL 语句准备后的逻辑
System.out.println("After preparing the SQL statement");
return result;
}
}
在这个示例中,intercept
方法会在 prepare
方法执行时被调用,允许开发者在 SQL 语句被准备和执行前后插入自定义代码。
实际案例
让我们在一个 Spring Boot 项目中结合 MyBatis 的 Executor、ParameterHandler、ResultSetHandler、和 StatementHandler 构建一个实际案例。我们将创建一个简单的用户管理系统,其中包含用户的添加、查询功能,并通过拦截器在关键节点添加日志记录、性能监控等自定义逻辑。
步骤 1: 定义实体和映射文件
首先,定义一个 User
实体:
public class User {
private Integer id;
private String name;
private String email;
// Getters and setters...
}
然后,创建一个 MyBatis 映射文件 UserMapper.xml
:
<mapper namespace="com.example.mybatisdemo.mapper.UserMapper">
<insert id="insertUser" parameterType="User">
INSERT INTO users (name, email) VALUES (#{name}, #{email})
</insert>
<select id="getUserById" parameterType="int" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
</mapper>
步骤 2: 定义 Mapper 接口
@Mapper
public interface UserMapper {
void insertUser(User user);
User getUserById(int id);
}
步骤 3: 定义拦截器
- Executor 拦截器 - 记录执行时间:
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class ExecutorInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = invocation.proceed();
long endTime = System.currentTimeMillis();
System.out.println("Execution time: " + (endTime - startTime) + "ms");
return result;
}
}
- ParameterHandler 拦截器 - 记录参数信息:
@Intercepts({
@Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class})
})
public class ParameterHandlerInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
System.out.println("Parameters: " + parameterHandler.getParameterObject());
return invocation.proceed();
}
}
- ResultSetHandler 拦截器 - 在结果集后打印日志:
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class ResultSetHandlerInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object result = invocation.proceed();
System.out.println("Result: " + result);
return result;
}
}
- StatementHandler 拦截器 - 修改 SQL 语句:
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class StatementHandlerInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler handler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
System.out.println("Original SQL: " + sql);
// 此处可根据需要修改 sql
// String modifiedSql = sql.replace(...);
return invocation.proceed();
}
}
步骤 4: 注册拦截器
在 Spring Boot 配置类中注册拦截器:
@Configuration
public class MyBatisConfig {
@Bean
public ExecutorInterceptor executorInterceptor() {
return new ExecutorInterceptor();
}
@Bean
public ParameterHandlerInterceptor parameterHandlerInterceptor() {
return new ParameterHandlerInterceptor();
}
@Bean
public ResultSetHandlerInterceptor resultSetHandlerInterceptor() {
return new ResultSetHandlerInterceptor();
}
@Bean
public StatementHandlerInterceptor statementHandlerInterceptor() {
return new StatementHandlerInterceptor();
}
}
步骤 5: 使用 Mapper 进行操作
在你的服务层或控制器中,使用 UserMapper
来执行
数据库操作:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void createUser(User user) {
userMapper.insertUser(user);
}
public User getUser(int id) {
return userMapper.getUserById(id);
}
}
当你执行 createUser
或 getUser
方法时,你的拦截器将会被触发,你可以看到控制台上打印的相关信息,以及 MyBatis 如何处理 SQL 操作以及如何通过拦截器介入这些过程。
转载自:https://juejin.cn/post/7343138527418941475