MyBatis使用TypeHandler(枚举转换、数据加密模糊查询)、Cache(分布式系统二级缓存)、Interceptor(插件开发)

发布于:2022-11-29 ⋅ 阅读:(628) ⋅ 点赞:(0)

TypeHandler 

MyBatis已经实现的TypeHandler 

注册

        mybatis默认定义了一批TypeHandler,正常情况下这些TypeHandler就可以满足我们的使用了.mybatis通过TypeHandlerRegister来管理TypeHandler 

配置

        一种是通过typeHandler 标签(先),

        一种通过package标签(后)。package标签没有了JavaType与jdbcType属性。但1.可以在TypeHandler的实现类上标注注释即可确定@MappedJdbcTypes(JdbcType.VARCHAR):要处理的jdbc类型、 @MappedTypes(Encrypt.class) :要处理的Java类型。2.在Mapper.xml 的 resultMap标签下 result 标签内配置typehander属性

<typeHandlers>

        <!-- <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="tk.mybatis.simple.type.Enable"/> -->
        <typeHandler handler="tk.mybatis.simple.type.EnableTypeHandler" javaType="tk.mybatis.simple.type.Enable"/>
        <!-- <typeHandler handler="tk.mybatis.simple.type.EncryptStringTypeHandler"/>-->
        <!-- <typeHandler handler="tk.mybatis.simple.type.EncryptTypeHandler"/>-->
        <!--使用该注解可以指定(package 应放在 typeHandler的后面)-->
        <package name="tk.mybatis.simple.type"/>
</typeHandlers>

使用案例 EnumOrdinalTypeHandler

SQL

create table sys_role
(
    id          bigint auto_increment comment '角色ID'
        primary key,
    role_name   varchar(50) null comment '角色名',
    enabled     int         null comment '有效标志',
    create_by   bigint      null comment '创建人',
    create_time datetime    null comment '创建时间'
)
    comment '角色表' charset = utf8;

枚举类

public enum EnumOrdinal {
    disable,    // 0
    enable;     // 1
}

实体类

    /**
     * 有效标志
     */
    private EnumOrdinal enabled;

    public EnumOrdinal getEnabled() {
        return enabled;
    }

    public void setEnabled(EnumOrdinal enabled) {
        this.enabled = enabled;
    }

mybatis-config.xml

<typeHandlers>
            <!-- 因为EnumOrdinalTypeHandler类未标明注解@MappedTypes
                所以这里一定要配置javaType ,否则TypeHandlerRegistry.register()会提示 Unable to find a usable constructor for ***-->
          <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="tk.mybatis.simple.type.EnumOrdinal"/>

</typeHandlers>

 执行效果

内置枚举 TypeHandler 的不足与补充

        mybatis 提供的Enum有许多局限性 Enum(String name, int ordinal) 这是因为 EnumOrdinal-TypeHandler 源码三个地方限制了TypeHandler 的类型。我们可以通过改造 构造方法、设置值方法(setNonNullParameter)、获取值方法(getNullableResult的三个重载方法)

通用枚举转换的实现 BaseTypeHandler、@MappedTypes背后的执行逻辑-ibatishttp://t.csdn.cn/kXXeI

TypeHandler的拓展

        实现数据加密解密

import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;

import java.nio.charset.StandardCharsets;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

// 识别不了嵌套的association; BaseTypeHandler 与 TypeHandler 的区别 :BaseTypeHandler配置的时候不需要指定 javaType
// 加密思路就是 字符串(包括中文)不能直接使用AES算法解密,因为可能出现中文乱码的情况实现先转为 HEX 再进行AES_ENCRYPT
// 解密则是将顺序倒置
// 此方法不能进行模糊查询
public class EncryptStringTypeHandler extends BaseTypeHandler<String> {

