1.拦截器介绍

Mybatis允许在已映射语句执行过程中的某一阶段进行拦截调用。默认情况下,Mybatis允许使用插件来拦截的接口和方法包括以下几个:

  • Executor(update、query、flushStatements、commit、rollback、getTransaction、close、isClosed)
  • ParameterHandler(getParameterObject、setParameters)
  • ResultSetHandler(handleResultSets、handleCursorResultSets、handleOutputParameters)
  • StatementHandler(prepare、parameterize、batch、update、query、queryCursor、getParameterHandler、getBoundSql)

Mybatis插件可以实现Interceptor接口(org.apache.ibatis.plugin.Interceptor)从而对拦截对象和方法进行处理。Interceptor接口包含3个方法,源码如下:

  1. package org.apache.ibatis.plugin;
  2. import java.util.Properties;
  3. public interface Interceptor {
  4. Object intercept(Invocation var1) throws Throwable;
  5. default Object plugin(Object target) {return Plugin.wrap(target, this);}
  6. default void setProperties(Properties properties) {}
  7. }
  • setProperties(Properties properties):用于传递插件的参数,可以通过参数改变插件的行为。参数传递可以通过mybatis全局配置文件plugin元素中的property元素配置,配置参数后的参数在拦截器初始化时会通过serProperties方法传递给拦截器。

    1. <plugins>
    2. <!-- interceptor用于指定拦截器的全限定名 -->
    3. <plugin interceptor="com.fly.interceptor.MyInterceptor">
    4. <property name="prop1" value="value1"/>
    5. <property name="prop1" value="value1"/>
    6. </plugin>
    7. </plugins>
  • plugin(Object target):plugin方法的target参数就是拦截器要拦截的对象,该方法会在创建被拦截的接口实现类被调用。该方法实现很简单,只要调用Mybatis提供的Plugin类(org.apache.ibatis.plugin.Plugin)的wrap静态方法就可以通过Java的动态代理拦截目标对象。Plugin.wrap()会自动判断拦截器的签名和被拦截对象的接口是否匹配,只有匹配的情况下才会使用动态代理拦截目标对象,所以Plugin.wrap()的实现方法无需做额外的判断。

  • intercept(Invocation var1):intercept()是Mybatis运行时要执行的拦截方法。通过invocation可以得到很多有用的信息。例如下面: ```java package com.fly.interceptor;

import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Invocation;

import java.lang.reflect.Method;

public class MyInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { //获取当前被拦截的对象 Object target = invocation.getTarget(); //获取当前被拦截的方法 Method method = invocation.getMethod(); //获取被拦截方法中的参数 Object[] args = invocation.getArgs(); //调用proceed()可以执行被拦截对象真正的方法,proceed()实际上执行了method.invoke(target,args)方法 Object proceed = invocation.proceed(); return null; } }

当配置多个拦截器时,Mybatis会遍历所有拦截器,按顺序执行拦截器的plugin(),被拦截的对象就会被层层代理。在执行拦截对象的方法时,会一层层地调用拦截器,拦截器通过invocation.proceed()调用一下层的方法,知道真正的方法被执行。方法执行的结果会从最里面开始向外一层层返回。例如存在按顺序配置A、B、C3个签名相同的拦截器,Mybatis会按照C>B>A>target.processd()>A>B>C的顺序执行。如果A、B、C签名不同,就会按照Mybatis拦截对象的逻辑执行。

除了需要实现Interceptor接口外,还需要给实现Interceptor接口的实现类添加拦截器注解。**拦截器注解分为@Intercepts(org.apache.ibatis.plugin.Intercepts)和签名注解@Signature(org.apache.ibatis.plugin.Signature)**,这两个注解用来配置拦截器要拦截的接口的方法。

@Intercepts注解中的属性是一个@Signature数组,可以在同一个拦截器中同时拦截不同的接口和方法。@Signature注解包含3个属性:

- **type**:用于设置拦截的接口,可选值是**Executor、ParameterHandler、ResultSetHandler、StatementHandler接口。**
- **method**:设置拦截接口中的方法名,可选值是**Executor、ParameterHandler、ResultSetHandler、StatementHandler**4个接口中的方法名。
- **args**:设置拦截器方法的参数类型数组,通过方法名和参数类型可以确定唯一一个方法。

由于Mybatis代码实现原因,可以被拦截的4个接口中的方法并不是全都可以被拦截,下面会介绍可以被拦截的方法以及方法被调用的位置和对应的拦截器签名依次列举出来。

<a name="dSnTn"></a>
#### 1.1 Executor接口可拦截方法

- **int **update(MappedStatement var1, Object var2) **throws **SQLException:此方法会在所有INSERT、UPDATE、DELETE执行时调用,如果想拦截增、改、删这3类操作,接口方法签名如下:
```java
@Intercepts(
        @Signature(
                type = Executor.class,
                method = "update",
                args = {MappedStatement.class,Object.class}
        )
)
  • List query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws SQLException:该方法会在所有SELECT查询方法执行时被调用。通过这个接口参数可以获取很多有用的信息,因此该方法也是最常被拦截的一个方法。使用该方法需要注意的是,虽然Executor接口还有其他的参数不同但同名的query方法,由于Mybatis设计原因,其他同名的query不能被拦截。方法签名如下:

    @Intercepts(
          @Signature(
                  type = Executor.class,
                  method = "query",
                  args = {MappedStatement.class,Object.class, RowBounds.class, ResultHandler.class}
          )
    )
    
  • Cursor queryCursor(MappedStatement var1, Object var2, RowBounds var3) throws SQLException:该方法只有在查询返回值类型为Cursor(游标)时被调用。方法签名如下:

    @Intercepts(
          @Signature(
                  type = Executor.class,
                  method = "queryCursor",
                  args = {MappedStatement.class,Object.class, RowBounds.class}
          )
    )
    
  • List flushStatements() throws SQLException:该方法会在通过SqlSession方法调用flushStatements()或执行的接口方法中带有@Flush注解时才会被调用。方法签名如下:

    @Intercepts(
          @Signature(
                  type = Executor.class,
                  method = "flushStatements",
                  args = {}
          )
    )
    
  • void commit(boolean var1) throws SQLException:该方法只在通过Sqlsession调用commit方法时才会被调用。方法签名:

    @Intercepts(
          @Signature(
                  type = Executor.class,
                  method = "commit",
                  args = {boolean.class}
          )
    )
    
  • void rollback(boolean var1) throws SQLException:该方法只在通过Sqlsession调用rollback方法时才被调用。方法签名:

    @Intercepts(
          @Signature(
                  type = Executor.class,
                  method = "rollback",
                  args = {boolean.class}
          )
    )
    
  • Transaction getTransaction():该方法只通过Sqlsession方法获取数据库连接时才被调用,方法签名:

    @Intercepts(
          @Signature(
                  type = Executor.class,
                  method = "getTransaction",
                  args = {}
          )
    )
    
  • void close(boolean var1):该方法只在延迟加载获取新的Executor后才会被执行,方法签名:

    @Intercepts(
          @Signature(
                  type = Executor.class,
                  method = "close",
                  args = {boolean.class}
          )
    )
    
  • boolean isClosed():该方法只在延迟加载执行查询方法前被执行,签名如下:

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

    1.2 ParameterHandler接口可拦截方法

  • Object getParameterObject():该方法只在执行存储过程处理出参时被调用。方法签名:

    @Intercepts(
          @Signature(
                  type = ParameterHandler.class,
                  method = "getParameterObject",
                  args = {}
          )
    )
    
  • void setParameters(PreparedStatement var1) throws SQLException:该方法在所有数据库方法设置SQL参数时被调用。方法签名:

    @Intercepts(
          @Signature(
                  type = ParameterHandler.class,
                  method = "setParameters",
                  args = {PreparedStatement.class}
          )
    )
    

    1.3 ResultSetHandler接口可拦截方法

  • List handleResultSets(Statement var1) throws SQLException:该方法会在除存储过程及返回值类型为Cursor以外的查询方法中被调用。方法签名:

    @Intercepts(
          @Signature(
                  type = ResultSetHandler.class,
                  method = "handleResultSets",
                  args = {Statement.class}
          )
    )
    
  • Cursor handleCursorResultSets(Statement var1) throws SQLException:该方法只会在返回值为Cursor的查询方法中被调用。方法签名:

    @Intercepts(
          @Signature(
                  type = ResultSetHandler.class,
                  method = "handleCursorResultSets",
                  args = {Statement.class}
          )
    )
    
  • void handleOutputParameters(CallableStatement var1) throws SQLException:该方法只会在使用存储过程出参时被调用。方法签名:

    @Intercepts(
          @Signature(
                  type = ResultSetHandler.class,
                  method = "handleOutputParameters",
                  args = {CallableStatement.class}
          )
    )
    

    ResultSetHandler接口的handleResultSets方法对于拦截处理Mybatis的查询结果非常有用,并且由于这个接口被调用的位置在处理二级缓存之前,因此通过这种方式处理的结果可以执行二级缓存。

    1.4 StatementHandler接口可拦截方法

  • Statement prepare(Connection var1, Integer var2) throws SQLException:该方法在数据库执行前被调用,优于StatementHandler接口中的其他方法而被执行。方法签名:

    @Intercepts(
          @Signature(
                  type = StatementHandler.class,
                  method = "prepare",
                  args = {Connection.class,Integer.class}
          )
    )
    
  • void parameterize(Statement var1) throws SQLException:该方法在prepare方法执行执行,用于处理参数信息。方法签名:

    @Intercepts(
          @Signature(
                  type = StatementHandler.class,
                  method = "parameterize",
                  args = {Statement.class}
          )
    )
    
  • void batch(Statement var1) throws SQLException:在全局设置配置defaultExecutorType=”BATCH”时,执行数据操作才会被调用该方法。方法签名:

    @Intercepts(
          @Signature(
                  type = StatementHandler.class,
                  method = "batch",
                  args = {Statement.class}
          )
    )
    
  • List query(Statement var1, ResultHandler var2) throws SQLException:该方法在执行SELECT方法时调用。方法签名:

    @Intercepts(
          @Signature(
                  type = StatementHandler.class,
                  method = "query",
                  args = {Statement.class,ResultHandler.class}
          )
    )
    
  • Cursor queryCursor(Statement var1) throws SQLException:该放啊是3.4.0版本新增的,只会在返回值为Cursor查询中被调用。方法签名:

    @Intercepts(
          @Signature(
                  type = StatementHandler.class,
                  method = "queryCursor",
                  args = {Statement.class}
          )
    )
    

    2.下划线键值转小写驼峰形式插件

    上面看了那么多API是时候开始实战了,现在有这么一个场景,有些人为了方法扩展使用Map类型作为返回值,在使用Map类型作为返回值时,Map中的键值就是查询结果中的列名,而列名一般都是大写或下划线形式,例如USERNAME或user_name,但是Java实体类的属性是以驼峰形式命名(例如userName),而且由于不同数据库查询结果列的大小写也不一致,因此为了保证在使用Map时Map的键能与实体类属性一致,即将不同格式的列名转换为Java中驼峰形式。我们可以通过拦截ResultSetHandler接口中handleResultSets方法去处理返回值为Map类型的结果。

