欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

MyBatis 拦截器 - 项目中使用

程序员文章站 2022-06-28 18:30:04
一、MyBatis 拦截器介绍二、自定义拦截器类2.1、Interceptor接口2.2、@Intercepts注解三、项目使用3.1、jar3.2、xml配置代码涉及知识点:1、辅助类2、@PostConstruct 注解...

一、MyBatis 拦截器介绍

MyBatis拦截器设计初衷为了供用户在某些时候不动原有逻辑,通过拦截某些方法的调用,拦截的方法执行前后进添加逻辑。当然,也可以执行自己的逻辑,不执行被拦截的方法。

Mybatis核心对象 解释
SqlSession 作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能
Executor MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护
StatementHandler 封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合
ParameterHandler 负责对用户传递的参数转换成JDBC Statement 所需要的参数
ResultSetHandler 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;
TypeHandler 负责java数据类型和jdbc数据类型之间的映射和转换
MappedStatement MappedStatement维护了一条mapper.xml文件里面 select 、update、delete、insert节点的封装
SqlSource 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
BoundSql 表示动态生成的SQL语句以及相应的参数信息
Configuration MyBatis所有的配置信息都维持在Configuration对象之中

二、自定义拦截器类

实现Interceptor接口,自定义拦截器类上添加@Intercepts注解。

2.1、Interceptor接口
public interface Interceptor {

    /**
     * 代理对象每次调用的方法,就是要进行拦截的时候要执行的方法。在这个方法里面做我们自定义的逻辑处理
     */
    Object intercept(Invocation invocation) throws Throwable;

    /**
     * plugin方法是拦截器用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理
     *
     * 当返回的是代理的时候我们可以对其中的方法进行拦截来调用intercept方法 -- Plugin.wrap(target, this)
     * 当返回的是当前对象的时候 就不会调用intercept方法,相当于当前拦截器无效
     */
    Object plugin(Object target);

    /**
     * 用于在Mybatis配置文件中指定一些属性的,注册当前拦截器的时候可以设置一些属性
     */
    void setProperties(Properties properties);

}
2.2、@Intercepts注解

Intercepts注解需要一个Signature(拦截点)参数数组。通过Signature来指定拦截哪个对象里面的哪个方法。
MyBatis 拦截器 - 项目中使用
Signature来指定咱们需要拦截那个类对象的哪个方法。
MyBatis 拦截器 - 项目中使用
自定义一个MybatisInterceptor类,来拦截Executor类里面的update。
MyBatis 拦截器 - 项目中使用

三、项目使用

3.1、jar
		<!-- DAO: MyBatis -->
        <!-- <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId>
            <version>3.3.0</version> </dependency> <dependency> <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId> <version>1.2.3</version> </dependency> -->
        <!-- mybatis-plus 依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>3.1.2</version>
        </dependency>
        <!--扩展包 - 解决 NoClassDefFoundError: org/mybatis/logging/LoggerFactory-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-extension</artifactId>
            <version>3.1.2</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>  
            <artifactId>lombok</artifactId>   
            <version>1.16.18</version>     
            <scope>provided</scope>
        </dependency>
3.2、xml配置
	<!-- 3、配置mybatis-plus的sqlSessionFactory -->
    <bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <!-- 扫描sql配置文件:mapper需要的xml文件 -->
		<property name="mapperLocations" value="classpath:mapper/*.xml" />
        <property name="plugins">
        	<array>
        		<!-- 集成PageHelper分页插件 -->
				<bean class="com.github.pagehelper.PageHelper">
					<property name="properties">
						<value>dialect=mysql</value>
					</property>
				</bean>
        	 	<!-- 自带的 mybatis-plus性能拦截器 -->
				<!-- <bean id="performanceInterceptor" 
						class="com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor"/>-->
                <!-- 自定义 mybatis-plus性能拦截器,兼打印sql,不建议生产环境配置  -->
                <bean id="mybatisInterceptor"
                       class="com.common.MybatisInterceptor"/>
            </array>
        </property>
    </bean>