    private static final byte[] KEYS = "12345678abcdefgh".getBytes(StandardCharsets.UTF_8);

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        if (parameter == null){
            ps.setString(i,null);
        }
        AES aes = SecureUtil.aes(KEYS);
        String s = aes.encryptHex(parameter);
        ps.setString(i,s);
    }

    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String string = rs.getString(columnName);
        return SecureUtil.aes(KEYS).decryptStr(string);
    }

    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String string = rs.getString(columnIndex);
        return SecureUtil.aes(KEYS).decryptStr(string);
    }

    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String string = cs.getString(columnIndex);
        return SecureUtil.aes(KEYS).decryptStr(string);
    }

}

       简单实现MySQL加密数据模糊查询

create table user_info
(
    id           varchar(64) not null
        primary key,
    name_decrypt varchar(64) null comment '加密后的名字',
    user_name    varchar(64) null comment '加密前的名字'
)
    charset = utf8;
 -- 添加数据
INTO mybatis.user_info (id, name_decrypt, user_name) VALUES ('2', 'AEF5650465C8A91F11086D72A6C54039', 'Jay');

-- 模糊查询
 SELECT * FROM user_info WHERE AES_DECRYPT(UNHEX(name_decrypt),'1024') LIKE CONCAT('%','a','%') LIMIT 1;

Java工具类


import org.apache.commons.codec.binary.Hex;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.util.Locale;
/*<dependency>
<groupId>org.apache.directory.studio</groupId>
<artifactId>org.apache.commons.codec</artifactId>
<version>1.8</version>
</dependency>*/

/**
 * @author Jay
 */
public class AESUtil {

    /**
     * secretKey 的最长长度
     */
    public static final int LENGTH = 16;
    /**
     * 编码格式
     */
    private static final String CHARSET_NAME = "UTF-8";
    /**
     * 算法/模式/填充
     */
    public static final String TRANSFORMATION = "AES/ECB/PKCS5Padding";
    public static final String ALGORITHM = TRANSFORMATION.split("/")[0];

    /**
     * 加密的Key-可自行设置 长度应小于16
     */
    private static final String AES_KEY = "1024";

    public static void main(String[] args) throws Exception {
        String str = "Jay";
        String byteStr = "AEF5650465C8A91F11086D72A6C54039";

        System.out.println("要加密的数据:" + str + ",加密后:" + encryptThenToHex(str));
        System.out.println("要解密的数据:" + byteStr + ",解密后:" + hexThenDecrypt(byteStr));

//        SELECT HEX(AES_ENCRYPT('Jay','1024')) FROM DUAL;
//        SELECT AES_DECRYPT(UNHEX('AEF5650465C8A91F11086D72A6C54039'),'1024') FROM DUAL;
    }

    /**
     * AES 加密
     */
    public static String encrypt(String parameter) {
        try {
            return hexThenDecrypt(parameter);
        } catch (Exception e) {
            System.out.println("AES加密失败");
            return parameter;
        }
    }

    /**
     * AES 解密
     */
    public static String decrypt(String parameter) {
        try {
            return encryptThenToHex(parameter);
        } catch (Exception e) {
            System.out.println("AES解密失败");
            return parameter;
        }
    }

    /**
     * 字符串经 AES 加密后用 Hex encodeHex
     *
     * @param str 要加密的字符串
     * @return char[]转 String后的数据
     */
    private static String encryptThenToHex(String str) throws Exception {
        final Cipher encryptCipher = Cipher.getInstance(TRANSFORMATION);
        encryptCipher.init(Cipher.ENCRYPT_MODE, generateKeyStr());

        /* SELECT HEX(AES_ENCRYPT( str , key_str ))  FROM DUAL; -- 加密
                    AES_ENCRYPT(str,key_str[,init_vector][,kdf_name][,salt][,info | iterations])
            HEX( str )
         */
        char[] bytes = Hex.encodeHex(encryptCipher.doFinal(str.getBytes(CHARSET_NAME)));

        return new String(bytes).toUpperCase(Locale.ROOT);
    }