sql语句

create table goods(
id int not null primary key auto_increment,
goods_name varchar(30) not null comment '商品名',
goods_num int not null comment '商品库存数量',
price DECIMAL(18,2) not null comment '价格',
create_time TIMESTAMP not null comment '上架时间'  
)engine=innodb CHARACTER set=utf8mb4 collate=utf8mb4_general_ci row_format=dynamic comment '商品表';

insert into goods(goods_name,goods_num,price,create_time) 
values('三上悠亚逼真版充气娃娃',1000,29999.00,now()),('波多野结衣逼真版充气娃娃',100,19999.00,now())
,('特斯拉Model16未来战士款',10000,1999999.00,now()),('奔驰机甲战士202020款运动版',20000,2999999999.00,now());

SELECT id,goods_name,goods_num,price,create_time from goods;

Goods.class

package com.fly.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true) //开启链式调用
public class Goods {
    private Integer id;
    private String goodsName;
    private Integer goodsNum;
    private Double price;
    private String createTime;
}

GoodsMapper.class

package com.fly.mapper;
import com.fly.entity.Goods;
import java.util.Map;
public interface GoodsMapper {

    Map<String, Object> findGoodsByMap();
}

goodsMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.fly.mapper.GoodsMapper">
    <select id="findGoodsByMap" resultType="java.util.Map">
        SELECT id,goods_name,goods_num,price,create_time from goods where id=1;
    </select>
