一、缓存

我们知道,在Mybatis中是有缓存实现的。分一级缓存和二级缓存,不过一级缓存其实没啥用。因为我们知道它是基于sqlSession的,而sqlSession在每一次的方法执行时都会被新创建。二级缓存是基于namespace,离开了它也是不行。有没有一种方式来提供自定义的缓存机制呢?

1、Executor

Executor是Mybatis中的执行器。所有的查询就是调用它的 <E>List<E>query()方法。我们就可以在这里进行拦截,不让它执行后面的查询动作, 直接从缓存返回。

在这个类里面,我们先获取参数中的缓存标记和缓存的Key,去查询Redis。如果命中,则返回;未命中,接着执行它本身的方法。

@Intercepts({@Signature(method = "query", type = Executor.class,args = {MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class})})//BeanFactoryAware是Spring中的接口。目的是获取jedisService的Beanpublic class ExecutorInterceptor implements Interceptor,BeanFactoryAware{private JedisServiceImpl jedisService;@SuppressWarnings("unchecked")public Object intercept(Invocation invocation) throws Throwable {if (invocation.getTarget() instanceof CachingExecutor) {//获取CachingExecutor所有的参数Object[] params = invocation.getArgs();//第二个参数就是业务方法的参数Map<String,Object> paramMap = (Map<String, Object>) params[1];String isCache = paramMap.get("isCache").toString();//判断是否需要缓存,并取到缓存的Key去查询Redisif (isCache!=null && "true".equals(isCache)) {String cacheKey = paramMap.get("cacheKey").toString();String cacheResult = jedisService.getString(cacheKey);if (cacheResult!=null) {System.out.println("已命中Redis缓存,直接返回.");return JSON.parseObject(cacheResult, new TypeReference<List<Object>>(){});}else {return invocation.proceed();}}}return invocation.proceed();}//返回代理对象public Object plugin(Object target) {if (target instanceof Executor) {return Plugin.wrap(target, this);}return target;}public void setProperties(Properties properties) {}public void setBeanFactory(BeanFactory beanFactory) throws BeansException {jedisService = (JedisServiceImpl) beanFactory.getBean("jedisServiceImpl");}
}

以上方法只是从缓存中获取数据,但什么时候往缓存中添加数据呢?总不能在每个业务方法里面调用Redis的方法,以后如果把Redis换成了别的数据库,岂不是很尴尬。

回忆一下Mybatis执行方法的整个流程。在提交执行完SQL之后,它是怎么获取返回值的呢?

2、ResultSetHandler

没有印象吗?就是这句 returnresultSetHandler.<E>handleResultSets(ps);其中的resultSetHandler就是DefaultResultSetHandler实例的对象。它负责解析并返回从数据库查询到的数据,那么我们就可以在返回之后把它放到Redis。