代码
/**
 * 参考文献:http://www.yangxuwang.com/jingyan/1533818219451005
 * <p>
 * 定义自己的Interceptor最重要的是要实现plugin方法和intercept方法,在plugin方法中我们可以决定是否要进行拦截进而决定要返回
 * 一个什么样的目标对象。而intercept方法就是要进行拦截的时候要执行的方法。
 * <p>
 * 对于实现自己的Interceptor而言有两个很重要的注解,一个是@Intercepts,其值是一个@Signature数组。@Intercepts用于表明当前的对象是一个Interceptor,
 * 而@Signature则表明要拦截的接口、方法以及对应的参数类型
 * Mybatis支持对Executor、StatementHandler、PameterHandler和ResultSetHandler进行拦截,也就是说会对这4种对象进行代理
 */

/**
 * method:表示拦截的方法,mybatis支持的方法有 update, query, flushStatements, commit, rollback, getTransaction, close, isClosed
 * 方法,其中,update包括新增、修改、删除等方法,query用于查询,其它的基本用不到。
 * args:表示拦截的参数类型,有MappedStatement、Object、RowBounds和ResultHandler等等.
 * type:表示拦截的类,有Executor、StatementHandler、ParameterHandler和ResultSetHandler。
 *
 * @author Xin
 */

@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})})
public class MybatisInterceptor implements Interceptor {

    /**
     * intercept方法就是要进行拦截的时候要执行的方法
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();

        MappedStatement ms = (MappedStatement) args[0];
        ms.getStatementType();
        // 当前SQL使用的是哪个Mapper,即哪个Mapper类
        String mapper = ms.getResource();
        System.out.println("mapper: " + mapper); // file [D:\svn\bidding\target\classes\mapper\AdminMapper.xml]
        Configuration configuration = ms.getConfiguration();
        // 执行当前SQL的Mapper id,其组成 [ 类型.方法 ]
        String mapperID = ms.getId();

        System.out.println("SQL的Mapper id: " + mapperID); // com.jiuan.bidding.dao.AdminMapper.changeSalt
        // 获取当前执行的SQL使用哪个数据源,我这里的数据源组件使用的是Druid,如果使用c3p0或者其他,则需要查看相关API,一般来降一个项目可能会配多个数据源,但是数据源组件都会使用一个
        DruidDataSource dataSource = (DruidDataSource) configuration.getEnvironment().getDataSource();
        // 获取数据库的类型[即mysql,或者oracle等等]
        String dbType = dataSource.getDataSourceStat().getDbType();

        System.out.println("数据库的类型: " + dbType); // mysql

        // 存放的是SQL的参数[它是一个实例对象]
        Object parameterObject = args[1];
        Object target = invocation.getTarget();
        StatementHandler handler = configuration.newStatementHandler((Executor) target, ms, parameterObject, RowBounds.DEFAULT, null, null);

        System.out.println("SQL的参数: " + handler); // org.apache.ibatis.executor.statement.RoutingStatementHandler@11ac67cc
        /**
         * commandName.startsWith(增/删/改/查),可以得到crud的具体类型[得到的是大写的INSERT UPDATE]
         * method.getName()得到的name可能为update, query, flushStatements, commit, rollback, getTransaction, close, isClosed
         */
        String commandName = ms.getSqlCommandType().name();
        Method method = invocation.getMethod();
        String methodName = method.getName();

        System.out.println("增/删/改/查: " + commandName); // UPDATE  DELETE
        System.out.println("增/删/改/查: " + methodName); // update

        BoundSql boundSql = ms.getBoundSql(parameterObject);
        // 这个ParameterMapping表示当前SQL绑定的是哪些参数,及参数类型,但并不是参数本身
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        // 将参数值转成json字符串
        String parameterObjects = JSON.toJSONString(boundSql.getParameterObject());

