mybatis高级特性

插件机制

mybatis采用责任链模式,通过动态代理组织多个插件,通过插件改变默认的sql的行为,myabtis允许通过插件来拦截四大对象:Executor、ParameterHandler、ResultSetHandler以及StatementHandler。

1、插件机制源码

  //创建参数处理器
  public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    //创建ParameterHandler
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    //插件在这里插入
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
  }

  //创建结果集处理器
  public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
      ResultHandler resultHandler, BoundSql boundSql) {
    //创建DefaultResultSetHandler(稍老一点的版本3.1是创建NestedResultSetHandler或者FastResultSetHandler)
    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
    //插件在这里插入
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
  }

  //创建语句处理器
  public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    //创建路由选择语句处理器
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    //插件在这里插入
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

  public Executor newExecutor(Transaction transaction) {
    return newExecutor(transaction, defaultExecutorType);
  }

  //产生执行器
  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    //判断使用的执行器类型
    executorType = executorType == null ? defaultExecutorType : executorType;
    //这句再做一下保护,囧,防止粗心大意的人将defaultExecutorType设成null?
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    //然后就是简单的3个分支,产生3种执行器BatchExecutor/ReuseExecutor/SimpleExecutor
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    //如果要求缓存,生成另一种CachingExecutor(默认就是有缓存),装饰者模式,所以默认都是返回CachingExecutor
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    //此处调用插件,通过插件可以改变Executor行为
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

每一个拦截器对目标类都进行一次代理

     /**
     *@target
     *@return 层层代理后的对象
     */
    public Object pluginAll(Object target) {
        //循环调用每个Interceptor.plugin方法
        for (Interceptor interceptor : interceptors) {
            target = interceptor.plugin(target);
        }
        return target;
    }

Interceptor 接口说明

/**
 * 拦截器接口
 *
 * @author Clinton Begin
 */
public interface Interceptor {

  /**
   * 执行拦截逻辑的方法
   *
   * @param invocation 调用信息
   * @return 调用结果
   * @throws Throwable 异常
   */
  Object intercept(Invocation invocation) throws Throwable;

  /**
   * 代理类
   *
   * @param target
   * @return
   */
  Object plugin(Object target);

  /**
   * 根据配置来初始化 Interceptor 方法
   * @param properties
   */
  void setProperties(Properties properties);

}

注解拦截器并签名

@Intercepts(@Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
))
参数说明:
type 要拦截四大对象的类型
method 拦截对象中哪个方法
args 需要传入的参数

2、手写分页插件

基于ThreadLocal传递分页参数,拦截StatementHandler

@Intercepts(@Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
))
public class PagePlugin implements Interceptor {
    // 插件的核心业务
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        /**
         * 1、拿到原始的sql语句
         * 2、修改原始sql,增加分页  select * from t_user limit 0,3
         * 3、执行jdbc去查询总数
         */
        // 从invocation拿到我们StatementHandler对象
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        // 拿到原始的sql语句
        BoundSql boundSql = statementHandler.getBoundSql();
        String sql = boundSql.getSql();

        // statementHandler 转成 metaObject
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);

        // spring context.getBean("userBean")
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        // 获取mapper接口中的方法名称  selectUserByPage
        String mapperMethodName = mappedStatement.getId();
        if (mapperMethodName.matches(".*ByPage")) {
            Page page = PageUtil.getPaingParam();
            //  select * from user;
            String countSql = "select count(0) from (" + sql + ") a";
            System.out.println("查询总数的sql : " + countSql);

            // 执行jdbc操作
            Connection connection = (Connection) invocation.getArgs()[0];
            PreparedStatement countStatement = connection.prepareStatement(countSql);
            ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler");
            parameterHandler.setParameters(countStatement);
            ResultSet rs = countStatement.executeQuery();
            if (rs.next()) {
                page.setTotalNumber(rs.getInt(1));
            }
            rs.close();
            countStatement.close();

            // 改造sql limit
            String pageSql = this.generaterPageSql(sql, page);
            System.out.println("分页sql:" + pageSql);

            //将改造后的sql设置回去
            metaObject.setValue("delegate.boundSql.sql", pageSql);

        }
        // 把执行流程交给mybatis
        return invocation.proceed();
    }

    // 把自定义的插件加入到mybatis中去执行
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    // 设置属性
    @Override
    public void setProperties(Properties properties) {

    }

    // 根据原始sql 生成 带limit sql
    public String generaterPageSql(String sql, Page page) {

        StringBuffer sb = new StringBuffer();
        sb.append(sql);
        sb.append(" limit " + page.getStartIndex() + " , " + page.getTotalSelect());
        return sb.toString();
    }

}
Data
@NoArgsConstructor
public class Page {
    public Page(int currentPage,int pageSize){
        this.currentPage=currentPage;
        this.pageSize=pageSize;
    }