    /**
     * 字符串经Hex decode 再 AES解密
     *
     * @param byteStr 待解密的数据
     * @return 解密字符串-result
     * @throws Exception 传递篡改后的字符串可能会解析失败,自行捕获异常
     */
    private static String hexThenDecrypt(String byteStr) throws Exception {
        final Cipher decryptCipher = Cipher.getInstance(TRANSFORMATION);
        decryptCipher.init(Cipher.DECRYPT_MODE, generateKeyStr());

        /* SELECT AES_DECRYPT(UNHEX( str ), key_str ) FROM DUAL;
                      UNHEX(str)
               AES_ENCRYPT(str,key_str[,init_vector][,kdf_name][,salt][,info | iterations]) */
        byte[] bytes;
        try {
            bytes = decryptCipher.doFinal(Hex.decodeHex(byteStr.toCharArray()));
        } catch (Exception e) {
            System.out.println("    - Error decrypting: " + e.getMessage());
            return "";
        }

        return new String(bytes);
    }

    /**
     * 模拟生成 MySQL 密钥字符串key_str,并生成密钥 <br/>
     *
     * @return SecretKeySpec 对称密钥(SecretKey)
     */
    private static SecretKeySpec generateKeyStr() {
        if (AESUtil.AES_KEY.length() > LENGTH) {
            throw new RuntimeException("Specify a key of length less than 16.");
        }
        try {
            final byte[] secretKey = new byte[LENGTH];
            int i = 0;
            for (byte b : AESUtil.AES_KEY.getBytes(CHARSET_NAME)) {
                secretKey[i % LENGTH] = b;
                i++;
            }
            // 通过 key 生成的 secretKey 如果 key 长度小于16 则会使用 0 填充, 如果 key 长度大于16 则会再从前往后覆盖设置
            return new SecretKeySpec(secretKey, ALGORITHM);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Failed to generate a key.", e);
        }
    }

}

 TypeHandler

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import tk.mybatis.simple.util.AESUtil;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * 泛型 String 可以配置成 其他的自定义类型
 * @author Jay
 */
public class AESTypeHandler extends BaseTypeHandler<String> {

    public AESTypeHandler() {
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        try {
            ps.setString(i, AESUtil.encrypt(parameter));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String value = rs.getString(columnName);
        return value == null ? null : AESUtil.decrypt(value);

    }

    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String value = rs.getString(columnIndex);
        return value == null ? null : AESUtil.decrypt(value);
    }

    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String value = cs.getString(columnIndex);
        return value == null ? null : AESUtil.decrypt(value);
    }
}

使用方法

@MapKey("id")
    List<Map> getUserNameByDecrypt(@Param("decrypt") String decrypt);


<select id="getUserNameByDecrypt" resultType="map">
		SELECT * FROM user_info WHERE user_name = #{decrypt,typeHandler=tk.mybatis.simple.plugin.typehandler.AESTypeHandler}
</select>



UserInfoMapper userInfoMapper = sqlSession.getMapper(UserInfoMapper.class);
            List<Map> j = userInfoMapper.getUserNameByDecrypt("AEF5650465C8A91F11086D72A6C54039");
            System.out.println(j);