@Intercepts({@Signature(method = "handleResultSets",type = ResultSetHandler.class,args = {Statement.class})})
public class ResultSetHandlerInterceptor implements Interceptor,BeanFactoryAware{private JedisServiceImpl jedisService;@SuppressWarnings("unchecked")public Object intercept(Invocation invocation) throws Throwable {Object result = null;if (invocation.getTarget() instanceof DefaultResultSetHandler) {//先执行方法,以获得结果集result = invocation.proceed();      DefaultResultSetHandler handler = (DefaultResultSetHandler) invocation.getTarget();//通过反射拿到里面的成员属性,是为了最终拿到业务方法的参数Field boundsql_field = getField(handler, "boundSql");BoundSql boundSql = (BoundSql)boundsql_field.get(handler);Field param_field = getField(boundSql, "parameterObject");Map<String,Object> paramMap = (Map<String, Object>) param_field.get(boundSql);String isCache = paramMap.get("isCache").toString();if (isCache!=null && "true".equals(isCache)) {String cacheKey = paramMap.get("cacheKey").toString();String cacheResult = jedisService.getString(cacheKey);//如果缓存中没有数据,就添加进去if (cacheResult==null) {jedisService.setString(cacheKey, JSONObject.toJSONString(result));}}}return result;}public Object plugin(Object target) {if (target instanceof ResultSetHandler) {return Plugin.wrap(target, this);}return target;}private Field getField(Object obj, String name) {Field field = ReflectionUtils.findField(obj.getClass(), name);field.setAccessible(true);return field;}public void setProperties(Properties properties) {}public void setBeanFactory(BeanFactory beanFactory) throws BeansException {jedisService = (JedisServiceImpl) beanFactory.getBean("jedisServiceImpl");}
}

通过这两个拦截器,就可以实现自定义缓存。当然了,处理逻辑还是看自己的业务来定,但大体流程就是这样的。这里面最重要的其实是cacheKey的设计,怎么做到通用性以及唯一性。为什么这样说呢?想象一下,如果执行了UPDATE操作,我们需要清除缓存,那么以什么规则来清除呢?还有,如果cacheKey的粒度太粗,相同查询方法的不同参数值怎么来辨别呢?这都需要深思熟虑来设计这个字段才行。

public @ResponseBody List<User> queryAll(){Map<String,Object> paramMap = new HashMap<>();paramMap.put("isCache", "true");paramMap.put("cacheKey", "userServiceImpl.getUserList");List<User> userList = userServiceImpl.getUserList(paramMap);return userList;
} 

二、分页

基本每个应用程序都有分页的功能。从数据库的角度来看,分页就是确定从第几条开始,一共取多少条的问题。比如在MySQL中,我们可以这样 select*fromuser limit0,10

在程序中,我们不能每个SQL语句都加上limit,万一换了不支持Limit的数据库也是麻烦事。同时,limit后的0和10也并非一成不变的,这个取决于我们的页面逻辑。

在解析完BoundSql之后,Mybatis开始调用StatementHandler.prepare()方法来构建预编译对象,并设置参数值和提交SQL语句。我们的目的就是在此之前修改BoundSql中的SQL语句。先来看下拦截器的定义。

@Intercepts({@Signature(type = StatementHandler.class, method = "prepare",args = {Connection.class,Integer.class})})
public class PageInterceptor implements Interceptor {public Object intercept(Invocation invocation) throws Throwable {return invocation.proceed();}public Object plugin(Object target) {if (target instanceof RoutingStatementHandler) {return Plugin.wrap(target, this);}return target;}
}

1、Page对象

那么,第一步,我们先创建一个Page对象。它负责记录和计算数据的起始位置和总条数,以便在页面通过计算来友好的展示分页。

public class Page {public Integer start;//当前页第一条数据在List中的位置,从0开始public static final Integer pageSize = 10;//每页的条数public Integer totals;//总记录条数public boolean needPage;//是否需要分页  public Page(int pages) {setNeedPage(true);start = (pages-1)*Page.pageSize;}public boolean isNeedPage() {return needPage;}public void setNeedPage(boolean needPage) {this.needPage = needPage;}
}

2、获取参数

从目标对象中,拿到各种参数,先要判断是否需要分页

@Intercepts({@Signature(type = StatementHandler.class, method = "prepare",args = {Connection.class,Integer.class})})
public class PageInterceptor implements Interceptor {public Object intercept(Invocation invocation) throws Throwable {if (invocation.getTarget() instanceof StatementHandler) {StatementHandler statementHandler = (StatementHandler)invocation.getTarget();Field delegate_field = getField(statementHandler, "delegate");StatementHandler preparedHandler = (StatementHandler)delegate_field.get(statementHandler);          Field mappedStatement_field = getField(preparedHandler, "mappedStatement");MappedStatement mappedStatement = (MappedStatement) mappedStatement_field.get(preparedHandler);Field boundsql_field = getField(preparedHandler, "boundSql");BoundSql boundSql = (BoundSql)boundsql_field.get(preparedHandler);String sql = boundSql.getSql();Object param = boundSql.getParameterObject();if (param instanceof Map) {Map paramObject = (Map)param;if (paramObject.containsKey("page")) {//判断是否需要分页Page page = (Page)paramObject.get("page");if (!page.isNeedPage()) {return invocation.proceed();}Connection connection = (Connection) invocation.getArgs()[0];setTotals(mappedStatement,preparedHandler,page,connection,boundSql);sql = pageSql(sql, page);Field sql_field = getField(boundSql, "sql");sql_field.setAccessible(true);sql_field.set(boundSql, sql);}}}return invocation.proceed();}
}

3、设置总条数

实际上,一次分页功能要设计到两次查询。一次是本身的SQL加上Limit标签,一次是不加Limit的标签并且应该是Count语句,来获取总条数。所以,就是涉及到 setTotals这个方法。 这个方法的目的是获取数据的总条数,它涉及几个关键点。

