前言

Mybatis拦截器注解的基本写法格式为:

@Intercepts({
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})

说明:
@Intercepts:标识该类是一个拦截器
@Signature:拦截器相关属性设置

type

拦截器的类型,总共有4种,按照执行的先后顺序为:

  1. Executor:拦截执行器的方法。
  2. ParameterHandler:拦截参数的处理。
  3. ResultHandler:拦截结果集的处理。
  4. StatementHandler:拦截Sql语法构建的处理,绝大部分我们是在这里设置我们的拦截器。

图片

method

可被拦截的方法,按照拦截器的不同类型,总共有下面这些方法:

拦截的类型 拦截的方法
Executor update, query, flushStatements, commit, rollback,getTransaction, close, isClosed
ParameterHandler getParameterObject, setParameters
StatementHandler prepare, parameterize, batch, update, query
ResultSetHandler handleResultSets, handleOutputParameters

args

设置对应接口中的哪一个方法,比如 Executorquery方法因为重载原因,方法有多个,args 就是指明参数类型,从而确定是哪一个方法。

这里的方法和上面的方法的要怎么理解呢?可以这样理解,上面的那个方法(method)是大类的方法,比如update方法,下面的这个方法(args)呢,是具体的方法,用来指明你到底的哪里的update方法。

注册拦截器

使用配置文件

<?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项即可-->
<configuration>
    <settings>
        <!--是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射。 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    <plugins>
        <plugin interceptor="com.example.IbatisInterceptor"/>
    </plugins>
</configuration>

然后在主配置文件中,将mybatis配置文件引入:

# Mybatis配置
mybatis:
  config-location: classpath:mybatis/config/mybatis-config.xml # mybatis配置文件,如果使用configuration的话就不需要mybatis配置文件了

todo:
mybatis的配置项中,有一个interceptors配置,其参数是一个列表(java.util.List)。通过查阅相关信息发现,不能在配置文件中引用自定义的拦截器。总之是暂时没找到 具体使用方法,先放着吧。

