上一章節經過源碼已經深刻了解到插件的加載機制和時機,本章節就實戰一下。拿兩個功能點來展現插件的使用。sql
咱們知道,在Mybatis中是有緩存實現的。分一級緩存和二級緩存,不過一級緩存其實沒啥用。由於咱們知道它是基於sqlSession的,而sqlSession在每一次的方法執行時都會被新建立。二級緩存是基於namespace,離開了它也是不行。有沒有一種方式來提供自定義的緩存機制呢?數據庫
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
沒有印象嗎?就是這句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;
}
複製代碼
基本每一個應用程序都有分頁的功能。從數據庫的角度來看,分頁就是肯定從第幾條開始,一共取多少條的問題。好比在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對象。它負責記錄和計算數據的起始位置和總條數,以便在頁面經過計算來友好的展現分頁。
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
這個方法。 這個方法的目的是獲取數據的總條數,它涉及幾個關鍵點。
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);
}
複製代碼
還獲取到總條數以後,還要修改一次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的設計、過時鍵的設置等....對於分頁你也應該更加清楚它們的實現邏輯,以便將來在選型的時候,你會多一份選擇。