    private int totalNumber;// 当前表中总条目数量
    private int currentPage;// 当前页的位置

    private int totalPage;	// 总页数
    private int pageSize = 3;// 页面大小

    private int startIndex;	// 检索的起始位置
    private int totalSelect;// 检索的总数目

    public void setTotalNumber(int totalNumber) {
        this.totalNumber = totalNumber;
        // 计算
        this.count();
    }

    public void count() {
        int totalPageTemp = this.totalNumber / this.pageSize;
        int plus = (this.totalNumber % this.pageSize) == 0 ? 0 : 1;
        totalPageTemp = totalPageTemp + plus;
        if (totalPageTemp <= 0) {
            totalPageTemp = 1;
        }
        this.totalPage = totalPageTemp;// 总页数

        if (this.totalPage < this.currentPage) {
            this.currentPage = this.totalPage;
        }
        if (this.currentPage < 1) {
            this.currentPage = 1;
        }
        this.startIndex = (this.currentPage - 1) * this.pageSize;// 起始位置等于之前所有页面输乘以页面大小
        this.totalSelect = this.pageSize;// 检索数量等于页面大小
    }
}

@Data
public class PageResponse<T> {
    private int totalNumber;
    private int currentPage;
    private int totalPage;
    private int pageSize = 3;
    private T data;

}
public class PageUtil {
    private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();

    public static void setPagingParam(int offset, int limit) {
        Page page = new Page(offset, limit);
        LOCAL_PAGE.set(page);
    }

    public static void removePagingParam() {
        LOCAL_PAGE.remove();
    }

    public static Page getPaingParam() {
        return LOCAL_PAGE.get();
    }

}

使用demo

 PageUtil.setPagingParam(page,size);
        List<TUser> tUsers = tUserDao.queryByPage();
        Page pageInfo = PageUtil.getPaingParam();
        PageResponse<List<TUser>> pageResponse = new PageResponse();
        pageResponse.setData(tUsers);
        pageResponse.setCurrentPage(pageInfo.getCurrentPage());
        pageResponse.setPageSize(pageInfo.getPageSize());
        pageResponse.setTotalNumber(pageInfo.getTotalNumber());
        pageResponse.setTotalPage(pageInfo.getTotalPage());
        PageUtil.removePagingParam();
        return pageResponse;

相关配置

server:
  port: 8085
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3339/mybatis?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true
    username: root
    password: 123456
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      auto-commit: true   #自动提交
      connection-timeout: 30000 #等待连接池分配连接的最大时长
      minimum-idle: 5   #最小连接数
      maximum-pool-size: 20   #最大连接数
      idle-timeout: 600000   #连接超时的最大时长(毫秒)
      pool-name: DateSourceHikariCP
      max-lifetime: 1800000  #连接的生命时长
      connection-test-query: select 1
mybatis:
  type-aliases-package: com.example.entity
  mapper-locations: classpath:mapper/*.xml
  config-location: classpath:mybatis.xml

3、手写插件实现读写分离

基于spring动态数据源和Theadlocal,拦截Executor

@Intercepts({// mybatis 执行流程
        @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 })
})
@Slf4j
public class DynamicPlugin implements Interceptor {
    private static final Map<String, String> cacheMap = new ConcurrentHashMap<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] objects = invocation.getArgs();
        MappedStatement ms = (MappedStatement) objects[0];

        String dynamicDataSource = null;

        if ((dynamicDataSource = cacheMap.get(ms.getId())) == null) {
            // 读方法
            if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) { // select * from user;    update insert
                // !selectKey 为自增id查询主键(SELECT LAST_INSERT_ID() )方法,使用主库
                if (ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) {
                    dynamicDataSource = "write";
                } else {
                    // 负载均衡,针对多个读库
                    dynamicDataSource = "read";
                }
            } else {
                dynamicDataSource = "write";
            }

            log.info("方法[{"+ms.getId()+"}] 使用了 [{"+dynamicDataSource+"}] 数据源, SqlCommandType [{"+ms.getSqlCommandType().name()+"}]..");
            // 把id(方法名)和数据源存入map,下次命中后就直接执行
            cacheMap.put(ms.getId(), dynamicDataSource);
        }
        // 设置当前线程使用的数据源
        DynamicDataSourceHolder.putDataSource(dynamicDataSource);

        return invocation.proceed();
    }

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

    @Override
    public void setProperties(Properties properties) {
    }
}
public final class DynamicDataSourceHolder {

    // 使用ThreadLocal记录当前线程的数据源key
    private static final ThreadLocal<String> holder = new ThreadLocal<String>();

    public static void putDataSource(String name){
        holder.set(name);
    }

    public static String getDataSource(){
        return holder.get();
    }

    /**
     * 清理数据源
     */
    public static void clearDataSource() {
        holder.remove();
    }

}
public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
      return  DynamicDataSourceHolder.getDataSource();
    }


}
@Configuration
public class DataSourceConfig {
    