        System.out.println("SQL绑定的是哪些参数,及参数类型:" + parameterMappings); //[ParameterMapping{property='salt', mode=IN, javaType=class java.lang.Object, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}, ParameterMapping{property='mm', mode=IN, javaType=class java.lang.Object, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}, ParameterMapping{property='oldsalt', mode=IN, javaType=class java.lang.Object, jdbcType=null, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}]
        System.out.println("参数值json字符串: " + parameterObjects);  // {"mm":"f9e2645255983eb44ce90ef10f2dbf51","salt":"2a8b30b479b549ce888569184ce1e553","oldsalt":"21903a20bb66484f9c3cb87bf7f1acdb","param3":"f9e2645255983eb44ce90ef10f2dbf51","param1":"2a8b30b479b549ce888569184ce1e553","param2":"21903a20bb66484f9c3cb87bf7f1acdb"}
        // "a3a36e56-4da0-444e-a37c-fc2bd85916e1"

        // 要拦截的SQL,通过拦截器的SQL 其不带参数
        String srcSQL = boundSql.getSql();
        // 返回拼装好参数的SQL
        String retSQL = formatSQL(srcSQL, dbType, parameterObjects);
        // 先执行当前的SQL方法,即通过当前拦截器的CRUD操作,因为我们要返回这个结果
        Object result = invocation.proceed();

        System.out.println("sql语句1: " + srcSQL); // UPDATE tb_bid_zyryxx SET SALT = ?, MM = ? WHERE SALT = ?
        System.out.println("sql语句2: " + retSQL); // UPDATE tb_bid_zyryxx SET SALT = ?, MM = ? WHERE SALT = ?
        System.out.println("result: " + result); // 1

        // 组装自己的SQL记录类
        BidderLog log = new BidderLog();
        // 记录SQL
        log.setId(IdUtil.getUUID());
        log.setUserid((String) session.getAttribute(Constants.SESSION_ORGAN_ID));
        log.setUsername((String) session.getAttribute(Constants.SESSION_ORGAN_NAME));
        log.setIp(InetAddress.getLocalHost().getHostAddress());
        log.setTime(new Date());
        // log.setModel(); 模块
         log.setContent(srcSQL + parameterObjects);
         bidderLogDao.insert(log);
        //记录影响行数
        // log.setResult(Integer.valueOf(Integer.parseInt(result.toString())));
        // 记录时间
        // log.setOperateDate(new Date());
        //TODO 还可以记录参数,或者单表id操作时,记录数据操作前的状态
        //获取insertSqlLog方法
        // ms = ms.getConfiguration().getMappedStatement("insertSqlLog");
        //替换当前的参数为新的ms
        // args[0] = ms;
        //insertSqlLog 方法的参数为 log
        args[1] = log;
        //执行insertSqlLog方法
        // invocation.proceed();

