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进行分页查询了。

相关文章
相关标签/搜索