MyBatis缓存配置

        一般提到 My atis 缓存 的时候,都 是指二级缓存。一级 缓存( 也叫本地 缓存〉默认会启用,
并且不能控 制。

一级缓存

        My Batis 一 级缓存存在于 SqlSession 的生命周期中,在同 SqlSession 中查询 时, MyBatis 会把执行的方法和参数通过算法生成缓存的键值,将键值和查询 结果存入一个 Map 对象中。如果同一个 SqlSession 中执行的方法和参数完全一致,那么通过算法会生成相同的键值,当 Map 缓存对象中己经存在该键值时,则会返回缓存中的对象。
@Test
    public void testL1Cache() {
        //获取 sqlSession
        SqlSession sqlSession = getSqlSession();
        SysUser user1;
        try {
            //获取 SysUserMapper 接口
            SysUserMapper SysUserMapper = sqlSession.getMapper(SysUserMapper.class);
            //调用 selectByPrimaryKey 方法,查询 id = 1 的用户
            user1 = SysUserMapper.selectByPrimaryKey(1L);
            //对当前获取的对象重新赋值
            user1.setUserName("New Name");
            //再次查询获取 id 相同的用户
            SysUser user2 = SysUserMapper.selectByPrimaryKey(1L);
            //虽然我们没有更新数据库,但是这个用户名和我们 user1 重新赋值的名字相同了
            Assert.assertEquals("New Name", user2.getUserName());
            //不仅如何,user2 和 user1 完全就是同一个实例
            Assert.assertEquals(user1, user2);
        } finally {
            //关闭当前的 sqlSession
            sqlSession.close();
        }
        System.out.println("开启新的 sqlSession");
        //开始另一个新的 session
        sqlSession = getSqlSession();
        try {
            //获取 SysUserMapper 接口
            SysUserMapper SysUserMapper = sqlSession.getMapper(SysUserMapper.class);
            //调用 selectByPrimaryKey 方法,查询 id = 1 的用户
            SysUser user2 = SysUserMapper.selectByPrimaryKey(1L);
            //第二个 session 获取的用户名仍然是 admin
            Assert.assertNotEquals("New Name", user2.getUserName());
            //这里的 user2 和 前一个 session 查询的结果是两个不同的实例
            Assert.assertNotEquals(user1, user2);
            //执行删除操作 (执行了任何的“增删改”操作,无论这些“增删改”操作是否影响到了缓存的数据)
            SysUserMapper.deleteByPrimaryKey(2L);
            //获取 user3
            SysUser user3 = SysUserMapper.selectByPrimaryKey(1L);
            //这里的 user2 和 user3 是两个不同的实例
            Assert.assertNotEquals(user2, user3);
        } finally {
            //关闭 sqlSession
            sqlSession.close();
        }
    }

MyBatis一级缓存失效的几种情况http://t.csdn.cn/k41od

解决思路

        增加 flushCache= ' true'   ,这个属性配置为 true 后, 再查询数据前清空当前的一 级缓存。
<select id="selectRolesByUserId" flushCache="true" resultType="tk.mybatis.simple.model.SysRole">

二级缓存

        MyBatis 全局配置 settings 中有一 个参数 cac heEnabl ed ,这个参数是 级缓存的全局开关,默认值是 true。
<!--org.apache.ibatis.session.Configuration-->
    <settings>
        <!--二级缓存在SqlSessionFactory 默认是打开的-->
        <setting name="cacheEnabled" value="true"/>
    </settings>

 读取命名空间的配置

<cache type="org.mybatis.caches.redis.RedisCache" 
           eviction="LRU" flushInterval="0" size="0" readOnly="false" blocking="false" />

 org.apache.ibatis.builder.xml.XMLMapperBuilder#cacheElement

private void cacheElement(XNode context) throws Exception {
    if (context != null) {
      String type = context.getStringAttribute("type", "PERPETUAL");
      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
      String eviction = context.getStringAttribute("eviction", "LRU");
      Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
      Long flushInterval = context.getLongAttribute("flushInterval");
      Integer size = context.getIntAttribute("size");
      boolean readWrite = !context.getBooleanAttribute("readOnly", false);
      boolean blocking = context.getBooleanAttribute("blocking", false);
      Properties props = context.getChildrenAsProperties();
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
  }

分布式系统缓存&自定义缓存

自定义缓存会使得部分参数不可用

cache type="cache.RedisCache"  size="不可用" flushInterval="不可用" eviction="不可以" readOnly="不可用" blocking="不可用"/>

 查询语句标注了 useCache="false" flushCache="true" 使用了 ResultHandler 的情况需要注意

 

 

MyBatis · GitHub

/**
 * 参照 org.apache.ibatis.cache.impl.ScheduledCache、 PerpetualCache<br/>
 * 间隔 60s 刷新一次,使用自定义的 Cache 不支持解析参数
 * <cache type="cache.RedisCache"  size="" flushInterval="" eviction="" readOnly="" blocking=""/><br/>
 * org.apache.ibatis.mapping.CacheBuilder#build() L92
 *
 * @author Jay
 */
public class RedisCache implements Cache {

    /**
     * 当前 Mapper的命名空间
     */
    private final String id;
    /**
     *  清除 field 的时间间隔
     */
    private long clearInterval = 60 * 1000;
    /**
     *  最后一次清除的时间
     */
    private long lastClear;

    private HadesClient hadesClient;

    public RedisCache(String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        }
        this.id = id;
        this.lastClear = System.currentTimeMillis();
    }

    @Override
    public String getId() {
        return this.id;
    }

    @Override
    public synchronized void putObject(Object key, Object value) {
        clearWhenStale();
        if (ObjectUtils.isEmpty(key) || ObjectUtils.isEmpty(value)) {
            return;
        }
        hadesClient.hset(id.getBytes(StandardCharsets.UTF_8),
                key.toString().getBytes(StandardCharsets.UTF_8),
                SerializeUtil.serialize(value));
    }

    @Override
    public synchronized Object getObject(Object key) {
        if (clearWhenStale()) {
            return null;
        }
        byte[] bytes = hadesClient.hget(id.getBytes(StandardCharsets.UTF_8), key.toString().getBytes(StandardCharsets.UTF_8));
        return SerializeUtil.unSerialize(bytes);

    }

    @Override
    public synchronized Object removeObject(Object key) {
        clearWhenStale();
        return hadesClient.hdel(id.getBytes(StandardCharsets.UTF_8), key.toString().getBytes(StandardCharsets.UTF_8));
    }

    @Override
    public synchronized void clear() {
        getIfNecessary();
        lastClear = System.currentTimeMillis();
        // 删除过期的缓存
        hadesClient.del(id.getBytes(StandardCharsets.UTF_8));
    }

    @Override
    public int getSize() {
        clearWhenStale();
        return hadesClient.hkeys(id.getBytes(StandardCharsets.UTF_8)).size();
    }

    @Override
    public ReadWriteLock getReadWriteLock() {
        return null;
    }

    @Override
    public boolean equals(Object o) {
        if (getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        }
        if (this == o) {
            return true;
        }
        if (!(o instanceof Cache)) {
            return false;
        }

        Cache otherCache = (Cache) o;
        return getId().equals(otherCache.getId());
    }

    @Override
    public int hashCode() {
        if (getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        }
        return getId().hashCode();
    }

    /**
     * 检查 hadesClient
     */
    private void getIfNecessary() {
        if (hadesClient == null) {
            this.hadesClient = SpringContextUtil.getBean(HadesClient.class);
        }
    }

    /**
     *  过期时清除
     * @return
     */
    private boolean clearWhenStale() {
        if (System.currentTimeMillis() - lastClear > clearInterval) {
            clear();
            return true;
        }
        return false;
    }
}

二级缓存适用场景

        二级缓存虽然好处很多,但并不是什么时候都可以使用,在以下场景中,推荐使用二 级缓存
以查询为主的应用中,只有尽可能少的增、删、改操作和 绝大多数以单表操作存在时,由于很少存在互相关联的情况,因此不会出现脏数据(概率就会减小)。可以按业务划分对表进行分组时 如关联的表比较少,可以通过 参照缓存进行配置。 除了推荐使用的情况,如果脏读对系统没有影响,也可以考虑使用 在无法保证数据不 现脏读的情况下, 建议在业务层使用可控制的缓存代替二 级缓存

Mybatis 默认实现了哪些缓存,都可以设置什么参数

        以MyBatis-3.4.1为例 实现了 PERPETUAL、FIFO、LRU、SOFT、WEAK    二 级缓存需要配置在 Mapper xml 映射 文件中 或者配置在 Mapper.ja va 接口中;
        

MyBatis配置文件哪些属性由谁加载

org.apache.ibatis.session.Configuration

MyBatis 插件开发

MyBatis 的执行流程

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

        Executor

        拦截执行器的方法


import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.executor.BatchResult;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.transaction.Transaction;

public abstract class MyExecutor implements Executor {
    /**
     *  该方法会在所有的 INSERT UPDATE DELET 执行时被调用,
     */
    @Override
    public int update(MappedStatement ms, Object parameter) throws SQLException {
        return 0;
    }

    /**
     * 该方法不可被拦截
     */
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException {
        return null;
    }

    /**
     * 该方法会在所有 SELECT 查询方法执行时被调用
     */
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        return null;
    }

    /**
     * 该方法只有在查询 的返回值类型为 Cursor 时被调用
     */
    @Override
    public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
        return null;
    }

    /**
     * 该方法只在通过 SqlSession 方法调用 flushStatements 方法或执行的接口方法中带有@Flush 注解时才被调用
     */
    @Override
    public List<BatchResult> flushStatements() throws SQLException {
        return null;
    }

    /**
     * 只在通过 SqlSession 方法调用 commit 方法时才被调用
     */
    @Override
    public void commit(boolean required) throws SQLException {

    }

    /**
     * 过 SqlSession口方法调用 rollback 方法时才被调用
     */
    @Override
    public void rollback(boolean required) throws SQLException {

    }

    @Override
    public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        return null;
    }

    @Override
    public boolean isCached(MappedStatement ms, CacheKey key) {
        return false;
    }

    @Override
    public void clearLocalCache() {

    }

    @Override
    public void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType) {

    }

    /**
     * 在通过 SqlSession 方法获取数据库连接时才被调用,
     */
    @Override
    public Transaction getTransaction() {
        return null;
    }

    /**
     * 只在延迟加载获取新的 Executor 后才会被执行
     */
    @Override
    public void close(boolean forceRollback) {

    }

    /**
     * 该方法只在延迟加载执行查询方法前被执行
     */
    @Override
    public boolean isClosed() {
        return false;
    }

    @Override
    public void setExecutorWrapper(Executor executor) {

    }
}

        ParameterHandler

        拦截参数的处理

