zoukankan      html  css  js  c++  java
  • 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>
    
  • 相关阅读:
    对软件测试的理解
    Android 经典欧美小游戏 guess who
    Shell脚本 | 安卓应用权限检查
    自动化测试 | UI Automator 进阶指南
    Shell脚本 | 截取包名
    自动化测试 | UI Automator 入门指南
    杂谈随感 | 与测试无关
    Shell脚本 | 性能测试之内存
    Shell脚本 | 健壮性测试之空指针检查
    "java.lang.IllegalStateException: No instrumentation registered! Must run under a registering instrumentation."问题解决
  • 原文地址:https://www.cnblogs.com/hhhshct/p/13934122.html
Copyright © 2011-2022 走看看