# Mybatis配置
mybatis:
#  config-location: classpath:mybatis/config/mybatis-config.xml # mybatis配置文件,如果使用configuration的话就不需要mybatis配置文件了
  mapper-locations: classpath:mybatis/*.xml # mybatis映射文件
  configuration: # mybatis全局配置
    map-underscore-to-camel-case: true #开启驼峰命名规则
    interceptors:
      - xxx
      - xxxxx

使用配置类

import com.example.IbatisInterceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;


@Configuration
public class IbatisInterceptorConfig {

    @Bean
    public String MyBatisInterceptor(SqlSessionFactory sqlSessionFactory) {
        IbatisInterceptor ibatisInterceptor = new IbatisInterceptor();
        Properties properties = new Properties();
        properties.setProperty("driver", "mysql");
        properties.setProperty("username", "root");
        properties.setProperty("password", "123456");

        // 给拦截器添加自定义参数
        ibatisInterceptor.setProperties(properties);
        sqlSessionFactory.getConfiguration().addInterceptor(ibatisInterceptor);
        return "interceptor";
    }

}

使用注解

直接在拦截器类上面使用@Component注解即可(本文中就是直接使用的注解)

使用拦截器

使用mybatis拦截器,需要实现Interceptor接口的三个方法:

  1. intercept():当方法被拦截时调用,用于设置拦截后需要执行的业务逻辑,自定义拦截器时,此方法是必须实现的。
  2. plugin():在mybatis加载配置时被调用。设置是否需要对对象进行拦截
  • 拦截对象:调用wrap()方法,返回一个代理对象,并执行intercept()方法
  • 不拦截对象:直接返回目标原本对象,不会执行intercept()方法
  1. setProperties():在mybatis加载配置时被调用。获取并设置mybatis配置文件或配置类中的property属性的值,配置类见上面注册拦截器时的演示

mybatis配置文件演示:

   <environments default="mysql">
        <environment id="mysql">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <!--mybatis配置项,上面的setProperties()方法中获取的propertys就是这里的-->
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/test"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>

原理图:
图片

简单使用

以拦截执行器方法为例。
示例:拦截更新操作,在更新操作时统一添加 “更新时间 ”字段,同时打印当前sql语句。

从语义角度分析:
insert、delete、update都是属于更新(update)操作;
而select属于查询(query)操作。


import com.example.bean.User;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
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 org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.sql.Date;
import java.time.LocalDateTime;
import java.util.Properties;


@Component
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class IbatisInterceptor implements Interceptor {

    private Logger logger = LoggerFactory.getLogger(IbatisInterceptor.class);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        Object[] args = invocation.getArgs();
        SqlCommandType sqlCommandType = null;

        for (Object object : args) {

            // 从MappedStatement中获取到操作类型
            if (object instanceof MappedStatement) {
                MappedStatement mappedStatement = (MappedStatement) object;
                sqlCommandType = mappedStatement.getSqlCommandType();

                // 获取当前擦操作的sql语句
                BoundSql boundSql = mappedStatement.getBoundSql(object);
                String sql = boundSql.getSql();

                // 打印当前操作类型及当前操作的sql语句
                logger.info("操作类型: {},操作语句:{}", sqlCommandType, sql);
            }
            // 判断参数的类型是否和实体类一致(这里只有一个参数,所以直接用if判断)
            if (object instanceof User) {
                if (SqlCommandType.UPDATE == sqlCommandType) {
                    // 由于没有写额外的获取实体类属性的方法,所以这里只能利用反射机制,获取需要修改的字段对应的实体类属性
                    Field updateTime = object.getClass().getDeclaredField("updateTime");
                    updateTime.setAccessible(true);
                    // 对数据进行修改
                    updateTime.set(object, Date.valueOf(LocalDateTime.now().toLocalDate()));
                }

            }
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        //  拦截属于Executor的对象
        if (target instanceof Executor) {
            // 返回代理
            return Plugin.wrap(target, this);
        }
        //  否则返回对象本身
        return target;
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

拦截多个请求

示例:只做简单示例,不涉及业务逻辑


import com.example.bean.User;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.sql.PreparedStatement;
import java.util.Properties;


@Component
@Intercepts({@Signature(type = Executor.class,method = "query", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "createCacheKey", args = {MappedStatement.class, Object.class, RowBounds.class, BoundSql.class}),
        @Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),

})
public class IbatisInterceptor implements Interceptor {

    private Logger logger = LoggerFactory.getLogger(IbatisInterceptor.class);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        Object[] args = invocation.getArgs();
        SqlCommandType sqlCommandType = null;

        for (Object object : args) {

            // 从MappedStatement中获取到操作类型
            if (object instanceof MappedStatement) {
                MappedStatement mappedStatement = (MappedStatement) object;
                sqlCommandType = mappedStatement.getSqlCommandType();

                // 判断参数的类型是否和实体类一致(这里只有一个参数,所以直接用if判断)
                if (object instanceof User) {
                    if (SqlCommandType.UPDATE == sqlCommandType) {
//                   执行更新操作修改
                    } else if (SqlCommandType.SELECT == sqlCommandType) {
//                    执行查询操作修改
                    } else if (SqlCommandType.DELETE == sqlCommandType) {
//                    执行删除操作修改
                    } else if (SqlCommandType.INSERT == sqlCommandType) {
//                    执行插入操作修改
                    }
                }
            }else if(object instanceof PreparedStatement){
                // 从PreparedStatement中获取到操作类型
                PreparedStatement preparedStatement =  (PreparedStatement) object;
//                doSomething
            }
            // 判断参数的类型是否和实体类一致(这里只有一个参数,所以直接用if判断)
                if (object instanceof User) {
                    if (SqlCommandType.UPDATE == sqlCommandType) {
//                   执行更新操作修改
                    } else if (SqlCommandType.SELECT == sqlCommandType) {
//                    执行查询操作修改
                    } else if (SqlCommandType.DELETE == sqlCommandType) {
//                    执行删除操作修改
                    } else if (SqlCommandType.INSERT == sqlCommandType) {
//                    执行插入操作修改
                    }
                }
        }
        return invocation.proceed();
    }
  
    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        }
        return target;
    }
  
    @Override
    public void setProperties(Properties properties) {
//        String username = properties.getProperty("username");
//        String password = properties.getProperty("password");
    }
}

拦截并修改sql语句

从上面的示例中我们能够发现,是可以在拦截器中获取到sql语句的,那么我们就可以通过反射修改sql语句。
将通过id删除数据的sql语句改为通过name删除的sql语句:
有两种方法:
第一种是通过Statement修改

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.plugin.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.Properties;


@Component
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class IbatisInterceptor implements Interceptor {

    private Logger logger = LoggerFactory.getLogger(IbatisInterceptor.class);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        Object[] args = invocation.getArgs();
        SqlCommandType sqlCommandType = null;

        for (Object object : args) {

            // 从MappedStatement中获取到操作类型
            if (object instanceof MappedStatement) {
                MappedStatement mappedStatement = (MappedStatement) object;
                sqlCommandType = mappedStatement.getSqlCommandType();

                // 获取当前擦操作的sql语句
                BoundSql boundSql = mappedStatement.getBoundSql(object);
                String sql = boundSql.getSql();

                // 打印当前操作类型及当前操作的sql语句
                logger.info("操作类型: {},操作语句:{}", sqlCommandType, sql);

                // 设置新的sql语句
                String newSql = "delete from users where u_name=?";
              
                /*
                * 通过statement修改sql语句
                */
      
                // 重新new一个查询语句对像
                BoundSql newBoundSql = new BoundSql(mappedStatement.getConfiguration(), newSql, boundSql.getParameterMappings(), boundSql.getParameterObject());

                // 把新的查询放到statement里
                MappedStatement neMappedStatement = copyFromMappedStatement(mappedStatement, new BoundSqlSqlSource(newBoundSql));
                for (ParameterMapping mapping : boundSql.getParameterMappings()) {
                    String prop = mapping.getProperty();
                    if (boundSql.hasAdditionalParameter(prop)) {
                        newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
                    }
                }

                final Object[] queryArgs = invocation.getArgs();
                // 这个i就是设置你需要修改哪个拦截的请求,对应上面的@Intercepts注解中的数组中的元素(@Signature注解)对应的索引
                // 因为我上面只有一个(@Signature注解的)元素,对应的索引自然也就是0
                int i = 0;
                queryArgs[i] = neMappedStatement;
            }
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        //  拦截属于Executor的对象
        if (target instanceof Executor) {
            // 返回代理
            return Plugin.wrap(target, this);
        }
        //  否则返回对象本身
        return target;
    }

    @Override
    public void setProperties(Properties properties) {

    }

    private MappedStatement copyFromMappedStatement(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(ms.getKeyProperties()[0]);
        }
        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();
    }

    public static class BoundSqlSqlSource implements SqlSource {
        private BoundSql boundSql;

        public BoundSqlSqlSource(BoundSql boundSql) {
            this.boundSql = boundSql;
        }

        public BoundSql getBoundSql(Object parameterObject) {
            return boundSql;
        }
    }
}


