Mybatis源碼分析(七)自定義緩存、分頁的實現

上一章節經過源碼已經深刻了解到插件的加載機制和時機,本章節就實戰一下。拿兩個功能點來展現插件的使用。sql

1、緩存

咱們知道,在Mybatis中是有緩存實現的。分一級緩存和二級緩存,不過一級緩存其實沒啥用。由於咱們知道它是基於sqlSession的,而sqlSession在每一次的方法執行時都會被新建立。二級緩存是基於namespace,離開了它也是不行。有沒有一種方式來提供自定義的緩存機制呢?數據庫

一、Executor

Executor是Mybatis中的執行器。全部的查詢就是調用它的<E> List<E> query()方法。咱們就能夠在這裏進行攔截,不讓它執行後面的查詢動做, 直接從緩存返回。緩存

在這個類裏面,咱們先獲取參數中的緩存標記和緩存的Key,去查詢Redis。若是命中,則返回;未命中,接着執行它自己的方法。bash

@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換成了別的數據庫,豈不是很尷尬。app

回憶一下Mybatis執行方法的整個流程。在提交執行完SQL以後,它是怎麼獲取返回值的呢?ui

二、ResultSetHandler

沒有印象嗎?就是這句return resultSetHandler.<E> handleResultSets(ps);其中的resultSetHandler就是DefaultResultSetHandler實例的對象。它負責解析並返回從數據庫查詢到的數據,那麼咱們就能夠在返回以後把它放到Redis。this

@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的粒度太粗,相同查詢方法的不一樣參數值怎麼來辨別呢?這都須要深思熟慮來設計這個字段才行。spa

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;
} 
複製代碼

2、分頁

基本每一個應用程序都有分頁的功能。從數據庫的角度來看,分頁就是肯定從第幾條開始,一共取多少條的問題。好比在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;
    }
}
複製代碼

一、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;
    }
}
複製代碼

二、獲取參數

從目標對象中,拿到各類參數,先要判斷是否須要分頁

@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();
	}
}
複製代碼

三、設置總條數

實際上,一次分頁功能要設計到兩次查詢。一次是自己的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);    
}
複製代碼

四、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
複製代碼

3、總結

本章節重點闡述了Mybatis中插件的實際使用過程。在平常開發中,緩存和分頁基本上都是能夠常見的功能點。你徹底能夠高度自定義本身的緩存機制,緩存的時機、緩存Key的設計、過時鍵的設置等....對於分頁你也應該更加清楚它們的實現邏輯,以便將來在選型的時候,你會多一份選擇。

相關文章
相關標籤/搜索