        // 返回拦截器拦截的执行结果
        return result;
    }

    /**
     * plugin方法是拦截器用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理。
     * 当返回的是代理的时候我们可以对其中的方法进行拦截来调用intercept方法,当然也可以调用其他方法
     * 对于plugin方法而言,其实Mybatis已经为我们提供了一个实现。Mybatis中有一个叫做Plugin的类,
     * 里面有一个静态方法wrap(Object target,Interceptor interceptor),通过该方法可以决定要返回的对象是目标对象还是对应的代理。
     */
    @Override
    public Object plugin(Object o) {
		// 只拦截Executor对象,减少目标被代理的次数
        if (o instanceof Executor) {
            return Plugin.wrap(o, this);
        }
        return o;
    }

    /**
     * setProperties方法是用于在Mybatis配置文件中指定一些属性的
     * 这个方法在Configuration初始化当前的Interceptor时就会执行
     */
    @Override
    public void setProperties(Properties prop) {
        String maxTime = prop.getProperty("maxTime");
        String format = prop.getProperty("format");
        if (StringUtils.isNotEmpty(maxTime)) {
            this.maxTime = Long.parseLong(maxTime);
        }
        if (StringUtils.isNotEmpty(format)) {
            this.format = Boolean.valueOf(format);
        }
    }

    /**
     * @describe: 组装SQL
     * @params:
     * @Author: Kanyun
     * @Date: 2018/8/22 10:53
     */
    public String formatSQL(String src, String dbType, String params) {
        // 要传入的SQLUtils的参数集合,实际上虽然泛型是Object,但其实都是基本数据类型
        List<Object> paramList = new ArrayList();
        // 有了JSON字符串我们就可以通过正则表达式得到参数了
        System.out.println(params);
        // 需要注意的是这个SQLUtils是Druid数据源中的一个工具类,因为有现成的拼sql的工具,所以我就不再重复造*了,如果你的项目并没有使用Druid,
        // 则需要将这个工具类加入到你的项目中
        String retSQL = SQLUtils.format(src, dbType, paramList);
        return retSQL;
    }

    /**
     * 获取此方法名的具体 Method
     *
     * @param clazz      class 对象
     * @param methodName 方法名
     * @return 方法
     */
    public Method getMethodRegular(Class<?> clazz, String methodName) {
        if (Object.class.equals(clazz)) {
            return null;
        }
        for (Method method : clazz.getDeclaredMethods()) {
            if (method.getName().equals(methodName)) {
                return method;
            }
        }
        return getMethodRegular(clazz.getSuperclass(), methodName);
    }

    /**
     * 获取sql语句开头部分
     *
     * @param sql ignore
     * @return ignore
     */
    private int indexOfSqlStart(String sql) {
        String upperCaseSql = sql.toUpperCase();
        Set<Integer> set = new HashSet<>();
        set.add(upperCaseSql.indexOf("SELECT "));
        set.add(upperCaseSql.indexOf("UPDATE "));
        set.add(upperCaseSql.indexOf("INSERT "));
        set.add(upperCaseSql.indexOf("DELETE "));
        set.remove(-1);
        if (CollectionUtils.isEmpty(set)) {
            return -1;
        }
        List<Integer> list = new ArrayList<>(set);
        list.sort(Comparator.naturalOrder());
        return list.get(0);
    }

}

涉及知识点:

1、辅助类

Mybatis拦截器实现通用Mapper,注入Mybatis会报错,nullpointException(空指针)
大佬提出,辅助类:

一开始:BidderHelp.getBidderLog() 执行一次调用一次。

/**
 * 拦截器 - 辅助类
 *
 * @author Xin
 */
public class BidderHelp implements InitializingBean {

    private static BidderHelp instance = null;

    @Autowired
    private BidderLogMapper bidderLogDao;

    @Override
    public void afterPropertiesSet() throws Exception {
        BidderHelp.instance = this;
    }

    public static BidderLogMapper getBidderLog(){
        return  instance.bidderLogDao;
    }
}

优化:服务器启动加载一次,然后不在调用。

/**
 * 拦截器 - 辅助类
 */
@Component
public class BidderHelp implements InitializingBean {

    public static BidderHelp instance;

    @Autowired
    public BidderLogMapper bidderLogDao;

    @Override
    public void afterPropertiesSet() throws Exception {
        BidderHelp.instance = this;
    }

    /**
     * @PostConstruct 用于在完成依赖项注入以执行任何初始化之后需要执行的方法。必须在类投入使用 之前调用此方法。
     * 初始化bidderLogDao
     */
    @PostConstruct
    public void initialize() {
        instance = this;
        instance.bidderLogDao = this.bidderLogDao;
    }
}
2、@PostConstruct 注解

java 注解,不是Spring滴!!

Java中该注解的说明:@PostConstruct该注解被用来修饰一个 非静态的void()方法。被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。PostConstruct在构造函数之后执行,init()方法之前执行。

该注解的方法在整个Bean初始化中的执行顺序:
Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法)
MyBatis 拦截器 - 项目中使用

本文地址:https://blog.csdn.net/weixin_45395031/article/details/107464469