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

MyBatis架构图

        程序启动时 org.mybatis.spring.SqlSessionFactoryBean 会通过 org.apache.ibatis.builder.xml.XMLMapperBuilder 解析Mapper XML 文件,生成 MappedStatement 对象 并存放在 org.apache.ibatis.session.Configuration#mappedStatements (类型为 Map<String, MappedStatement>)

MappedStatement

        MappedStatement 属性中包含了 ParameterMap 与  List<ResultMap>, 前者是处理预编译SQL,即查询参数的数据类型。后者是为查询结果转换数据类型使用的。且两者都会结合TypeHandler 对数据进行再加工。

ParameterHandler & BaseTypeHandler#setParameter & JdbcType

        Mybatis的执行流程 如上调用栈,其通过 MapperProxy 代理 获取到 SqlSession 经 Executor 创建 StatementHandler ,StatementHandler 完善预编译SQL  (PreparedStatement),ParameterHandler(调用栈的第三行) 在此期间完成对参数的设置,如果没 ParameterMap 没有指定 TypeHandler 则会匹配 UnknownTypeHandler 通过  JdbcType 尝试查找出一个  TypeHandler 为其设置值.

JdbcType 官方解释

在这个表格之后的所支持的 JDBC 类型列表中的类型。JDBC 类型是仅 仅需要对插入,更新和删除操作可能为空的列进行处理。这是 JDBC jdbcType 的需要,而不是 MyBatis 的。如果你直接使用 JDBC 编程,你需要指定 这个类型-但仅仅对可能为空的值。

ResultSetHandler & JavaType

转换结果的调用栈,经过StatementHandler, 进入 ResultHandler, 由 ResultMap 的 propertyMapping 确定 TypeHandler, 最后进行结果转换

        JavaType 会在解析 MapperStatement 的时候根据编写的 JavaType 去设置合适的 TypeHandler

JavaType 官方解释

        一个 Java 类的完全限定名,或一个类型别名(参考上面内建类型别名 的列表) 。如果你映射到一个 JavaBean,MyBatis 通常可以断定类型。 然而,如果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证所需的行为。

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背后的执行逻辑-ibatisicon-default.png?t=N7T8http://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);

 CBC模式加密

public static void main(String[] args) throws Exception {
        // 加密密钥 以及 Iv 算法参数 的 16进制表示方式
        String keyStr = "1234567890123456";
        String inputStr = "PassWord";

        // CBC 需要结合 Iv 算法
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKeySpec key = new SecretKeySpec(keyStr.getBytes(StandardCharsets.UTF_8), "AES");
        IvParameterSpec ivParameterSpec = new IvParameterSpec(keyStr.getBytes());

        cipher.init(Cipher.ENCRYPT_MODE, key, ivParameterSpec);
        byte[] bytes = cipher.doFinal(inputStr.getBytes(StandardCharsets.UTF_8));
        // 16进制表示
        String result = Hex.encodeHexString(bytes);
        System.out.println("加密结果:" + result);

        cipher.init(Cipher.DECRYPT_MODE, key, ivParameterSpec);
        byte[] bytes1 = cipher.doFinal(Hex.decodeHex(result));
        System.out.println("解密结果:" + new String(bytes1));

        /*-- https://dev.mysql.com/doc/refman/8.0/en/encryption-functions.html#function_aes-encrypt
        -- mysql AES 默认使用 aes-128-ebc, 如果要设置 iv 向量则需要修改默认值
        SET block_encryption_mode = 'aes-128-cbc';
        SELECT HEX(AES_ENCRYPT('PassWord', '1234567890123456', '1234567890123456'));*/
}

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一级缓存失效的几种情况icon-default.png?t=N7T8http://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 插件开发

        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从入门到精通

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值