Mybatis Interceptor 講解

Mybatis Interceptor 講解

簡介

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

  • private Object target 被代理對象
  • private Method method; mapper執行方法
  • private Object[] args; 參數,包括三個對象

args包括三個對象,分別是:mybatis

  • [0] MappedStatement
  • [1] 用戶調用方法時傳入的參數
  • [3] RowBounds,默認是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傳參。這裏就不作演示了。框架

  • 修改sql

MappedStatement中獲取SqlSource,若是是RawSqlSource類型,我就建立一個PageRawSqlSource對象。 在PageRawSqlSource的構造方法中,咱們從RawSqlSource中獲取要須要的信息。

在執行時,Mybatis在獲取sql時咱們經過getPageSql方法增長分頁語句。 經過getPageParameterMapping方法增長分頁參數。建立一個新的BoundSql對象,返回。

  • 添加sql參數

修改args[1],值設置爲setPageParameter方法的返回值。

至此,咱們就能夠經過Mapper進行分頁查詢了。

相關文章
相關標籤/搜索