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