Mybatis拦截器

young 552 2022-01-19

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