Mybatis源码分析自定义缓存、分页的实现

一、缓存

我们知道,在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的Bean
public 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去查询Redis
if (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

没有印象吗?就是这句return resultSetHandler.<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 * from user limit 0,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;
}
--------------------------------

原文地址:https://www.cnblogs.com/hyhy904/p/11182991.html

时间: 2024-10-10 20:29:38

Mybatis源码分析自定义缓存、分页的实现的相关文章

Mybatis源码分析之Cache二级缓存原理 (五)

一:Cache类的介绍 讲解缓存之前我们需要先了解一下Cache接口以及实现MyBatis定义了一个org.apache.ibatis.cache.Cache接口作为其Cache提供者的SPI(ServiceProvider Interface) ,所有的MyBatis内部的Cache缓存,都应该实现这一接口 Cache的实现类中,Cache有不同的功能,每个功能独立,互不影响,则对于不同的Cache功能,这里使用了装饰者模式实现. 看下cache的实现类,如下图: 1.FIFOCache:先进

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

* 本文速览 由于本篇文章篇幅比较大,所以这里拿出一节对本文进行快速概括.本篇文章对 MyBatis 配置文件中常用配置的解析过程进行了较为详细的介绍和分析,包括但不限于settings,typeAliases和typeHandlers等,本文的篇幅也主要在对这三个配置解析过程的分析上.下面,我们来一起看一下本篇文章的目录结构. 从目录上可以看出,2.3节.2.5节和2.8节的内容比较多.其中2.3节是关于settings配置解析过程的分析,除了对常规的 XML 解析过程分析,本节额外的分析了元

mybatis源码分析之cache创建

XMLMapperBuilder.java //解析<cache /> 配置元素,创建cache对象 private void cacheElement(XNode context) throws Exception {     if (context != null) {       String type = context.getStringAttribute("type", "PERPETUAL");       Class<? exten

【MyBatis源码分析】select源码分析及小结

示例代码 之前的文章说过,对于MyBatis来说insert.update.delete是一组的,因为对于MyBatis来说它们都是update:select是一组的,因为对于MyBatis来说它就是select. 本文研究一下select的实现流程,示例代码为: 1 public void testSelectOne() { 2 System.out.println(mailDao.selectMailById(8)); 3 } selectMailById方法的实现为: 1 public M

MyBatis 源码分析 - 插件机制

1.简介 一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展.这样的好处是显而易见的,一是增加了框架的灵活性.二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作.以 MyBatis 为例,我们可基于 MyBatis 插件机制实现分页.分表,监控等功能.由于插件和业务无关,业务也无法感知插件的存在.因此可以无感植入插件,在无形中增强功能. 开发 MyBatis 插件需要对 MyBatis 比较深了解才行,一般来说最好能够掌握 MyBatis 的源码,门槛相对较高.本篇

MyBatis 源码分析系列文章合集

1.简介 我从七月份开始阅读MyBatis源码,并在随后的40天内陆续更新了7篇文章.起初,我只是打算通过博客的形式进行分享.但在写作的过程中,发现要分析的代码太多,以至于文章篇幅特别大.在这7篇文章中,有4篇文章字数超过了1万,最长的一篇文章约有2.7万字(含代码).考虑到超长文章对读者不太友好,以及拆分文章工作量也不小等问题.遂决定将博文整理成电子书,方便大家阅读. 经过两周紧张的排版,<一本小小的MyBatis源码分析书>诞生了.本书共7章,约300页.本书以电子书的形式发布,大家可自由

jQuery1.9.1源码分析--数据缓存Data模块

阅读目录 jQuery API中Data的基本使用方法介绍 jQuery.acceptData(elem)源码分析 jQuery.data(elem, name, data)源码分析 internalRemoveData方法源码分析 internalData方法的源码分析 jQuery.fn.extend({data: function( key, value ) {}})源码分析 jQuery.extend({removeData: function( elem, name ) {}})源码分

MyBatis源码分析-SQL语句执行的完整流程

MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简单的 XML 或注解,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录.如何新建MyBatis源码工程请点击MyBatis源码分析-IDEA新建MyBatis源码工程. MyBatis框架主要完成的是以下2件事情: 根据JD

Mybatis源码分析

MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简单的 XML 或注解,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录.如何新建MyBatis源码工程请点击MyBatis源码分析-IDEA新建MyBatis源码工程. MyBatis框架主要完成的是以下2件事情: 根据JD