Mybatis插件原理以及实现自定义Sql拦截器插件

一、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 [email protected]
 * @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 [email protected]
 * @version 1.0
 * @name
 * @description
 * @date 2018/8/19
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface CreateTime {
    String value() default "";
}


/**
 * @author [email protected]
 * @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 [email protected]
 * @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

猜你喜欢

转载自blog.csdn.net/jiaqingShareing/article/details/81840657