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

栏目: Java · 发布时间: 5年前

内容简介:上一章节通过源码已经深入了解到插件的加载机制和时机,本章节就实战一下。拿两个功能点来展示插件的使用。我们知道,在Mybatis中是有缓存实现的。分一级缓存和二级缓存,不过一级缓存其实没啥用。因为我们知道它是基于sqlSession的,而sqlSession在每一次的方法执行时都会被新创建。二级缓存是基于namespace,离开了它也是不行。有没有一种方式来提供自定义的缓存机制呢?Executor是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;
} 
--------------------------------
关小羽
小露娜
亚麻瑟
小鲁班
数据总条数:4
复制代码

三、总结

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


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Algorithms

Algorithms

Sanjoy Dasgupta、Christos H. Papadimitriou、Umesh Vazirani / McGraw-Hill Education / 2006-10-16 / GBP 30.99

This text, extensively class-tested over a decade at UC Berkeley and UC San Diego, explains the fundamentals of algorithms in a story line that makes the material enjoyable and easy to digest. Emphasi......一起来看看 《Algorithms》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具