</mapper>

CameHumpInterceptor.class(下划线、大写格式转驼峰格式插件)

package com.fly.plugin;

import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;

import java.sql.Statement;
import java.util.*;

@Intercepts(
        @Signature(
                type= ResultSetHandler.class,
                method="handleResultSets",
                args ={Statement.class})
)
public class CameHumpInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("invocation():运行时进行拦截的方法");
        //先执行查询sql语句得到结果,因为ResultSetHandler接口handleResultSets会拦截所有查询
        List<Object> list =(List<Object>)invocation.proceed();
        for (Object object : list){
            //如果结果是Map类型
            if(object instanceof Map){
                processMap((Map<String, Object>) object);
            }else {
                break;
            }
        }
        return list;
    }
    //处理map
    private void processMap(Map<String,Object> map){
        //防止列名重复,所以转set
        Set<String> keySet=new HashSet<>(map.keySet());
        //挨个遍历列名
        for(String key:keySet){
            if((key.charAt(0)>='A' && key.charAt(0)<='Z') || key.indexOf("_") >=0){
                Object value=map.get(key);
                map.remove(key);
                map.put(underlineToCameHump(key),value);
            }
        }
    }

    //处理下划线风格为驼峰风格
    public static String underlineToCameHump(String inputString){
        StringBuilder sb=new StringBuilder();
        boolean nextUpperCase=false;
        for(int i=0;i<inputString.length();i++){
            char c=inputString.charAt(i);
            if(c=='_'){
                if(sb.length()>0){
                    nextUpperCase=true;
                }
            }else{
                if(nextUpperCase){
                    sb.append(Character.toUpperCase(c));
                    nextUpperCase=false;
                }else{
                    sb.append(Character.toLowerCase(c));
                }
            }
        }
        return sb.toString();
    }

    @Override
    public Object plugin(Object target) {
        System.out.println("plugin():此方法在创建interceptor接口实现类前执行");
        return Plugin.wrap(target,this);
    }

    @Override
    public void setProperties(Properties properties) {
        System.out.println("setProperties():此方法用于设置plugin的参数,在plugin()之前执行");
    }
}