    @Value("${spring.datasource.db01.jdbcUrl}")
    private String db01Url;
    @Value("${spring.datasource.db01.username}")
    private String db01Username;
    @Value("${spring.datasource.db01.password}")
    private String db01Password;
    @Value("${spring.datasource.db01.driverClassName}")
    private String db01DiverClassName;

    @Bean("dataSource01")
    public DataSource dataSource01(){
        HikariDataSource dataSource01 = new HikariDataSource();
        dataSource01.setJdbcUrl(db01Url);
        dataSource01.setDriverClassName(db01DiverClassName);
        dataSource01.setUsername(db01Username);
        dataSource01.setPassword(db01Password);
        return dataSource01;
    }

    @Value("${spring.datasource.db02.jdbcUrl}")
    private String db02Url;
    @Value("${spring.datasource.db02.username}")
    private String db02Username;
    @Value("${spring.datasource.db02.password}")
    private String db02Password;
    @Value("${spring.datasource.db02.driverClassName}")
    private String db02DiverClassName;

    @Bean("dataSource02")
    public DataSource dataSource02(){
        HikariDataSource dataSource02 = new HikariDataSource();
        dataSource02.setJdbcUrl(db02Url);
        dataSource02.setDriverClassName(db02DiverClassName);
        dataSource02.setUsername(db02Username);
        dataSource02.setPassword(db02Password);
        return dataSource02;
    }
    @Bean("multipleDataSource")
    public DataSource multipleDataSource(@Qualifier("dataSource01") DataSource dataSource01,
                                         @Qualifier("dataSource02") DataSource dataSource02) {
        Map<Object, Object> datasources = new HashMap<Object, Object>();
        datasources.put("write", dataSource01);
        datasources.put("read", dataSource02);
        DynamicDataSource multipleDataSource = new DynamicDataSource();
        multipleDataSource.setDefaultTargetDataSource(dataSource01);
        multipleDataSource.setTargetDataSources(datasources);
        return multipleDataSource;
    }

}
public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
      return  DynamicDataSourceHolder.getDataSource();
    }
}
public class DynamicDataSourceTransactionManager extends DataSourceTransactionManager {
    private static final long serialVersionUID = 1L;

    public DynamicDataSourceTransactionManager(DataSource dataSource){
        super(dataSource);
    }

    /**
     * 只读事务到读库,读写事务到写库
     *
     * @param transaction
     * @param definition
     */
    @Override
    protected void doBegin(Object transaction, TransactionDefinition definition) {

        // 设置数据源
        boolean readOnly = definition.isReadOnly();
        if (readOnly) {
            DynamicDataSourceHolder.putDataSource("read");
        } else {
            DynamicDataSourceHolder.putDataSource("write");
        }
        super.doBegin(transaction, definition);
    }

    /**
     * 清理本地线程的数据源
     *
     * @param transaction
     */
    @Override
    protected void doCleanupAfterCompletion(Object transaction) {
        super.doCleanupAfterCompletion(transaction);
        DynamicDataSourceHolder.clearDataSource();
    }
}
Configuration
@MapperScan("com.example.dao")
@EnableTransactionManagement
public class MybatisConfig implements TransactionManagementConfigurer {

    private static String mybatisConfigPath = "mybatis-config.xml";

    @Autowired
    @Qualifier("multipleDataSource")
    private DataSource multipleDataSource;

    @Bean("sqlSessionFactoryBean")
    public SqlSessionFactory sqlSessionFactoryBean() throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(multipleDataSource);
        bean.setTypeAliasesPackage("com.example.entity");
        bean.setConfigLocation(new ClassPathResource(mybatisConfigPath));
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        bean.setMapperLocations(resolver.getResources("classpath*:mapper/*.xml"));
        return bean.getObject();

    }

    public PlatformTransactionManager annotationDrivenTransactionManager() {
        return new DynamicDataSourceTransactionManager(multipleDataSource);
    }
}

mybatis二级缓存

mybatis默认开启一级缓存,一级缓存是sqlsession级别的,所以在实际场景中并没有什么用,Mybatis二级缓存默认关闭,使用方式如下: 1、在全局配置文件中加入

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