import org.apache.ibatis.executor.parameter.ParameterHandler;

import java.sql.PreparedStatement;
import java.sql.SQLException;

public abstract class MyParameterHandler implements ParameterHandler {
    /**
     * 法只在执行存储过程处理出参的时候被调用。
     */
    @Override
    public Object getParameterObject() {
        return null;
    }

    /**
     * 所有数据库方法设置 SQL 参数时被调用。
     */
    @Override
    public void setParameters(PreparedStatement ps) throws SQLException {

    }
}

        ResultSetHandler

        拦截结果集的处理

import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.executor.resultset.ResultSetHandler;

import java.sql.CallableStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;

public abstract class MyResultSetHandler implements ResultSetHandler {

    /**
     * 在 除 存储过程及返回值类型为 Cursor<T> 的查询方法中被调用(3.4.0 版本中新增)
     */
    @Override
    public <E> List<E> handleResultSets(Statement stmt) throws SQLException {
        return null;
    }


    @Override
    public <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException {
        return null;
    }
    /**
     * 只在使用存储过程处理出参时被调用
     */
    @Override
    public void handleOutputParameters(CallableStatement cs) throws SQLException {

    }
}

        StatementHandler

        拦截Sql语法构建的处理

import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.session.ResultHandler;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;

public abstract class MyStatementHandler implements StatementHandler {
    /**
     * 该方法会在数据库执行前被调用 优先于当前接口中的其他方法而被执行
     */
    @Override
    public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
        return null;
    }

    /**
     * 该方法在 prepare 方法之后执行,用于处理参数信息
     */
    @Override
    public void parameterize(Statement statement) throws SQLException {

    }

    /**
     * 在全局设置配置 defaultExecutorType BATCH 时,执行数据操作才会调用该方
     */
    @Override
    public void batch(Statement statement) throws SQLException {

    }

    @Override
    public int update(Statement statement) throws SQLException {
        return 0;
    }

    /**
     * 执行 SELECT 方法时调用
     */
    @Override
    public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
        return null;
    }

    /**
     * 只会在返回值类型为 Cursor<T >的查询中被调用
     */
    @Override
    public <E> Cursor<E> queryCursor(Statement statement) throws SQLException {
        return null;
    }

    @Override
    public BoundSql getBoundSql() {
        return null;
    }

    @Override
    public ParameterHandler getParameterHandler() {
        return null;
    }
}

