Mybatis是比較流行的數據庫持久層架構,能夠很方便的與spring集成。框架比較輕量化,因此學習和上手的時間短,是一個不錯的選擇。 做爲一個開源框架,Mybatis的設計值得稱道。其中之一就是咱們能夠經過插件很方便的擴展Mybatis的功能。 下面咱們經過一個簡單的例子說明其工做原理。java
插件類首先必須實現org.apache.ibatis.plugin.Interceptor
接口,以下:spring
@Intercepts( //Signature定義被攔截的接口方法,能夠有一個或多個。 @Signature( //攔截的接口類型,支持 Executor,ParameterHandler,ResultSetHandler,StatementHandler //這裏以Executor爲例 type = Executor.class, //Executor中的方法名 method = "query", //Executor中query方法的參數類型 args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} )) public class ExamplePlugin implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { //咱們能夠在這裏作一些擴展工做,但必定要在瞭解mybatis運行原理以後才能開發出你所指望的效果。 System.out.println("example plugin ..." + invocation); //由於mybatis的使用責任鏈方式,這裏必定要顯示的調用proceed方法便調用能傳遞下去。 return invocation.proceed(); } @Override public Object plugin(Object target) { //判斷是不是本攔截器須要攔截的接口類型,若是是增長代理 if (target instanceof Executor) { return Plugin.wrap(target, this); } //若是不是返回源對象。 else { return target; } } @Override public void setProperties(Properties properties) { //能夠設置攔截器的屬性,在這裏我先忽略。 } }
Plugin.wrap(target, this)
這句代碼的功能是用咱們的插件代理target
對象。實現以下:sql
public static Object wrap(Object target, Interceptor interceptor) { Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<?> type = target.getClass(); Class<?>[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0) { return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target; }
Plugin
實現了InvocationHandler
接口,經過jdk的代理機制把咱們的插件(Interceptor
)做爲代理插入Mybatis的邏輯中。數據庫
咱們經過Mapper執行查詢的時候,插件會先於Mybatis的內部執行代碼執行,其中起關鍵做用的是intercept
方法,成功與否全靠它了。apache
要實現插件,必須先了解Invocation
對象的屬性,屬性以下session
args包括三個對象,分別是:mybatis
RowBounds.DEFAULT
咱們以Mysql數據庫分頁爲例,看插件是如何改變執行效果。架構
Mybatis執行的sql是經過xml方式配置的,因此,若是咱們要實現分頁功能須要修改執行的sql,設置分頁參數便可。說來容易作來難啊,直接上代碼。app
import org.apache.ibatis.builder.StaticSqlSource; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.plugin.*; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.SystemMetaObject; import org.apache.ibatis.scripting.defaults.RawSqlSource; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import java.util.*; /** * Mysql分頁插件 * Created by WangHuanyu on 2015/11/5. */ @Intercepts( //Signature定義被攔截的接口方法,能夠有一個或多個。 @Signature( //攔截的接口類型,支持 Executor,ParameterHandler,ResultSetHandler,StatementHandler //這裏以Executor爲例 type = Executor.class, //Executor中的方法名 method = "query", //Executor中query方法的參數類型 args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} )) public class ExamplePagePlugin implements Interceptor { //分頁的id後綴 String SUFFIX_PAGE = "_PageHelper"; //count查詢的id後綴 String SUFFIX_COUNT = SUFFIX_PAGE + "_Count"; //第一個分頁參數 String PAGEPARAMETER_FIRST = "First" + SUFFIX_PAGE; //第二個分頁參數 String PAGEPARAMETER_SECOND = "Second" + SUFFIX_PAGE; String PROVIDER_OBJECT = "_provider_object"; //存儲原始的參數 String ORIGINAL_PARAMETER_OBJECT = "_ORIGINAL_PARAMETER_OBJECT"; @Override public Object intercept(Invocation invocation) throws Throwable { //咱們能夠在這裏作一些擴展工做,但必定要在瞭解mybatis運行原理以後才能開發出你所指望的效果。 System.out.println("example plugin ..." + invocation); final Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0]; MetaObject msObject = SystemMetaObject.forObject(ms); BoundSql boundSql = ms.getBoundSql(args[1]); SqlSource sqlSource = ms.getSqlSource(); SqlSource tempSqlSource = sqlSource; SqlSource pageSqlSource; //實例中只演示RowSqlSource if (tempSqlSource instanceof RawSqlSource) { pageSqlSource = new PageRawSqlSource((RawSqlSource) tempSqlSource); } else { throw new RuntimeException("沒法處理該類型[" + sqlSource.getClass() + "]的SqlSource"); } msObject.setValue("sqlSource", pageSqlSource); //添加分頁參數 args[1] = setPageParameter(ms, args[1], boundSql, 6, 5); //執行分頁查詢 //由於mybatis的使用責任鏈方式,這裏必定要顯示的調用proceed方法便調用能傳遞下去。 return invocation.proceed(); } @Override public Object plugin(Object target) { //判斷是不是本攔截器須要攔截的接口類型,若是是增長代理 if (target instanceof Executor) { return Plugin.wrap(target, this); } //若是不是返回源對象。 else { return target; } } @SuppressWarnings({"unchecked", "varargs"}) public Map setPageParameter(MappedStatement ms, Object parameterObject, BoundSql boundSql, int startrow, int pagesize) { Map paramMap = processParameter(ms, parameterObject, boundSql); paramMap.put(PAGEPARAMETER_FIRST, startrow); paramMap.put(PAGEPARAMETER_SECOND, pagesize); return paramMap; } public Map<String, Object> processParameter(MappedStatement ms, Object parameterObject, BoundSql boundSql) { Map<String, Object> paramMap = null; if (parameterObject == null) { paramMap = new HashMap<String, Object>(); } else if (parameterObject instanceof Map) { //解決不可變Map的狀況 paramMap = new HashMap<String, Object>(); paramMap.putAll((Map<String, Object>) parameterObject); } else { paramMap = new HashMap<String, Object>(); //動態sql時的判斷條件不會出如今ParameterMapping中,可是必須有,因此這裏須要收集全部的getter屬性 //TypeHandlerRegistry能夠直接處理的會做爲一個直接使用的對象進行處理 boolean hasTypeHandler = ms.getConfiguration().getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass()); MetaObject metaObject = SystemMetaObject.forObject(parameterObject); if (!hasTypeHandler) { for (String name : metaObject.getGetterNames()) { paramMap.put(name, metaObject.getValue(name)); } } //下面這段方法,主要解決一個常見類型的參數時的問題 if (boundSql.getParameterMappings() != null && boundSql.getParameterMappings().size() > 0) { for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) { String name = parameterMapping.getProperty(); if (!name.equals(PAGEPARAMETER_FIRST) && !name.equals(PAGEPARAMETER_SECOND) && paramMap.get(name) == null) { if (hasTypeHandler || parameterMapping.getJavaType().equals(parameterObject.getClass())) { paramMap.put(name, parameterObject); break; } } } } } //備份原始參數對象 paramMap.put(ORIGINAL_PARAMETER_OBJECT, parameterObject); return paramMap; } @Override public void setProperties(Properties properties) { //能夠設置攔截器的屬性,在這裏我先忽略。 } public class PageRawSqlSource implements SqlSource { private SqlSource sqlSource; private String sql; private List<ParameterMapping> parameterMappings; private Configuration configuration; private SqlSource original; public PageRawSqlSource(RawSqlSource rawSqlSource) { this.original = rawSqlSource; MetaObject metaObject = SystemMetaObject.forObject(rawSqlSource); StaticSqlSource staticSqlSource = (StaticSqlSource) metaObject.getValue("sqlSource"); metaObject = SystemMetaObject.forObject(staticSqlSource); this.sql = (String) metaObject.getValue("sql"); this.parameterMappings = (List<ParameterMapping>) metaObject.getValue("parameterMappings"); this.configuration = (Configuration) metaObject.getValue("configuration"); this.sqlSource = staticSqlSource; } @Override public BoundSql getBoundSql(Object parameterObject) { String tempSql = sql; tempSql = getPageSql(tempSql); return new BoundSql(configuration, tempSql, getPageParameterMapping(configuration, original.getBoundSql(parameterObject)), parameterObject); } public String getPageSql(String sql) { StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14); sqlBuilder.append(sql); sqlBuilder.append(" limit ?,?"); return sqlBuilder.toString(); } public List<ParameterMapping> getPageParameterMapping(Configuration configuration, BoundSql boundSql) { List<ParameterMapping> newParameterMappings = new ArrayList<ParameterMapping>(); if (boundSql != null && boundSql.getParameterMappings() != null) { newParameterMappings.addAll(boundSql.getParameterMappings()); } newParameterMappings.add(new ParameterMapping.Builder(configuration, PAGEPARAMETER_FIRST, Integer.class).build()); newParameterMappings.add(new ParameterMapping.Builder(configuration, PAGEPARAMETER_SECOND, Integer.class).build()); return newParameterMappings; } } }
本實例只是做爲分頁演示,因此參數硬編碼爲args[1] = setPageParameter(ms, args[1], boundSql, 6, 5);
。 真實環境能夠用Threadlocal
傳參。這裏就不作演示了。框架
從MappedStatement
中獲取SqlSource
,若是是RawSqlSource類型
,我就建立一個PageRawSqlSource
對象。 在PageRawSqlSource
的構造方法中,咱們從RawSqlSource
中獲取要須要的信息。
在執行時,Mybatis在獲取sql時咱們經過getPageSql
方法增長分頁語句。 經過getPageParameterMapping
方法增長分頁參數。建立一個新的BoundSql
對象,返回。
修改args[1]
,值設置爲setPageParameter
方法的返回值。
至此,咱們就能夠經過Mapper進行分頁查詢了。