2、在使用二级缓存的mapper.xml中加入

<mapper namespace="com.study.mybatis.mapper.UserMapper">
	<!--开启本mapper的namespace下的二级缓存-->
	<cache eviction="LRU" flushInterval="100000" readOnly="true" size="1024"/>
</mapper>
    <!--eviction:代表的是缓存回收策略,目前MyBatis提供以下策略。
        (1) LRU,最近最少使用的,一处最长时间不用的对象
        (2) FIFO,先进先出,按对象进入缓存的顺序来移除他们
        (3) SOFT,软引用,移除基于垃圾回收器状态和软引用规则的对象
        (4) WEAK,弱引用,更积极的移除基于垃圾收集器状态和弱引用规则的对象。这里采用的是LRU,移除最长时间不用的对形象
        flushInterval:刷新间隔时间,单位为毫秒,这里配置的是100秒刷新,如果你不配置它,那么当
        SQL被执行的时候才会去刷新缓存。
        size:引用数目,一个正整数,代表缓存最多可以存储多少个对象,不宜设置过大。设置过大会导致内存溢出。
        这里配置的是1024个对象
        readOnly:只读,意味着缓存数据只能读取而不能修改,这样设置的好处是我们可以快速读取缓存,缺点是我们没有办法修改缓存,他的默认值是false,不允许我们修改
    -->

这样我们就实现了基于单机jvm内存的myabtis二级缓存,如果是分布式应用,可以引入myabtis-redis相关依赖,实现基于redis的分布式缓存

cache type="org.mybatis.caches.redis.RedisCache" />

也可以自定义缓存,myabtis为我们预留了Cache接口

mybatis自定义类型转换器

通过用于特殊字段的统一转换、敏感字段加密等,使用方式如下:

public class MyTypeHandler implements TypeHandler {

    //private static String KEY = "123456";

    /**
     * 通过preparedStatement对象设置参数,将T类型的数据存入数据库。
     *
     * @param ps
     * @param i
     * @param parameter
     * @param jdbcType
     * @throws SQLException
     */
    @Override
    public void setParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
        try {
            String encrypt = EncryptUtil.encode(((String) parameter).getBytes());
            ps.setString(i, encrypt);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 通过列名或者下标来获取结果数据,也可以通过CallableStatement获取数据。
    @Override
    public Object getResult(ResultSet rs, String columnName) throws SQLException {
        String result = rs.getString(columnName);
        if (result != null && result != "") {
            try {
                return EncryptUtil.decode(result.getBytes());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return result;
    }

    @Override
    public Object getResult(ResultSet rs, int columnIndex) throws SQLException {
        String result = rs.getString(columnIndex);
        if (result != null && result != "") {
            try {
                return EncryptUtil.decode(result.getBytes());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return result;
    }

    @Override
    public Object getResult(CallableStatement cs, int columnIndex) throws SQLException {
        String result = cs.getString(columnIndex);
        if (result != null && result != "") {
            try {
                return EncryptUtil.decode(result.getBytes());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return result;
    }
}
public class EncryptUtil {

    //base64 解码
    public static String decode(byte[] bytes) {
        return new String(Base64.decodeBase64(bytes));
    }

    //base64 编码
    public static String encode(byte[] bytes) {
        return new String(Base64.encodeBase64(bytes));
    }
}

mybatis配置文件中引入类型转换器

     <plugins>
        <plugin interceptor="com.example.plugin.PagePlugin" >
            <property name="type" value="mysql"/>
        </plugin>
    </plugins>

在需要使用的字段中指定类型转换器

      <resultMap id="resultListUser" type="com.example.entity.User" >
	<result column="password" property="password" typeHandler="com.example.typehandler.MyTypeHandler" />
      </resultMap>

      <update id="updateUser" parameterType="com.example.entity.User">
	UPDATE user userName=#{userName typeHandler="com.example.typehandler.MyTypeHandler"} WHERE id=#{id}
      </update>

完整mybatis配置

<?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="mapUnderscoreToCamelCase" value="true" />
        <!--这个配置使全局的映射器(二级缓存)启用或禁用缓存-->
        <setting name="cacheEnabled" value="true" />
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>

    <!-- 自定义类型处理器-->
    <typeHandlers>
        <typeHandler handler="com.study.mybatis.handler.AllenTypeHandle" />
    </typeHandlers> 

    <!-- 插件 -->
    <plugins>
        <plugin interceptor="com.example.plugin.PagePlugin" >
            <property name="type" value="mysql"/>
        </plugin>
    </plugins>
    

</configuration>
原文地址:https://www.cnblogs.com/hhhshct/p/13934122.html