  • 修改原来的SQL,改成Count语句。

  • 修改原来方法的返回值类型。

  • 执行SQL。

  • 把修改后的SQL和返回值类型,再改回去。

private void setTotals(MappedStatement mappedStatement,StatementHandler preparedHandler,Page page,Connection connection,BoundSql boundSql){//原来的返回值类型Class<?> old_type = Object.class;ResultMap resultMap = null;List<ResultMap> resultMaps = mappedStatement.getResultMaps();if (resultMaps!=null && resultMaps.size()>0) {resultMap = resultMaps.get(0);old_type = resultMap.getType();//修改返回值类型为Integer,因为我们获取的是总条数Field type_field = getField(resultMap, "type");type_field.setAccessible(true);type_field.set(resultMap, Integer.class);}//修改SQL为count语句String old_sql = boundSql.getSql();String count_sql = getCountSql(old_sql);Field sql_field = getField(boundSql, "sql");sql_field.setAccessible(true);sql_field.set(boundSql, count_sql);//执行SQL 并设置总条数到Page对象Statement statement =  prepareStatement(preparedHandler, connection);List<Object> resObjects = preparedHandler.query(statement, null);int result_count = (int) resObjects.get(0);page.setTotals(result_count);/*** 还要把sql和返回类型修改回去,这点很重要*/Field sql_field_t = getField(boundSql, "sql");sql_field_t.setAccessible(true);sql_field_t.set(boundSql, old_sql);Field type_field = getField(resultMap, "type");type_field.setAccessible(true);type_field.set(resultMap, old_type);
}
private String getCountSql(String sql) {    int index = sql.indexOf("from");    return "select count(1) " + sql.substring(index);
}

4、Limit

还获取到总条数之后,还要修改一次SQL,是加上Limit。最后执行,并返回结果。

String sql = boundSql.getSql();//加上Limit,从start开始sql = pageSql(sql, page);Field sql_field = getField(boundSql, "sql");sql_field.setAccessible(true);sql_field.set(boundSql, sql);private String pageSql(String sql, Page page) {StringBuffer sb = new StringBuffer();sb.append(sql);sb.append(" limit ");sb.append(page.getStart());sb.append("," + Page.pageSize);return sb.toString();}

最后,在业务方法里面直接调用即可。当然了,记住要把Page参数传过去。

public @ResponseBody List<User> queryAll(HttpServletResponse response) throws IOException {Page page = new Page(1);Map<String,Object> paramMap = new HashMap<>();paramMap.put("isCache", "true");paramMap.put("cacheKey", "userServiceImpl.getUserList");paramMap.put("page", page);List<User> userList = userServiceImpl.getUserList(paramMap);for (User user : userList) {System.out.println(user.getUsername());}System.out.println("数据总条数:"+page.getTotals());return userList;}--------------------------------关小羽小露娜亚麻瑟小鲁班数据总条数:4

三、总结

本章节重点阐述了Mybatis中插件的实际使用过程。在日常开发中,缓存和分页基本上都是可以常见的功能点。你完全可以高度自定义自己的缓存机制,缓存的时机、缓存Key的设计、过期键的设置等....对于分页你也应该更加清楚它们的实现逻辑,以便未来在选型的时候,你会多一份选择。

Mybatis源码分析(七)自定义缓存、分页的实现相关推荐

  1. okhttp配置缓存策略_Okhttp缓存源码分析以及自定义缓存实现

    原标题:Okhttp缓存源码分析以及自定义缓存实现 昨日,南京市公安局官方微博"平安南京"发布公告称,钱宝实际控制人张小雷因涉嫌违法犯罪于26日向当地警方投案自首.消息一出,迅速引 ...

  2. MyBatis 源码分析 - 缓存原理

    1.简介 在 Web 应用中,缓存是必不可少的组件.通常我们都会用 Redis 或 memcached 等缓存中间件,拦截大量奔向数据库的请求,减轻数据库压力.作为一个重要的组件,MyBatis 自然 ...