拦截器接口

package org.apache.ibatis.plugin;

import java.util.Properties;


public interface Interceptor {
    // 要执行的拦截方法
  Object intercept(Invocation invocation) throws Throwable;
    // 在创建被拦截的接口实现类时被调用 主要的方法实现是 return Plugin.wrap(target, this) ;
  Object plugin(Object target);
   
  // 传递插件的参数,可以通过参数来改变插件的行为。
  void setProperties(Properties properties);

}

拦截顺序

        如果存在按顺序配置的 三个签名 相同的拦截器, MyBaits 会按照 C>B>A>target.proceed()>A>B>C 的顺序执行。

拦截器签名

 可以仅配置一个 @Signature;或者是一个 Signature集合 (@Intercepts({@Signature(***), @Signature(***), ........}))

@Signature 注解包含以下三个属性。
        type:设置拦截 接口,可选值是前面提到的 4  个接口
        method:设置拦截接口中的方法名 可选值是前面 4  个接口对应的方法,需要和接口匹配
        args: 设置拦截方法的参数类型数组通过接口、方法名和参数类型可以确定唯 一 个方法

实现案例

对返回结果为Map类型的进行小驼峰转换

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

import java.sql.Statement;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;

/**
 * 就是循环判断结果。如果是 Map 类型的结果,就对 Map
 * 进行处理 处理时为了避免把己经是驼峰的值转换为纯小写,因此通过首字母是否为大写或是
 * 否包含下画线来判断(实际应用中要根据实际情况修改)。如果符合其中 个条件就转换为驼峰
 * 形式,删除对应的 key ,使用新的 key 来代替 。
 */
