自定义Mybatis拦截器与动态SQL的完美结合
MyBatis的插件主要分为四大类,分别拦截四大核心对象:Executor、StatementHandler、ParameterHandler、ResultSetHandler。这些插件可以用来实现多种功能,例如性能监控、事务处理、安全控制等。
Executor 拦截器:
介绍说明: Executor 拦截器主要用于拦截数据库的执行器,它负责执行 MyBatis 的 SQL 语句。
作用: Executor 拦截器可以拦截执行器的 update(写操作)和 query(读操作)方法,使你能够在执行 SQL 语句前后注入自定义逻辑。
使用场景: 适用于需要在数据库写入或读取操作前后执行额外逻辑的情况,比如日志记录、性能监控等。
StatementHandler 拦截器:
介绍说明: StatementHandler 拦截器主要用于拦截 SQL 语句的处理,包括 SQL 语句的创建和参数的设置。
作用: 可以在 SQL 语句执行之前对其进行修改,也可以拦截参数的设置过程。
使用场景: 适用于需要在 SQL 语句执行前对其进行动态修改或在参数设置时执行特定逻辑的场景。
ParameterHandler 拦截器:
介绍说明: ParameterHandler 拦截器主要用于拦截参数的设置过程。
作用: 允许你拦截参数设置的过程,可以在执行 SQL 语句前修改参数的值。
使用场景: 适用于需要在执行 SQL 语句前对参数进行额外处理的情况,例如参数加密、验证等。
ResultSetHandler 拦截器:
介绍说明: ResultSetHandler 拦截器主要用于拦截结果集的处理过程。
作用: 可以在 MyBatis 处理查询结果集之前或之后执行自定义逻辑。
使用场景: 适用于需要对查询结果集进行额外处理的情况,例如结果集的转换、过滤等。
通过MyBatis提供的强大机制,使用插件是非常简单的,只需实现Interceptor 接口,并指定想要拦截的方法签名即可。
代码实践
这是自定义拦截器的核心接口。要创建一个自定义拦截器,你需要实现此接口
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
intercept() 方法允许你在 MyBatis 执行的每个方法周围执行逻辑。
plugin() 方法用于为目标对象创建代理。
setProperties() 方法允许你从 XML 配置中设置自定义属性。
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.select.FromItem;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.example.mysql.entity.TimeEntity;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Properties;
@Slf4j
@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}),
@Signature(type = Executor.class, method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
public class IbatisAuditDataInterceptor implements Interceptor {
private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取当前执行的SQL语句
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
/**
* 通过MetaObject优雅访问对象的属性,这里是访问MappedStatement的属性;
* MetaObject是Mybatis提供的一个用于方便、优雅访问对象属性的对象,通过它可以简化代码
* 不需要try/catch各种reflect异常,同时它支持对JavaBean、Collection、Map三种类型对象的操作。
*/
MetaObject msObject = MetaObject.forObject(ms, new DefaultObjectFactory(), new DefaultObjectWrapperFactory(),new DefaultReflectorFactory());
Object parameterObject = args[1];
BoundSql boundSql = ms.getBoundSql(parameterObject);
String sql = boundSql.getSql();
log.info("原SQL:{}", sql.replaceAll("\\n", ""));
// 获取当前执行的SQL语句的操作类型
SqlCommandType sqlCommandType = ms.getSqlCommandType();
//解析SQL语句
Statement statement = CCJSqlParserUtil.parse(sql);
//请求头获取用户权限PID
HttpServletRequest request = getHttpServletRequest();
String pid = request.getHeader("pid");
if(sqlCommandType == SqlCommandType.SELECT){
Select selectStatement = (Select) statement;
PlainSelect plain = (PlainSelect) selectStatement.getSelectBody();
FromItem fromItem = plain.getFromItem();
StringBuffer whereSql = new StringBuffer();
if (fromItem.getAlias() != null) {
whereSql.append(fromItem.getAlias().getName()).append(".pid = ").append(pid);
} else {
whereSql.append("pid = ").append(pid);
}
Expression where = plain.getWhere();
if (where != null) {
whereSql.append(" and ( " + where + " )");
}
Expression expression = CCJSqlParserUtil.parseCondExpression(whereSql.toString());
plain.setWhere(expression);
BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), selectStatement.toString(), boundSql.getParameterMappings(), boundSql.getParameterObject());
MappedStatement mappedStatement = copyFromMappedStatement(ms, new BoundSqlSqlSource(newBoundSql));
args[0] = mappedStatement;
log.info("现SQL:{}", selectStatement.toString().replaceAll("\\n", ""));
}else if(sqlCommandType == SqlCommandType.INSERT){
if(parameterObject instanceof TimeEntity){
BeanUtils.setProperty(parameterObject, "time", dtf.format(LocalDateTime.now()));
BeanUtils.setProperty(parameterObject, "pid", pid);
}
}else if(sqlCommandType == SqlCommandType.UPDATE){
if(parameterObject instanceof TimeEntity){
BeanUtils.setProperty(parameterObject, "time", dtf.format(LocalDateTime.now()));
}
Update updateStatement = (Update) statement;
Expression where = updateStatement.getWhere();
StringBuffer whereSql = new StringBuffer();
if (where != null) {
whereSql.append("pid = ").append(pid).append(" and ( " + where + " )");
}else{
whereSql.append("pid = ").append(pid);
}
Expression expression = CCJSqlParserUtil.parseCondExpression(whereSql.toString());
updateStatement.setWhere(expression);
BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), updateStatement.toString(), boundSql.getParameterMappings(), boundSql.getParameterObject());
MappedStatement mappedStatement = copyFromMappedStatement(ms, new BoundSqlSqlSource(newBoundSql));
args[0] = mappedStatement;
log.info("现SQL:{}", updateStatement.toString().replaceAll("\\n", ""));
}else if(sqlCommandType == SqlCommandType.DELETE){
Delete deleteStatement = (Delete) statement;
Expression where = deleteStatement.getWhere();
StringBuffer whereSql = new StringBuffer();
if (where != null) {
whereSql.append("pid = ").append(pid).append(" and ( " + where + " )");
}else{
whereSql.append("pid = ").append(pid);
}
Expression expression = CCJSqlParserUtil.parseCondExpression(whereSql.toString());
deleteStatement.setWhere(expression);
BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), deleteStatement.toString(), boundSql.getParameterMappings(), boundSql.getParameterObject());
MappedStatement mappedStatement = copyFromMappedStatement(ms, new BoundSqlSqlSource(newBoundSql));
args[0] = mappedStatement;
log.info("现SQL:{}", deleteStatement.toString().replaceAll("\\n", ""));
}
return invocation.proceed();
}
@Override
public Object plugin(Object o) {
return Plugin.wrap(o, this);
}
@Override
public void setProperties(Properties properties) {
}
private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
MappedStatement.Builder builder = new MappedStatement.Builder(
ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());
builder.resource(ms.getResource());
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.keyGenerator(ms.getKeyGenerator());
if(ms.getKeyProperties() != null && ms.getKeyProperties().length > 0)builder.keyProperty(String.join(",", ms.getKeyProperties()));
builder.timeout(ms.getTimeout());
builder.parameterMap(ms.getParameterMap());
builder.resultMaps(ms.getResultMaps());
builder.resultSetType(ms.getResultSetType());
builder.cache(ms.getCache());
builder.flushCacheRequired(ms.isFlushCacheRequired());
builder.useCache(ms.isUseCache());
return builder.build();
}
private static class BoundSqlSqlSource implements SqlSource {
private final BoundSql boundSql;
BoundSqlSqlSource(BoundSql boundSql) {
this.boundSql = boundSql;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
return boundSql;
}
}
private HttpServletRequest getHttpServletRequest() {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
return sra.getRequest();
}
}
配置拦截器:
@Configuration
public class MybatisPlusConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sessionFactory.setMapperLocations(resolver.getResources("classpath*:/mapper/**/*.xml"));
//map接收返回值值为null的问题,默认是当值为null,将key返回
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setCallSettersOnNulls(true);
// configuration.setLogImpl(StdOutImpl.class);
IbatisAuditDataInterceptor interceptor = new IbatisAuditDataInterceptor();
configuration.addInterceptor(interceptor);
sessionFactory.setConfiguration(configuration);
return sessionFactory.getObject();
}
}
测试:
总结
MyBatis插件的核心是拦截器,它能够捕获并处理特定的事件,比如SQL语句的执行、参数的处理等。通过这种方式,插件可以实现各种功能,比如性能监控、事务处理、安全控制等。
MyBatis插件的使用非常灵活,开发者可以根据实际需求选择不同的插件,并将其应用到MyBatis的四大核心对象中。这种插件机制不仅提高了MyBatis的灵活性,也使得开发者可以更加方便地对框架进行扩展和定制。
此外,MyBatis插件的实现原理是基于动态代理的,也就是说,MyBatis的四大核心对象实际上都是代理对象。这种机制使得插件可以在不修改原有代码的基础上,对核心对象进行拦截和处理。
总的来说,MyBatis插件是一种非常强大的扩展机制,它使得开发者可以更加灵活地使用MyBatis框架,并且可以根据实际需求对框架进行定制和扩展。