MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
- Executor:拦截执行器的方法,支持method:update, query, flushStatements, commit, rollback, getTransaction, close, isClosed
- ParameterHandler:拦截参数的处理,支持method:getParameterObject, setParameters
- ResultSetHandler:拦截结果集的处理,支持method: handleResultSets, handleOutputParameters
- StatementHandler:拦截Sql语法构建的处理,支持method: prepare, parameterize, batch, update, query
自定义拦截器
1.创建自定义拦截器
创建一个类,实现org.apache.ibatis.plugin.Interceptor
接口,重写public Object intercept(Invocation invocation) throws Throwable
方法
public class MybatisExecuteTimePlugin implements Interceptor{
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 不做处理返回invocation.proceed()即可
return null;
}
}
2.添加拦截器注解
在类上添加org.apache.ibatis.plugin.Intercepts
注解,在Intercepts
中添加org.apache.ibatis.plugin.Signature
注解,Signature
可以定义多个。
Signature
-
type:上述拦截器的Class,如Executor.class、StatementHandler.class等
-
method:执行方法,拦截器对应的方法
-
args: 参数
- MappedStatement.class,
- Object.class
- RowBounds.class
- ResultHandler.class
- CacheKey.class
- BoundSql.class
Invocation
invocation有三个属性:target、method、args,这三个属性分别对应Signature
中的type,method,args,其中args的顺序与Signature
中的args顺序相同
Object.class
args参数表中,Object.class是特殊的对象类型。如果有数据库统一的实体 Entity 类,即包含表公共字段,比如创建、更新操作对象和时间的基类等,在编写代码时尽量依据该对象来操作,会简单很多。
Object parameter = invocation.getArgs()[1];
if (parameter instanceof BaseEntity) {
BaseEntity entity = (BaseEntity) parameter;
}
如果参数不是实体,而且具体的参数,那么 Mybatis 也做了一些处理,比如 @Param("name") String name
类型的参数,会被包装成 Map
接口的实现来处理,即使是原始的 Map
也是如此。
Object parameter = invocation.getArgs()[1];
if (parameter instanceof Map) {
Map map = (Map) parameter;
}
SqlCommandType 命令类型
Executor
提供的方法中,update
包含了 新增,修改和删除类型,无法直接区分,需要借助 MappedStatement
类的属性 SqlCommandType
来进行判断,该类包含了所有的操作类型
public enum SqlCommandType {
UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
}
毕竟新增和修改的场景,有些参数是有区别的,比如创建时间和更新时间,update
时是无需兼顾创建时间字段的
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
SqlCommandType commandType = ms.getSqlCommandType();
3.添加拦截器
代码为PageHelper
的自动装配代码
@Configuration
@ConditionalOnBean(SqlSessionFactory.class)
@EnableConfigurationProperties(PageHelperProperties.class)
@AutoConfigureAfter(MybatisAutoConfiguration.class)
public class PageHelperAutoConfiguration {
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
@Autowired
private PageHelperProperties properties;
/**
* 接受分页插件额外的属性
*
* @return
*/
@Bean
@ConfigurationProperties(prefix = PageHelperProperties.PAGEHELPER_PREFIX)
public Properties pageHelperProperties() {
return new Properties();
}
@PostConstruct
public void addPageInterceptor() {
PageInterceptor interceptor = new PageInterceptor();
Properties properties = new Properties();
//先把一般方式配置的属性放进去
properties.putAll(pageHelperProperties());
//在把特殊配置放进去,由于close-conn 利用上面方式时,属性名就是 close-conn 而不是 closeConn,所以需要额外的一步
properties.putAll(this.properties.getProperties());
interceptor.setProperties(properties);
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
}
}
}
如果只有一个拦截器的话,直接在拦截器中类上添加Spring的注解即可
测试
查询
修改SQL,统计执行时间,打印SQL
测试SQL
<select id="select" resultMap="BaseResultMap">
select * from wf_workflow_status where process_instance_id = #{processInstanceId,jdbcType=VARCHAR} and PROCESS_NAME = #{young,jdbcType=VARCHAR} and APPROVE_NAME = #{young,jdbcType=VARCHAR}
</select>
拦截器代码
@Slf4j
@Component
@Intercepts({@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class MybatisExecuteTimePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
log.info("invocation.targetName:{}", invocation.getTarget().getClass().getName());
log.info("invocation.methodName:{}", invocation.getMethod().getName());
Object[] args = invocation.getArgs();
MappedStatement mappedStatement = (MappedStatement) args[0];
BoundSql sql = mappedStatement.getBoundSql(args[1]);
// 获取xml中的参数列表
List<ParameterMapping> list = sql.getParameterMappings();
// 获取Ognl上下文
OgnlContext context =
(OgnlContext) Ognl.createDefaultContext(sql.getParameterObject(), new DefaultMemberAccess(true));
RowBounds rowBounds = (RowBounds) args[2];
Executor executor = (Executor) invocation.getTarget();
// 创建CacheKey
CacheKey cacheKey = executor.createCacheKey(mappedStatement, args[1], rowBounds, sql);
// 拼接新SQL
BoundSql boundSql =
new BoundSql(mappedStatement.getConfiguration(), "select * from (" + sql.getSql() + ")", list, args[1]);
ResultHandler resultHandler = (ResultHandler) args[3];
long start = System.currentTimeMillis();
try {
// 执行SQL
Object proceed = executor.query(mappedStatement, args[1], rowBounds, resultHandler, cacheKey, boundSql);
return proceed;
} finally {
// 获取参数值
List<String> params = new ArrayList<>(list.size());
for (ParameterMapping mapping : list) {
params.add(getValue(mapping, context));
}
// 获取执行的MapperId
String mapperId = mappedStatement.getId();
// 打印执行SQL及执行时间
log.info("{}====>SQL:{};===>cost:{}(ms)", mapperId, formatSql(boundSql.getSql(), params),
(System.currentTimeMillis() - start));
}
}
private String formatSql(String sql, List<String> values) {
sql = sql.replaceAll("\n", "").replaceAll("\t", "");
for (String value : values) {
sql = sql.replaceFirst("\\?", value);
}
return sql;
}
private String getValue(ParameterMapping mapping, OgnlContext context) throws OgnlException {
JdbcType jdbcType = mapping.getJdbcType();
Object value = Ognl.getValue(Ognl.parseExpression(mapping.getProperty()), context, context.getRoot());
if (value == null) {
return "";
} else if (jdbcType == JdbcType.VARCHAR) {
return " '" + value + "' ";
} else if (jdbcType == JdbcType.DATE || jdbcType == JdbcType.TIMESTAMP) {
return " '" + new SimpleDateFormat("yyyy-MM-dd HH42:mm:ss").format((Date) value) + "' ";
} else {
return value.toString();
}
}
}
输出日志
2022-01-19 09:56:11.622 DEBUG 3496 --- [ main] c.e.t.dao.WorkflowStatusMapper.select : ==> Preparing: select * from (select * from wf_workflow_status where process_instance_id = ? and PROCESS_NAME = ? and APPROVE_NAME = ?)
2022-01-19 09:56:11.703 DEBUG 3496 --- [ main] c.e.t.dao.WorkflowStatusMapper.select : ==> Parameters: 1(String), young(String), young(String)
2022-01-19 09:56:11.816 DEBUG 3496 --- [ main] c.e.t.dao.WorkflowStatusMapper.select : <== Total: 0
2022-01-19 09:56:11.826 INFO 3496 --- [ main] c.e.t.plugins.MybatisExecuteTimePlugin : com.example.testmybatisplugins.dao.WorkflowStatusMapper.select====>SQL:select * from (select * from wf_workflow_status where process_instance_id = '1' and PROCESS_NAME = 'young' and APPROVE_NAME = 'young' );===>cost:994(ms)
参考:
https://www.cnblogs.com/fangjian0423/p/mybatis-interceptor.html
https://segmentfault.com/a/1190000040485072
https://segmentfault.com/a/1190000017393523
https://github.com/pagehelper/Mybatis-PageHelper
https://github.com/jkuhnert/ognl/blob/master/src/test/java/ognl/DefaultMemberAccess.java
https://mybatis.org/mybatis-3/zh/configuration.html#plugins