mybatis.xml添加plugins配置。

<?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>

    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>

    <!-- 类型别名配置 -->
    <typeAliases>
        <!-- 指定一个包名,MyBatis会在包名下面搜索需要的 Java Bean -->
        <package name="com.fly.entity"/>
    </typeAliases>



    <!-- 类型处理器配置 -->
    <typeHandlers>
        <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"
                     javaType="com.fly.entityEnum.SexEnum" />
    </typeHandlers>
        <!-- plugins配置 -->
    <plugins>
        <plugin interceptor="com.fly.plugin.CameHumpInterceptor"/>
    </plugins>


    <!-- environments用于指定运行环境配置,default="development"指定运行环境为开发环境 -->
    <environments default="development">
        <environment id="development">
            <!-- 配置事务管理器 类型为JDBC -->
            <transactionManager type="JDBC"/>
            <!-- 配置数据源 数据源类型为POOLED  -->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/test?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=UTC&amp;allowMultiQueries=true"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <!-- 多数据库支持 -->
    <databaseIdProvider type="DB_VENDOR">
        <property name="mysql" value="mysql"/>
    </databaseIdProvider>
    <!-- mappers指定一组mapper(映射器) -->
    <mappers>
        <!-- mapper的XML映射文件包含了SQL代码和映射定义信息,resource指定mapper文件的地址,
        待会要在src/main/resources目录下创建一个mapper文件夹,
        然后在mapper文件夹创建一个名为studentMapper.xml的mapper映射文件
         -->
        <mapper resource="mapper/studentMapper.xml"/>
        <mapper resource="mapper/blogMapper.xml"/>
        <mapper resource="mapper/userMapper.xml"/>
        <mapper resource="mapper/userRoleMapper.xml"/>
        <mapper resource="mapper/goodsMapper.xml"/>
    </mappers>
</configuration>

测试类:

package com.fly.test;

import com.fly.entity.Goods;
import com.fly.mapper.GoodsMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.BeforeClass;
import org.junit.Test;

import java.io.InputStream;
import java.util.Map;
import java.util.Set;

/**
 * 测试插件
 */
public class TestPlugin {
    private static SqlSessionFactory sqlSessionFactory;
    private static SqlSession sqlSession;
    //@BeforeClass表示针对所有测试,只执行一次,且必须为static void
    @BeforeClass
    public static void init() {
        try {
            InputStream stream = Resources.getResourceAsStream("mybatis.xml");
            //SqlSessionFactoryBuilder通过获取配置文件信息得到SqlSessionFactory,看到build()就想到了建造者模式
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(stream);
            sqlSession = sqlSessionFactory.openSession();
            stream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 测试下划线、大写格式转驼峰格式插件
     */
    @Test
    public void findGoodsByMap(){
        GoodsMapper mapper = sqlSession.getMapper(GoodsMapper.class);
        Map<String, Object> goodsByMap = mapper.findGoodsByMap();
        Set<String> strings = goodsByMap.keySet();
        for (String string:strings){
            System.out.println("key:"+string);
        }
    }
}

效果图:555.jpg