  3. 源码通透-mybatis源码分析以及整合spring过程

    源码通透-mybatis源码分析以及整合spring过程 mybatis源码分析版本:mybaits3 (3.5.0-SNAPSHOT) mybatis源码下载地址:https://github.co ...

  4. MyBatis源码分析(一)MyBatis整体架构分析

    文章目录 系列文章索引 一.为什么要用MyBatis 1.原始JDBC的痛点 2.Hibernate 和 JPA 3.MyBatis的特点 4.MyBatis整体架构 5.MyBatis主要组件及其相 ...

  5. MyBatis 源码分析 - SQL 的执行过程

    本文速览 本篇文章较为详细的介绍了 MyBatis 执行 SQL 的过程.该过程本身比较复杂,牵涉到的技术点比较多.包括但不限于 Mapper 接口代理类的生成.接口方法的解析.SQL 语句的解析.运 ...

  6. Mybatis源码分析第一天------Mybatis实用篇

    Mybatis源码分析第一天------Mybatis实用篇 一切最基本的操作就是参考官方文档:https://mybatis.org/mybatis-3/zh/configuration.html ...

  7. MyBatis 源码分析 - 配置文件解析过程

    文章目录 * 本文速览 1.简介 2.配置文件解析过程分析 2.1 配置文件解析入口 2.2 解析 properties 配置 2.3 解析 settings 配置 2.3.1 settings 节点 ...

  8. 十年老架构师神级推荐,MyBatis源码分析,再也不用为源码担忧了

    十年老架构师神级推荐,MyBatis源码分析,再也不用为源码担忧了 前言 MyBatis是一个优秀的持久层ORM框架,它对jdbc的操作数据库的过程进行封装,使开发者只需要关注SQL 本身,而不需要花 ...

  9. MyBatis 源码分析系列文章合集

    1.简介 我从七月份开始阅读MyBatis源码,并在随后的40天内陆续更新了7篇文章.起初,我只是打算通过博客的形式进行分享.但在写作的过程中,发现要分析的代码太多,以至于文章篇幅特别大.在这7篇文章 ...

  10. MyBatis 源码分析 - 插件机制

    1.简介 一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展.这样的好处是显而易见的,一是增加了框架的灵活性.二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作.以 My ...

最新文章

  1. 《预训练周刊》第14期:World-GAN:Minecraft 世界的生成模型、CMU博士论文探究可控文本生成...
  2. python学习笔记(三)tuple(元组)
  3. python38怎么用_Python基础练习实例38(数组操作)
  4. 苹果应用上架遇到的问题(2017年4月27日)
  5. Android性能优化——界面流畅度优化
  6. Solrj 存储一个point类型的字段
  7. 6-Spring Boot缓存管理
  8. springboot 学习笔记【1】开发第一个spring boot应用
  9. 行业揭秘:A股日内“T+0” 赚钱的方法
  10. Hashcat使用记录
  11. RC电路 微分器 积分器 滤波器
  12. 一个小程序走完诉讼全程,腾讯云加速推动“智慧法院”方案落地
  13. 【Pinia】小菠萝的使用
  14. RCNN SPPNet Fast R-CNN Faster R-CNN Cascade R-CNN
  15. 一名网络工程师尴尬的现状?
  16. 浅谈带宽,网速和流量之间的关系
  17. 代码生成器的使用步骤
  18. 输出一个贷款的迁徙率计算的代码
  19. 商城系统-数据库设计
  20. es教程——地图搜索

热门文章

  1. android的sdk离线安装详细教程,Android编程之SDK安装组件的离线安装方法分享
  2. HTML如何让两个div并排在一行,如何实现两个或多个div并列于一行
  3. Matlab中的fread函数
  4. 图像的形态学开操作(开运算)和闭操作(闭运算)的概念和作用,并用OpenCV的函数morphologyEx()实现对图像的开闭操作
  5. 期货合约交易平台盈利模式有哪些?
  6. 华为OD机试 - 获得完美走位(C 语言解题)【独家】
  7. Android高工面试:用Glide加载Gif导致的卡顿,说一下你的优化思路
  8. 单调栈解决取矩形问题
  9. Python numpy列表加负号
  10. 不要和一种编程语言厮守终生:为工作正确选择 !