第二种是直接通过反射修改:


import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
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 org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.Properties;


@Component
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class IbatisInterceptor implements Interceptor {

    private Logger logger = LoggerFactory.getLogger(IbatisInterceptor.class);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        Object[] args = invocation.getArgs();
        SqlCommandType sqlCommandType = null;

        for (Object object : args) {

            // 从MappedStatement中获取到操作类型
            if (object instanceof MappedStatement) {
                MappedStatement mappedStatement = (MappedStatement) object;
                sqlCommandType = mappedStatement.getSqlCommandType();

                // 获取当前擦操作的sql语句
                BoundSql boundSql = mappedStatement.getBoundSql(object);
                String sql = boundSql.getSql();

                // 打印当前操作类型及当前操作的sql语句
                logger.info("操作类型: {},操作语句:{}", sqlCommandType, sql);

                // 设置新的sql语句
                String newSql = "delete from users where u_name=?";

                // 通过反射修改sql语句
                Field field = boundSql.getClass().getDeclaredField("sql");
                field.setAccessible(true);
                field.set(boundSql, newSql);
              
            }
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        //  拦截属于Executor的对象
        if (target instanceof Executor) {
            // 返回代理
            return Plugin.wrap(target, this);
        }
        //  否则返回对象本身
        return target;
    }

    @Override
    public void setProperties(Properties properties) {

    }

}

最后来个图吧:
图片

知识积累
将Maven项目组件上传到Maven中心仓(借助sonatype)
2024/7/21
String.valueOf()踩坑记录
2024/7/21
使用Mapstruct轻松实现实体类转换
2024/7/21
发表评论