一、Mybatis插件基本原理
Mybatis允许你在已经映射语句的执行过程中为某一点进行拦截调用。但是并不是对所有的方法都可以进行这种拦截处理,允许使用插件进行的拦截类如下,具体要拦截该类中哪一个方法则需要关注下mybatis的源码,去指定拦截时的方法入参:支持的拦截类如下:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
这四个对象之间的关系为:
如下图展示了Mybatis源码的Executor接口的update方法的入参形式:
自定义拦截器需要让我们类实现Interceptor接口:
@Intercepts({@Signature(type = Executor.class, method ="update", args = {MappedStatement.class, Object.class})})
public class ExamplePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
returninvocation.proceed();
}
@Override
public Object plugin(Object target) {
returnPlugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
每一个拦截器都必须实现上面的三个方法,其中:
- Object intercept(Invocation invocation)是实现拦截逻辑的地方,内部要通过invocation.proceed()显式地推进责任链前进,也就是调用下一个拦截器拦截目标方法。
- Object plugin(Object target) 就是用当前这个拦截器生成对目标target的代理,实际是通过Plugin.wrap(target,this) 来完成的,把目标target和拦截器this传给了包装函数。
- setProperties(Properties properties)用于设置额外的参数,参数配置在拦截器的Properties节点里。
注解里描述的是指定拦截方法的签名 [type,method,args] (即对哪种对象的哪种方法进行拦截),它在拦截前用于决断。比如在上面的代码中,我们要做的是对executor类的update方法进行拦截,并且该方法的入参为:MappedStatement和Object参数。
二、插件设计思路
其实,插件涉及到的是一种责任链的模式,责任链模式是一种对象行为模式,在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链,请求在这个链上传递,直到链上的某一个对象决定处理此请求。
三、自定义拦截器拦截查询Sql
- 定义拦截器类 实现显示sql的输出量仅为1条,拦截到执行的sql再对该sql进行改写
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.util.Properties;
/**
* @author jiaqing.xu@hand-china.com
* @version 1.0
* @name
* @description
* @date 2018/8/19
*/
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class SqlStatusInterceptor implements Interceptor {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler=(StatementHandler)invocation.getTarget();
MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
//获取sql
String sql= String.valueOf(metaStatementHandler.getValue("delegate.boundSql.sql"));
//添加limit条件
sql="select * from (" + sql + ") as temp limit 1";
//重新设置sql
metaStatementHandler.setValue("delegate.boundSql.sql",sql);
return invocation.proceed();
}
@Override
public Object plugin(Object o) {
return Plugin.wrap(o, this);
}
@Override
public void setProperties(Properties properties) {
String dialect = properties.getProperty("dialect");
logger.info("mybatis intercept dialect:{}", dialect);
}
}
- 在项目的mybatis-cfg.xml的mybatis配置文件中配置该拦截器的plugins标签,告诉mybatis插件的全限定类名
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 引入配置文件 -->
<properties resource="mysql.properties"></properties>
<!-- 为Java实体设置类别名 -->
<typeAliases>
<package name="com.hand.dto"/>
</typeAliases>
<plugins>
<plugin interceptor="com.hand.interceptor.SqlInsertInterceptor"></plugin>
</plugins>
<!-- 配置mybatis运行环境 -->
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<!-- 为mybatis的映射文件mapper.xml设置路径 -->
<mappers>
<package name="com/hand/dao"/>
</mappers>
</configuration>
四、自定义拦截器实现在新增/更新操作时动态为sql注入creationDate以及lastUpdateDate时间字段
- 定义拦截器时运行时注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author jiaqing.xu@hand-china.com
* @version 1.0
* @name
* @description
* @date 2018/8/19
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface CreateTime {
String value() default "";
}
/**
* @author jiaqing.xu@hand-china.com
* @version 1.0
* @name
* @description
* @date 2018/8/19
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface UpdateTime {
String value() default "";
}
- 拦截器的核心代码
import com.hand.annotation.CreateTime;
import com.hand.annotation.UpdateTime;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
import java.util.Date;
import java.util.Properties;
/**
* @author jiaqing.xu@hand-china.com
* @version 1.0
* @name
* @description
* @date 2018/8/19
*/
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class SqlInsertInterceptor implements Interceptor {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
// 获取 SQL 命令
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
logger.info("获取到的sql命令为:{}",sqlCommandType);
// 获取参数
Object parameter = invocation.getArgs()[1];
if (parameter != null) {
// 获取成员变量
Field[] declaredFields = parameter.getClass().getDeclaredFields();
for (Field field : declaredFields) {
if (field.getAnnotation(CreateTime.class) != null) {
if (SqlCommandType.INSERT.equals(sqlCommandType)) { // insert 语句插入 createTime
field.setAccessible(true);
if (field.get(parameter) == null) {
field.set(parameter, new Date());
}
}
}
if (field.getAnnotation(UpdateTime.class) != null) { // insert 或 update 语句插入 updateTime
if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
field.setAccessible(true);
if (field.get(parameter) == null) {
field.set(parameter, new Date());
}
}
}
}
}
return invocation.proceed();
}
@Override
public Object plugin(Object o) {
return Plugin.wrap(o,this);
}
@Override
public void setProperties(Properties properties) {
}
}
- Mybatis配置文件中添加插件
<plugins>
<plugin interceptor="com.hand.interceptor.SqlInsertInterceptor"></plugin>
</plugins>
- 数据库表中数据变化展示
通过这种自定义拦截器的方式,我们就可以实现在新增数据库记录或者删除数据库记录时,动态地添加我们需要的字段的值了,是不是很方便呢?当然,除了这种实现思路,偶们也亦可以利用Spring的AOP机制,合理地设置切点pointCut实现方法的拦截设置,也可以达到同样的实现效果。
本博文的源码地址为:https://download.csdn.net/download/jiaqingshareing/10614036