插件(plugins)
MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。
/**
* 默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
*
* Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
* ParameterHandler (getParameterObject, setParameters)
* ResultSetHandler (handleResultSets, handleOutputParameters)
* StatementHandler (prepare, parameterize, batch, update, query)
*/
@Slf4j
@Intercepts({
// method = "query"拦截select方法、而method = "update"则能拦截insert、update、delete的方法
@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})
})
// 只需实现 Interceptor 接口,并指定想要拦截的方法签名即可
public class MybatisInterceptorPlugin implements Interceptor {
//需要处理的表
private static final List<String> TABLE_LIST = Arrays.asList("model_meta");
// 1.通过Invocation中的args变量。我们能拿到MappedStatement这个对象(args[0]),传入sql语句的参数Object (args[1])。
@Override
public Object intercept(Invocation invocation) throws Throwable {
final Object[] args = invocation.getArgs();
// 获取 MappedStatement对象 和 参数对象
MappedStatement ms = (MappedStatement) args[0];
Object parameterObject = args[1];
// sql命令类型
SqlCommandType sqlCommandType = ms.getSqlCommandType();
try {
switch (sqlCommandType) {
case INSERT:
tableName = args[1].getClass().getAnnotation(Table.class).name();
if (!TABLE_LIST.contains(tableName)) {
return invocation.proceed();
}
// 自己的业务操作
break;
case UPDATE:
Field table = Example.class.getDeclaredField("table");
table.setAccessible(true);
tableName = ((EntityTable) table.get(((MapperMethod.ParamMap) args[1]).get("example"))).getName();
if(!checkUpdateParam(args[1])) {
return invocation.proceed();
}
if (!TABLE_LIST.contains(tableName)) {
return invocation.proceed();
}
break;
case SELECT:
// 2. sqlSource对象和传入sql语句的参数对象Object就能获得BoundSql。BoundSql的toString方法就能获取到有占位符的sql语句了,我们的业务逻辑就能在这里介入。
// 获取预编译sql对象
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 获取到拥有占位符的sql语句
String sql = boundSql.getSql();
SQLExprTableSource sqlTableSource = getTableNameBySql(sql);
if(sqlTableSource == null) {
break;
}
tableName = sqlTableSource.toString();
if (!TABLE_LIST.contains(tableName)) {
break;
}
// 3.获取到sql语句,根据规则替换表名,塞回BoundSql对象中、再把BoundSql对象塞回MappedStatement对象中。最后再赋值给args[0](实际被拦截方法所需的参数)就搞定了
String newSql = sql.replace(tableName, tableName + "_publish_view");
//重新生成一个BoundSql对象
BoundSql bs = new BoundSql(ms.getConfiguration(), newSql, boundSql.getParameterMappings(), parameterObject);
// start 解决There is no getter for property named '__frch_criterion_1' in 报错
for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
String property = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(property)) {
bs.setAdditionalParameter(property, boundSql.getAdditionalParameter(property));
}
}
// end 解决There is no getter for property named '__frch_criterion_1' in 报错
//重新生成一个MappedStatement对象
MappedStatement newMs = copyMappedStatement(ms, new BoundSqlSqlSource(bs));
//赋回给实际执行方法所需的参数中
args[0] = newMs;
default:
break;
}
} catch (Exception e) {
log.error("拦截,invocation:{}", invocation.toString(), e);
}
return invocation.proceed();
}
public static SQLExprTableSource getTableNameBySql(String sql) {
MySqlStatementParser mySqlStatementParser = new MySqlStatementParser(sql);
SQLStatement statement = mySqlStatementParser.parseStatement();
SQLExprTableSource sqlTableSource = null;
if(statement instanceof SQLSelectStatement){
SQLSelect selectQuery = ((SQLSelectStatement)statement).getSelect();
MySqlSelectQueryBlock sqlSelectQuery = (MySqlSelectQueryBlock)selectQuery.getQuery();
sqlTableSource = (SQLExprTableSource)sqlSelectQuery.getFrom();
// 可以获取where条件对象 SQLBinaryOpExpr
// selectList
}else if(statement instanceof SQLInsertStatement){
SQLInsertStatement sqlInsertStatement = (SQLInsertStatement)statement;
sqlTableSource = sqlInsertStatement.getTableSource();
}else if(statement instanceof SQLUpdateStatement){
SQLUpdateStatement sqlUpdateStatement = (SQLUpdateStatement)statement;
sqlTableSource = (SQLExprTableSource)sqlUpdateStatement.getTableSource();
}else if(statement instanceof SQLDeleteStatement){
SQLDeleteStatement sqlUpdateStatement = (SQLDeleteStatement)statement;
sqlTableSource = (SQLExprTableSource)sqlUpdateStatement.getTableSource();
}
return sqlTableSource;
}
/***
* 复制一个新的MappedStatement
* @param ms
* @param newSqlSource
* @return
*/
private MappedStatement copyMappedStatement (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();
}
/**
* MappedStatement构造器接受的是SqlSource
* 实现SqlSource接口,将BoundSql封装进去
*/
public static class BoundSqlSqlSource implements SqlSource {
private BoundSql boundSql;
public BoundSqlSqlSource(BoundSql boundSql) {
this.boundSql = boundSql;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
return boundSql;
}
}
}
@Configuration
@ConditionalOnBean({SqlSessionFactory.class})
public class MybatisConfig {
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
@PostConstruct
public void envInterceptor() {
MybatisInterceptorPlugin envInterceptor = new MybatisInterceptorPlugin();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
sqlSessionFactory.getConfiguration().addInterceptor(envInterceptor);
}
}
}