@SuppressWarnings({"unchecked","rawtypes"})
@Intercepts(@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}))
public class CameHumpInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 先执行结果,再对结果进行处理
        List<Object> result = (List<Object>) invocation.proceed();
        for (Object object : result) {
            // 如果结果是 Map 类型的,就对 Map 的 key 进行转换
            if (object instanceof Map)
                processMap((Map) object);
            else
                break;
        }
        return result;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }

    /**
     * 处理 Map 类型
     *
     * @param map
     */
    private void processMap(Map<String, Object> map) {
        HashSet<String> keySet = new HashSet<>(map.keySet());
        for (String key : keySet) {
            // 将以大写开头的字符串转换为小写,如果包含下划线也会处理为驼峰
            // 此处只通过这两个简单的标识来判断是否进行转换
            if (key.charAt(0) >= 'A' && key.charAt(0) <= 'Z' && key.contains("_")) {
                Object value = map.get(key);
                map.remove(key);
                map.put(underlineToCamelHump(key), value);
            }
        }
    }

    /**
     * 将下划线风格替换为驼峰风格
     */
    private String underlineToCamelHump(String key) {
        StringBuilder stringBuilder = new StringBuilder();
        boolean nextUpperCase = false;
        for (int i = 0; i < key.length(); i++) {
            char c = key.charAt(i);

            if (c == '_') {
                if (stringBuilder.length() > 0)
                    nextUpperCase = true;
            } else {
                if (nextUpperCase) {
                    stringBuilder.append(Character.toUpperCase(c));
                    nextUpperCase = false;
                } else {
                    stringBuilder.append(Character.toLowerCase(c));
                }
            }

        }
        return stringBuilder.toString();
    }
}

Mybatis_PageHelper: Mybatis分页插件 (gitee.com)

文章参考: MyBatis从入门到精通

本文含有隐藏内容,请 开通VIP 后查看