很早有这个想法,但具体的实现一直没去作,网上正好找到2篇,怕之后找不到,特意记录一下,原文地址:javascript
https://my.oschina.net/gaoguofan/blog/753406html
https://my.oschina.net/dendy/blog/385575java
MyBatis 分页拦截器实现mysql
拦截器的一个做用就是咱们能够拦截某些方法的调用,咱们能够选择在这些被拦截的方法执行先后加上某些逻辑,也能够在执行这些被拦截的方法时执行本身的逻辑而再也不执行被拦截的方法。Mybatis拦截器设计的一个初衷就是为了供用户在某些时候能够实现本身的逻辑而没必要去动Mybatis固有的逻辑。打个比方,对于Executor,Mybatis中有几种实现:BatchExecutor、ReuseExecutor、SimpleExecutor和CachingExecutor。这个时候若是你以为这几种实现对于Executor接口的query方法都不能知足你的要求,那怎么办呢?是要去改源码吗?固然不。咱们能够创建一个Mybatis拦截器用于拦截Executor接口的query方法,在拦截以后实现本身的query方法逻辑,以后能够选择是否继续执行原来的query方法。ajax
对于拦截器Mybatis为咱们提供了一个Interceptor接口,经过实现该接口就能够定义咱们本身的拦截器。咱们先来看一下这个接口的定义:算法
package org.apache.ibatis.plugin; import java.util.Properties; public interface Interceptor { Object intercept(Invocation invocation) throws Throwable; Object plugin(Object target); void setProperties(Properties properties); }
咱们能够看到在该接口中一共定义有三个方法,intercept、plugin和setProperties。plugin方法是拦截器用于封装目标对象的,经过该方法咱们能够返回目标对象自己,也能够返回一个它的代理。当返回的是代理的时候咱们能够对其中的方法进行拦截来调用intercept方法,固然也能够调用其余方法,这点将在后文讲解。setProperties方法是用于在Mybatis配置文件中指定一些属性的。
定义本身的Interceptor最重要的是要实现plugin方法和intercept方法,在plugin方法中咱们能够决定是否要进行拦截进而决定要返回一个什么样的目标对象。而intercept方法就是要进行拦截的时候要执行的方法。
对于plugin方法而言,其实Mybatis已经为咱们提供了一个实现。Mybatis中有一个叫作Plugin的类,里面有一个静态方法wrap(Object target,Interceptor interceptor),经过该方法能够决定要返回的对象是目标对象仍是对应的代理。这里咱们先来看一下Plugin的源码:spring
package org.apache.ibatis.plugin; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.apache.ibatis.reflection.ExceptionUtil; public class Plugin implements InvocationHandler { private Object target; private Interceptor interceptor; private Map<Class<?>, Set<Method>> signatureMap; private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) { this.target = target; this.interceptor = interceptor; this.signatureMap = signatureMap; } 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; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { Set<Method> methods = signatureMap.get(method.getDeclaringClass()); if (methods != null && methods.contains(method)) { return interceptor.intercept(new Invocation(target, method, args)); } return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } } private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) { Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); if (interceptsAnnotation == null) { // issue #251 throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); } Signature[] sigs = interceptsAnnotation.value(); Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>(); for (Signature sig : sigs) { Set<Method> methods = signatureMap.get(sig.type()); if (methods == null) { methods = new HashSet<Method>(); signatureMap.put(sig.type(), methods); } try { Method method = sig.type().getMethod(sig.method(), sig.args()); methods.add(method); } catch (NoSuchMethodException e) { throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e); } } return signatureMap; } private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) { Set<Class<?>> interfaces = new HashSet<Class<?>>(); while (type != null) { for (Class<?> c : type.getInterfaces()) { if (signatureMap.containsKey(c)) { interfaces.add(c); } } type = type.getSuperclass(); } return interfaces.toArray(new Class<?>[interfaces.size()]); } }
咱们先看一下Plugin的wrap方法,它根据当前的Interceptor上面的注解定义哪些接口须要拦截,而后判断当前目标对象是否有实现对应须要拦截的接口,若是没有则返回目标对象自己,若是有则返回一个代理对象。而这个代理对象的InvocationHandler正是一个Plugin。因此当目标对象在执行接口方法时,若是是经过代理对象执行的,则会调用对应InvocationHandler的invoke方法,也就是Plugin的invoke方法。因此接着咱们来看一下该invoke方法的内容。这里invoke方法的逻辑是:若是当前执行的方法是定义好的须要拦截的方法,则把目标对象、要执行的方法以及方法参数封装成一个Invocation对象,再把封装好的Invocation做为参数传递给当前拦截器的intercept方法。若是不须要拦截,则直接调用当前的方法。Invocation中定义了定义了一个proceed方法,其逻辑就是调用当前方法,因此若是在intercept中须要继续调用当前方法的话能够调用invocation的procced方法。
这就是Mybatis中实现Interceptor拦截的一个思想,若是用户以为这个思想有问题或者不能彻底知足你的要求的话能够经过实现本身的Plugin来决定何时须要代理何时须要拦截。如下讲解的内容都是基于Mybatis的默认实现即经过Plugin来管理Interceptor来说解的。
对于实现本身的Interceptor而言有两个很重要的注解,一个是@Intercepts,其值是一个@Signature数组。@Intercepts用于代表当前的对象是一个Interceptor,而@Signature则代表要拦截的接口、方法以及对应的参数类型。来看一个自定义的简单Interceptor:sql
package com.tiantian.mybatis.interceptor; import java.sql.Connection; import java.util.Properties; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Plugin; import org.apache.ibatis.plugin.Signature; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; @Intercepts( { @Signature(method = "query", type = Executor.class, args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }), @Signature(method = "prepare", type = StatementHandler.class, args = { Connection.class }) }) public class MyInterceptor implements Interceptor { public Object intercept(Invocation invocation) throws Throwable { Object result = invocation.proceed(); System.out.println("Invocation.proceed()"); return result; } public Object plugin(Object target) { return Plugin.wrap(target, this); } public void setProperties(Properties properties) { String prop1 = properties.getProperty("prop1"); String prop2 = properties.getProperty("prop2"); System.out.println(prop1 + "------" + prop2); } }
首先看setProperties方法,这个方法在Configuration初始化当前的Interceptor时就会执行,这里只是简单的取两个属性进行打印。
其次看plugin方法中咱们是用的Plugin的逻辑来实现Mybatis的逻辑的。
接着看MyInterceptor类上咱们用@Intercepts标记了这是一个Interceptor,而后在@Intercepts中定义了两个@Signature,即两个拦截点。第一个@Signature咱们定义了该Interceptor将拦截Executor接口中参数类型为MappedStatement、Object、RowBounds和ResultHandler的query方法;第二个@Signature咱们定义了该Interceptor将拦截StatementHandler中参数类型为Connection的prepare方法。
最后再来看一下intercept方法,这里咱们只是简单的打印了一句话,而后调用invocation的proceed方法,使当前方法正常的调用。
对于这个拦截器,Mybatis在注册该拦截器的时候就会利用定义好的n个property做为参数调用该拦截器的setProperties方法。以后在新建可拦截对象的时候会调用该拦截器的plugin方法来决定是返回目标对象自己仍是代理对象。对于这个拦截器而言,当Mybatis是要Executor或StatementHandler对象的时候就会返回一个代理对象,其余都是原目标对象自己。而后当Executor代理对象在执行参数类型为MappedStatement、Object、RowBounds和ResultHandler的query方法或StatementHandler代理对象在执行参数类型为Connection的prepare方法时就会触发当前的拦截器的intercept方法进行拦截,而执行这两个接口对象的其余方法时都只是作一个简单的代理。数据库
注册拦截器是经过在Mybatis配置文件中plugins元素下的plugin元素来进行的。一个plugin对应着一个拦截器,在plugin元素下面咱们能够指定若干个property子元素。Mybatis在注册定义的拦截器时会先把对应拦截器下面的全部property经过Interceptor的setProperties方法注入给对应的拦截器。因此,咱们能够这样来注册咱们在前面定义的MyInterceptor:apache
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <properties resource="config/jdbc.properties"></properties> <typeAliases> <package name="com.tiantian.mybatis.model"/> </typeAliases> <plugins> <plugin interceptor="com.tiantian.mybatis.interceptor.MyInterceptor"> <property name="prop1" value="prop1"/> <property name="prop2" value="prop2"/> </plugin> </plugins> <environments default="development"> <environment id="development"> <transactionManager type="JDBC" /> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </dataSource> </environment> </environments> <mappers> <mapper resource="com/tiantian/mybatis/mapper/UserMapper.xml"/> </mappers> </configuration>
Mybatis拦截器只能拦截四种类型的接口:Executor、StatementHandler、ParameterHandler和ResultSetHandler。这是在Mybatis的Configuration中写死了的,若是要支持拦截其余接口就须要咱们重写Mybatis的Configuration。Mybatis能够对这四个接口中全部的方法进行拦截。
下面将介绍一个Mybatis拦截器的实际应用。Mybatis拦截器经常会被用来进行分页处理。咱们知道要利用JDBC对数据库进行操做就必需要有一个对应的Statement对象,Mybatis在执行Sql语句前也会产生一个包含Sql语句的Statement对象,并且对应的Sql语句是在Statement以前产生的,因此咱们就能够在它成Statement以前对用来生成Statement的Sql语句下手。在Mybatis中Statement语句是经过RoutingStatementHandler对象的prepare方法生成的。因此利用拦截器实现Mybatis分页的一个思路就是拦截StatementHandler接口的prepare方法,而后在拦截器方法中把Sql语句改为对应的分页查询Sql语句,以后再调用StatementHandler对象的prepare方法,即调用invocation.proceed()。更改Sql语句这个看起来很简单,而事实上来讲的话就没那么直观,由于包括sql等其余属性在内的多个属性都没有对应的方法能够直接取到,它们对外部都是封闭的,是对象的私有属性,因此这里就须要引入反射机制来获取或者更改对象的私有属性的值了。对于分页而言,在拦截器里面咱们经常还须要作的一个操做就是统计知足当前条件的记录一共有多少,这是经过获取到了原始的Sql语句后,把它改成对应的统计语句再利用Mybatis封装好的参数和设置参数的功能把Sql语句中的参数进行替换,以后再执行查询记录数的Sql语句进行总记录数的统计。先来看一个咱们对分页操做封装的一个实体类Page:
import java.util.HashMap; import java.util.List; import java.util.Map; /** * 对分页的基本数据进行一个简单的封装 */ public class Page<T> { private int pageNo = 1;//页码,默认是第一页 private int pageSize = 15;//每页显示的记录数,默认是15 private int totalRecord;//总记录数 private int totalPage;//总页数 private List<T> results;//对应的当前页记录 private Map<String, Object> params = new HashMap<String, Object>();//其余的参数咱们把它分装成一个Map对象 public int getPageNo() { return pageNo; } public void setPageNo(int pageNo) { this.pageNo = pageNo; } public int getPageSize() { return pageSize; } public void setPageSize(int pageSize) { this.pageSize = pageSize; } public int getTotalRecord() { return totalRecord; } public void setTotalRecord(int totalRecord) { this.totalRecord = totalRecord; //在设置总页数的时候计算出对应的总页数,在下面的三目运算中加法拥有更高的优先级,因此最后能够不加括号。 int totalPage = totalRecord%pageSize==0 ? totalRecord/pageSize : totalRecord/pageSize + 1; this.setTotalPage(totalPage); } public int getTotalPage() { return totalPage; } public void setTotalPage(int totalPage) { this.totalPage = totalPage; } public List<T> getResults() { return results; } public void setResults(List<T> results) { this.results = results; } public Map<String, Object> getParams() { return params; } public void setParams(Map<String, Object> params) { this.params = params; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("Page [pageNo=").append(pageNo).append(", pageSize=") .append(pageSize).append(", results=").append(results).append( ", totalPage=").append(totalPage).append( ", totalRecord=").append(totalRecord).append("]"); return builder.toString(); } }
对于须要进行分页的Mapper映射,咱们会给它传一个Page对象做为参数,咱们能够看到Page对象里面包括了一些分页的基本信息,这些信息咱们能够在拦截器里面用到,而后咱们把除分页的基本信息之外的其余参数用一个Map对象进行包装,这样在Mapper映射语句中的其余参数就能够从Map中取值了。接着来看一下咱们的PageInterceptor的定义,对于PageInterceptor我就不作过多的说明,代码里面附有很详细的注释信息:
package com.tiantian.mybatis.interceptor; import java.lang.reflect.Field; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; import java.util.Properties; import org.apache.ibatis.executor.parameter.ParameterHandler; import org.apache.ibatis.executor.statement.RoutingStatementHandler; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Plugin; import org.apache.ibatis.plugin.Signature; import org.apache.ibatis.scripting.defaults.DefaultParameterHandler; import com.tiantian.mybatis.model.Page; /** * * 分页拦截器,用于拦截须要进行分页查询的操做,而后对其进行分页处理。 * 利用拦截器实现Mybatis分页的原理: * 要利用JDBC对数据库进行操做就必需要有一个对应的Statement对象,Mybatis在执行Sql语句前就会产生一个包含Sql语句的Statement对象,并且对应的Sql语句 * 是在Statement以前产生的,因此咱们就能够在它生成Statement以前对用来生成Statement的Sql语句下手。在Mybatis中Statement语句是经过RoutingStatementHandler对象的 * prepare方法生成的。因此利用拦截器实现Mybatis分页的一个思路就是拦截StatementHandler接口的prepare方法,而后在拦截器方法中把Sql语句改为对应的分页查询Sql语句,以后再调用 * StatementHandler对象的prepare方法,即调用invocation.proceed()。 * 对于分页而言,在拦截器里面咱们还须要作的一个操做就是统计知足当前条件的记录一共有多少,这是经过获取到了原始的Sql语句后,把它改成对应的统计语句再利用Mybatis封装好的参数和设 * 置参数的功能把Sql语句中的参数进行替换,以后再执行查询记录数的Sql语句进行总记录数的统计。 * */ @Intercepts( { @Signature(method = "prepare", type = StatementHandler.class, args = {Connection.class}) }) public class PageInterceptor implements Interceptor { private String databaseType;//数据库类型,不一样的数据库有不一样的分页方法 /** * 拦截后要执行的方法 */ public Object intercept(Invocation invocation) throws Throwable { //对于StatementHandler其实只有两个实现类,一个是RoutingStatementHandler,另外一个是抽象类BaseStatementHandler, //BaseStatementHandler有三个子类,分别是SimpleStatementHandler,PreparedStatementHandler和CallableStatementHandler, //SimpleStatementHandler是用于处理Statement的,PreparedStatementHandler是处理PreparedStatement的,而CallableStatementHandler是 //处理CallableStatement的。Mybatis在进行Sql语句处理的时候都是创建的RoutingStatementHandler,而在RoutingStatementHandler里面拥有一个 //StatementHandler类型的delegate属性,RoutingStatementHandler会依据Statement的不一样创建对应的BaseStatementHandler,即SimpleStatementHandler、 //PreparedStatementHandler或CallableStatementHandler,在RoutingStatementHandler里面全部StatementHandler接口方法的实现都是调用的delegate对应的方法。 //咱们在PageInterceptor类上已经用@Signature标记了该Interceptor只拦截StatementHandler接口的prepare方法,又由于Mybatis只有在创建RoutingStatementHandler的时候 //是经过Interceptor的plugin方法进行包裹的,因此咱们这里拦截到的目标对象确定是RoutingStatementHandler对象。 RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget(); //经过反射获取到当前RoutingStatementHandler对象的delegate属性 StatementHandler delegate = (StatementHandler)ReflectUtil.getFieldValue(handler, "delegate"); //获取到当前StatementHandler的 boundSql,这里不论是调用handler.getBoundSql()仍是直接调用delegate.getBoundSql()结果是同样的,由于以前已经说过了 //RoutingStatementHandler实现的全部StatementHandler接口方法里面都是调用的delegate对应的方法。 BoundSql boundSql = delegate.getBoundSql(); //拿到当前绑定Sql的参数对象,就是咱们在调用对应的Mapper映射语句时所传入的参数对象 Object obj = boundSql.getParameterObject(); //这里咱们简单的经过传入的是Page对象就认定它是须要进行分页操做的。 if (obj instanceof Page<?>) { Page<?> page = (Page<?>) obj; //经过反射获取delegate父类BaseStatementHandler的mappedStatement属性 MappedStatement mappedStatement = (MappedStatement)ReflectUtil.getFieldValue(delegate, "mappedStatement"); //拦截到的prepare方法参数是一个Connection对象 Connection connection = (Connection)invocation.getArgs()[0]; //获取当前要执行的Sql语句,也就是咱们直接在Mapper映射语句中写的Sql语句 String sql = boundSql.getSql(); //给当前的page参数对象设置总记录数 this.setTotalRecord(page, mappedStatement, connection); //获取分页Sql语句 String pageSql = this.getPageSql(page, sql); //利用反射设置当前BoundSql对应的sql属性为咱们创建好的分页Sql语句 ReflectUtil.setFieldValue(boundSql, "sql", pageSql); } return invocation.proceed(); } /** * 拦截器对应的封装原始对象的方法 */ public Object plugin(Object target) { return Plugin.wrap(target, this); } /** * 设置注册拦截器时设定的属性 */ public void setProperties(Properties properties) { this.databaseType = properties.getProperty("databaseType"); } /** * 根据page对象获取对应的分页查询Sql语句,这里只作了两种数据库类型,Mysql和Oracle * 其它的数据库都 没有进行分页 * * @param page 分页对象 * @param sql 原sql语句 * @return */ private String getPageSql(Page<?> page, String sql) { StringBuffer sqlBuffer = new StringBuffer(sql); if ("mysql".equalsIgnoreCase(databaseType)) { return getMysqlPageSql(page, sqlBuffer); } else if ("oracle".equalsIgnoreCase(databaseType)) { return getOraclePageSql(page, sqlBuffer); } return sqlBuffer.toString(); } /** * 获取Mysql数据库的分页查询语句 * @param page 分页对象 * @param sqlBuffer 包含原sql语句的StringBuffer对象 * @return Mysql数据库分页语句 */ private String getMysqlPageSql(Page<?> page, StringBuffer sqlBuffer) { //计算第一条记录的位置,Mysql中记录的位置是从0开始的。 int offset = (page.getPageNo() - 1) * page.getPageSize(); sqlBuffer.append(" limit ").append(offset).append(",").append(page.getPageSize()); return sqlBuffer.toString(); } /** * 获取Oracle数据库的分页查询语句 * @param page 分页对象 * @param sqlBuffer 包含原sql语句的StringBuffer对象 * @return Oracle数据库的分页查询语句 */ private String getOraclePageSql(Page<?> page, StringBuffer sqlBuffer) { //计算第一条记录的位置,Oracle分页是经过rownum进行的,而rownum是从1开始的 int offset = (page.getPageNo() - 1) * page.getPageSize() + 1; sqlBuffer.insert(0, "select u.*, rownum r from (").append(") u where rownum < ").append(offset + page.getPageSize()); sqlBuffer.insert(0, "select * from (").append(") where r >= ").append(offset); //上面的Sql语句拼接以后大概是这个样子: //select * from (select u.*, rownum r from (select * from t_user) u where rownum < 31) where r >= 16 return sqlBuffer.toString(); } /** * 给当前的参数对象page设置总记录数 * * @param page Mapper映射语句对应的参数对象 * @param mappedStatement Mapper映射语句 * @param connection 当前的数据库链接 */ private void setTotalRecord(Page<?> page, MappedStatement mappedStatement, Connection connection) { //获取对应的BoundSql,这个BoundSql其实跟咱们利用StatementHandler获取到的BoundSql是同一个对象。 //delegate里面的boundSql也是经过mappedStatement.getBoundSql(paramObj)方法获取到的。 BoundSql boundSql = mappedStatement.getBoundSql(page); //获取到咱们本身写在Mapper映射语句中对应的Sql语句 String sql = boundSql.getSql(); //经过查询Sql语句获取到对应的计算总记录数的sql语句 String countSql = this.getCountSql(sql); //经过BoundSql获取对应的参数映射 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); //利用Configuration、查询记录数的Sql语句countSql、参数映射关系parameterMappings和参数对象page创建查询记录数对应的BoundSql对象。 BoundSql countBoundSql = new BoundSql(mappedStatement.getConfiguration(), countSql, parameterMappings, page); //经过mappedStatement、参数对象page和BoundSql对象countBoundSql创建一个用于设定参数的ParameterHandler对象 ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, page, countBoundSql); //经过connection创建一个countSql对应的PreparedStatement对象。 PreparedStatement pstmt = null; ResultSet rs = null; try { pstmt = connection.prepareStatement(countSql); //经过parameterHandler给PreparedStatement对象设置参数 parameterHandler.setParameters(pstmt); //以后就是执行获取总记录数的Sql语句和获取结果了。 rs = pstmt.executeQuery(); if (rs.next()) { int totalRecord = rs.getInt(1); //给当前的参数page对象设置总记录数 page.setTotalRecord(totalRecord); } } catch (SQLException e) { e.printStackTrace(); } finally { try { if (rs != null) rs.close(); if (pstmt != null) pstmt.close(); } catch (SQLException e) { e.printStackTrace(); } } } /** * 根据原Sql语句获取对应的查询总记录数的Sql语句 * @param sql * @return */ private String getCountSql(String sql) { return "select count(1) from (" + sql + ")"; } /** * 利用反射进行操做的一个工具类 * */ private static class ReflectUtil { /** * 利用反射获取指定对象的指定属性 * @param obj 目标对象 * @param fieldName 目标属性 * @return 目标属性的值 */ public static Object getFieldValue(Object obj, String fieldName) { Object result = null; Field field = ReflectUtil.getField(obj, fieldName); if (field != null) { field.setAccessible(true); try { result = field.get(obj); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return result; } /** * 利用反射获取指定对象里面的指定属性 * @param obj 目标对象 * @param fieldName 目标属性 * @return 目标字段 */ private static Field getField(Object obj, String fieldName) { Field field = null; for (Class<?> clazz=obj.getClass(); clazz != Object.class; clazz=clazz.getSuperclass()) { try { field = clazz.getDeclaredField(fieldName); break; } catch (NoSuchFieldException e) { //这里不用作处理,子类没有该字段可能对应的父类有,都没有就返回null。 } } return field; } /** * 利用反射设置指定对象的指定属性为指定的值 * @param obj 目标对象 * @param fieldName 目标属性 * @param fieldValue 目标值 */ public static void setFieldValue(Object obj, String fieldName, String fieldValue) { Field field = ReflectUtil.getField(obj, fieldName); if (field != null) { try { field.setAccessible(true); field.set(obj, fieldValue); } catch (IllegalArgumentException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } }
接着咱们在Mybatis的配置文件里面注册该拦截器:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <properties resource="config/jdbc.properties"></properties> <typeAliases> <package name="com.tiantian.mybatis.model"/> </typeAliases> <plugins> <plugin interceptor="com.tiantian.mybatis.interceptor.PageInterceptor"> <property name="databaseType" value="Oracle"/> </plugin> </plugins> <environments default="development"> <environment id="development"> <transactionManager type="JDBC" /> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </dataSource> </environment> </environments> <mappers> <mapper resource="com/tiantian/mybatis/mapper/UserMapper.xml"/> </mappers>
这样咱们的拦截器就已经定义而且配置好了,接下来咱们就来测试一下。假设在咱们的UserMapper.xml中有以下这样一个Mapper映射信息:
<select id="findPage" resultType="User" parameterType="page"> select * from t_user </select>
那咱们就能够这样来测试它:
SqlSession sqlSession = sqlSessionFactory.openSession(); try { UserMapper userMapper = sqlSession.getMapper(UserMapper.class); Page<User> page = new Page<User>(); page.setPageNo(2); List<User> users = userMapper.findPage(page); page.setResults(users); System.out.println(page); } finally { sqlSession.close(); }
============分界线========================================
============分界线========================================
============分界线========================================
============分界线========================================
最新项目用到springMVC和mybatis,分页其实用一个RowBounds能够实现,可是高级查询很差封装, 通过反复测试,总算搞出来了,感受封装的不是很好,有待优化和提升!
原理:利用mybatis自定义插件功能,自定义一个拦截器,拦截须要分页的sql,并想办法经过BoundSql对象进行处理,大体分8步:
一、得到BoundSql对象
二、获取原始的写在配置文件中的SQL
三、拦截到mapper中定义的执行查询方法中的参数
四、解析参数,获取高级查询参数信息
五、解析参数,获取查询限制条件
六、根据四、5中的参数拼装并从新生成SQL语句
七、将SQL设置回BoundSql对象中
八、完成。
拦截器:
package com.wtas.page.interceptor; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import javax.xml.bind.PropertyException; import org.apache.ibatis.executor.ErrorContext; import org.apache.ibatis.executor.ExecutorException; import org.apache.ibatis.executor.statement.BaseStatementHandler; import org.apache.ibatis.executor.statement.RoutingStatementHandler; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.mapping.ParameterMode; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Plugin; import org.apache.ibatis.plugin.Signature; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.property.PropertyTokenizer; import org.apache.ibatis.scripting.xmltags.ForEachSqlNode; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.type.TypeHandler; import org.apache.ibatis.type.TypeHandlerRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.wtas.page.PageContext; import com.wtas.page.Pager; import com.wtas.page.Query; import com.wtas.utils.SystemUtil; /** * 查询分页拦截器,用户拦截SQL,并加上分页的参数和高级查询条件 * * @author dendy * */ @Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) }) public class PaginationInterceptor implements Interceptor { private final Logger logger = LoggerFactory .getLogger(PaginationInterceptor.class); private String dialect = ""; // 暂时不须要这个参数,如今根据参数类型来判断是不是分页sql // private String pageMethodPattern = ""; public Object intercept(Invocation ivk) throws Throwable { if (!(ivk.getTarget() instanceof RoutingStatementHandler)) { return ivk.proceed(); } RoutingStatementHandler statementHandler = (RoutingStatementHandler) ivk .getTarget(); BaseStatementHandler delegate = (BaseStatementHandler) SystemUtil .getValueByFieldName(statementHandler, "delegate"); MappedStatement mappedStatement = (MappedStatement) SystemUtil .getValueByFieldName(delegate, "mappedStatement"); // BoundSql封装了sql语句 BoundSql boundSql = delegate.getBoundSql(); // 得到查询对象 Object parameterObject = boundSql.getParameterObject(); // 根据参数类型判断是不是分页方法 if (!(parameterObject instanceof Query)) { return ivk.proceed(); } logger.debug(" beginning to intercept page SQL..."); Connection connection = (Connection) ivk.getArgs()[0]; String sql = boundSql.getSql(); Query query = (Query) parameterObject; // 查询参数对象 Pager pager = null; // 查询条件Map Map<String, Object> conditions = query.getQueryParams(); pager = query.getPager(); // 拼装查询条件 if (conditions != null) { Set<String> keys = conditions.keySet(); Object value = null; StringBuffer sb = new StringBuffer(); boolean first = true; for (String key : keys) { value = conditions.get(key); if (first) { sb.append(" where ").append(key).append(value); first = !first; } else { sb.append(" and ").append(key).append(value); } } sql += sb.toString(); } // 获取查询数来的总数目 String countSql = "SELECT COUNT(0) FROM (" + sql + ") AS tmp "; PreparedStatement countStmt = connection.prepareStatement(countSql); BoundSql countBS = new BoundSql(mappedStatement.getConfiguration(), countSql, boundSql.getParameterMappings(), parameterObject); setParameters(countStmt, mappedStatement, countBS, parameterObject); ResultSet rs = countStmt.executeQuery(); int count = 0; if (rs.next()) { count = rs.getInt(1); } rs.close(); countStmt.close(); // 设置总记录数 pager.setTotalResult(count); // 设置总页数 pager.setTotalPage((count + pager.getShowCount() - 1) / pager.getShowCount()); // 放到做用于 PageContext.getInstance().set(pager); // 拼装查询参数 String pageSql = generatePageSql(sql, pager); SystemUtil.setValueByFieldName(boundSql, "sql", pageSql); logger.debug("generated pageSql is : " + pageSql); return ivk.proceed(); } /** * setting parameters * * @param ps * @param mappedStatement * @param boundSql * @param parameterObject * @throws SQLException */ private void setParameters(PreparedStatement ps, MappedStatement mappedStatement, BoundSql boundSql, Object parameterObject) throws SQLException { ErrorContext.instance().activity("setting parameters") .object(mappedStatement.getParameterMap().getId()); List<ParameterMapping> parameterMappings = boundSql .getParameterMappings(); if (parameterMappings != null) { Configuration configuration = mappedStatement.getConfiguration(); TypeHandlerRegistry typeHandlerRegistry = configuration .getTypeHandlerRegistry(); MetaObject metaObject = parameterObject == null ? null : configuration.newMetaObject(parameterObject); for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; String propertyName = parameterMapping.getProperty(); PropertyTokenizer prop = new PropertyTokenizer(propertyName); if (parameterObject == null) { value = null; } else if (typeHandlerRegistry .hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } else if (boundSql.hasAdditionalParameter(propertyName)) { value = boundSql.getAdditionalParameter(propertyName); } else if (propertyName .startsWith(ForEachSqlNode.ITEM_PREFIX) && boundSql.hasAdditionalParameter(prop.getName())) { value = boundSql.getAdditionalParameter(prop.getName()); if (value != null) { value = configuration.newMetaObject(value) .getValue( propertyName.substring(prop .getName().length())); } } else { value = metaObject == null ? null : metaObject .getValue(propertyName); } @SuppressWarnings("unchecked") TypeHandler<Object> typeHandler = (TypeHandler<Object>) parameterMapping .getTypeHandler(); if (typeHandler == null) { throw new ExecutorException( "There was no TypeHandler found for parameter " + propertyName + " of statement " + mappedStatement.getId()); } typeHandler.setParameter(ps, i + 1, value, parameterMapping.getJdbcType()); } } } } /** * 生成Sql语句 * * @param sql * @param page * @return */ private String generatePageSql(String sql, Pager page) { if (page != null && (dialect != null || !dialect.equals(""))) { StringBuffer pageSql = new StringBuffer(); if ("mysql".equals(dialect)) { pageSql.append(sql); pageSql.append(" LIMIT " + page.getCurrentResult() + "," + page.getShowCount()); } else if ("oracle".equals(dialect)) { pageSql.append("SELECT * FROM (SELECT t.*,ROWNUM r FROM ("); pageSql.append(sql); pageSql.append(") t WHERE r <= "); pageSql.append(page.getCurrentResult() + page.getShowCount()); pageSql.append(") WHERE r >"); pageSql.append(page.getCurrentResult()); } return pageSql.toString(); } else { return sql; } } public Object plugin(Object arg0) { return Plugin.wrap(arg0, this); } public void setProperties(Properties p) { dialect = p.getProperty("dialect"); if (dialect == null || dialect.equals("")) { try { throw new PropertyException("dialect property is not found!"); } catch (PropertyException e) { e.printStackTrace(); } } // pageMethodPattern = p.getProperty("pageMethodPattern"); if (dialect == null || dialect.equals("")) { try { throw new PropertyException( "pageMethodPattern property is not found!"); } catch (PropertyException e) { e.printStackTrace(); } } } }
查询对象的封装:
一、map封装查询条件
二、pager对象封装查询限制条件,就是MySql中limit后的参数等附加信息
package com.wtas.page; /** * 分页描述信息 * * @author dendy * */ public class Pager { // 每一页的显示条数 private int showCount; // 总的页数 private int totalPage; // 查询的数据总条数 private int totalResult; // 当前页 private int currentPage; // 从第几条开始获取数据 @SuppressWarnings("unused") private int currentResult; public Pager() { this(1); } public Pager(int currentPage) { // 默认每页显示10条记录 this(currentPage, 10); } public Pager(int currentPage, int showCount) { this.currentPage = currentPage; if (showCount > 0) { this.showCount = showCount; } // 错误处理 if (this.currentPage < 1) { this.currentPage = 1; } } //只列出关键的getter和setter…… public int getTotalPage() { // 分页算法,计算总页数 return this.totalPage; } public int getCurrentResult() { // 计算从第几条获取数据 return (currentPage - 1) * showCount; } }
package com.wtas.page; import java.util.Map; /** * 封装查询蚕食和查询条件 * * @author dendy * */ public class Query { private Map<String, Object> queryParams; private Pager pager; public Map<String, Object> getQueryParams() { return queryParams; } public void setQueryParams(Map<String, Object> queryParams) { this.queryParams = queryParams; } //省略getter和setter }
控制层关键代码:
/** * 分页时获取全部的学生 * * @return */ @RequestMapping("pageStus") @ResponseBody public List<User> pageAllStudents(HttpServletRequest req) { try { Query query = new Query(); Pager pager = new Pager(); Map<String, Object> queryParams = new HashMap<String, Object>(); // 获取分页参数 String showCount = req.getParameter("showCount"); String currentPage = req.getParameter("currentPage"); if (StringUtils.hasLength(showCount)) { pager.setShowCount(Integer.parseInt(showCount)); } if (StringUtils.hasLength(currentPage)) { pager.setCurrentPage(Integer.parseInt(currentPage)); } // 高级查询条件:学生真实姓名 String trueNameForQuery = req.getParameter("trueNameForQuery"); if (StringUtils.hasLength(trueNameForQuery)) { queryParams.put(" u.REAL_NAME like ", "'%" + trueNameForQuery + "%'"); } query.setPager(pager); query.setQueryParams(queryParams); List<User> users = userService.pageUsersByRole(query); // req.setAttribute("pager", PageContext.getInstance().get()); return users; } catch (Exception e) { LOG.error("getAllStudents error : " + e.getMessage()); } return null; } @RequestMapping("getPager") @ResponseBody public Pager getPager() { return PageContext.getInstance().get(); }
dao中的方法:
/** * 级联查询全部某一角色的用户信息,带分页 * * @param roleValue * @param page * @return */ List<User> pageUsers(Object query);
dao的Mappder.xml定义:
<select id="pageUsers" resultMap="userMapping" parameterType="hashMap"> SELECT DISTINCT u.* FROM T_USER u LEFT JOIN T_REL_USER_ROLE ur ON u.id=ur.user_id LEFT JOIN T_ROLE r ON ur.role_id=r.id </select>
页面经过javascript来异常发送请求获取数据,关键代码:
/** * 处理分页 * * @param curPage * @param id */ function page(curPage, id) { if(curPage <= 0){ curPage = 1; } var trueNameForQuery = $("#findByTrueNameInput").val().trim(); var url = path + "/studygroup/pageStus.do"; var thCss = "class='s-th-class'"; var tdCss = "class='s-td-class'"; $.ajax({ type : "POST", url : url, dataType : "json", data : { "id" : id, "currentPage" : curPage, "trueNameForQuery" : trueNameForQuery }, success : function(data) { var json = eval(data); var res = "<tr><th " + thCss + ">选择</th>" + "<th " + thCss + ">用户名</th>" + "<th " + thCss + ">真实姓名</th>" + "<th " + thCss + ">性别</th>" + "<th " + thCss + ">学校</th>" + "<th " + thCss + ">年级</th>" + "<th " + thCss + ">班级</th></tr>"; for ( var i = 0; i < json.length; i++) { var userId = json[i].id; var name = json[i].name; var trueName = json[i].trueName; var sex = json[i].sex; var school = ""; if (json[i].school) { school = json[i].school.name; } var grade = ""; if (json[i].grade) { grade = json[i].grade.name; } var clazz = ""; if (json[i].clazz) { clazz = json[i].clazz.name; } res += "<tr><td align='center' " + tdCss + "><input type='checkbox' value='" + userId + "' /></td>" + "<td align='center' " + tdCss + ">" + (name || "") + "</td>" + "<td align='center' " + tdCss + ">" + (trueName || "") + "</td>" + "<td align='center' " + tdCss + ">" + (sex == 1 ? '女' : '男' || "") + "</td>" + "<td align='center' " + tdCss + ">" + school + "</td>" + "<td align='center' " + tdCss + ">" + grade + "</td>" + "<td align='center' " + tdCss + ">" + clazz + "</td>" + "</td></tr>"; } $("#inviteStudentsTbl").html(res); // 每次加载完成都要刷新分页栏数据 freshPager(id); } }); } /** * 从新获取分页对象,刷新分页工具栏 */ function freshPager(id){ var url = path + "/studygroup/getPager.do"; var studyGroupId = id; $.ajax({ type : "POST", url : url, dataType : "json", success : function (data) { var pager = eval(data); var currentPage = pager.currentPage; // var currentResult = pager.currentResult; // var showCount = pager.showCount; var totalPage = pager.totalPage; // var totalResult = pager.totalResult; var prePage = currentPage - 1; var nextPage = currentPage + 1; if (prePage <= 0) { prePage = 1; } if (nextPage > totalPage) { nextPage = totalPage; } $("#topPageId").attr("href", "javascript:page(1, " + studyGroupId + ");"); $("#prefixPageId").attr("href", "javascript:page(" + prePage + ", " + studyGroupId + ");"); $("#nextPageId").attr("href", "javascript:page(" + nextPage + ", " + studyGroupId + ");"); $("#endPageId").attr("href", "javascript:page(" + totalPage + ", " + studyGroupId + ");"); $("#curPageId").html(currentPage); $("#totalPageId").html(totalPage); } }); } /** * 按真实姓名搜索 */ function findByTrueName() { page(1, studyGroupId); }
end.
————————————————————————————————————————————————
应网友须要,贴上SystemUtil的代码:
package com.common.utils; import java.lang.reflect.Field; import javax.servlet.http.HttpSession; import com.common.consts.SystemConst; import com.wtas.sys.domain.User; /** * 系统工具类,定义系统经常使用的工具方法 * * @author dendy * */ public class SystemUtil { private SystemUtil() { } /** * 获取系统访问的相对路径,如:/WTAS * * @return */ public static String getContextPath() { return System.getProperty(SystemConst.SYSTEM_CONTEXT_PATH_KEY); } /** * 修改一个bean(源)中的属性值,该属性值从目标bean获取 * * @param dest * 目标bean,其属性将被复制到源bean中 * @param src * 须要被修改属性的源bean * @param filtNullProps * 源bean的null属性是否覆盖目标的属性<li>true : 源bean中只有为null的属性才会被覆盖<li>false * : 无论源bean的属性是否为null,均覆盖 * @throws IllegalArgumentException * @throws IllegalAccessException */ public static void copyBean(Object dest, Object src, boolean filtNullProps) throws IllegalArgumentException, IllegalAccessException { if (dest.getClass() == src.getClass()) { // 目标bean的全部字段 Field[] destField = dest.getClass().getDeclaredFields(); // 源bean的全部字段 Field[] srcField = src.getClass().getDeclaredFields(); for (int i = 0; i < destField.length; i++) { String destFieldName = destField[i].getName(); String destFieldType = destField[i].getGenericType().toString(); for (int n = 0; n < srcField.length; n++) { String srcFieldName = srcField[n].getName(); String srcFieldType = srcField[n].getGenericType() .toString(); // String srcTypeName = // srcField[n].getType().getSimpleName(); if (destFieldName.equals(srcFieldName) && destFieldType.equals(srcFieldType)) { destField[i].setAccessible(true); srcField[n].setAccessible(true); Object srcValue = srcField[n].get(src); Object destValue = destField[i].get(dest); if (filtNullProps) { // 源bean中的属性已经非空,则不覆盖 if (srcValue == null) { srcField[n].set(src, destValue); } } else { srcField[n].set(dest, srcValue); } } } } } } /** * 根据字段的值获取该字段 * * @param obj * @param fieldName * @return */ public static Field getFieldByFieldName(Object obj, String fieldName) { for (Class<?> superClass = obj.getClass(); superClass != Object.class; superClass = superClass .getSuperclass()) { try { return superClass.getDeclaredField(fieldName); } catch (NoSuchFieldException e) { } } return null; } /** * 获取对象某一字段的值 * * @param obj * @param fieldName * @return * @throws SecurityException * @throws NoSuchFieldException * @throws IllegalArgumentException * @throws IllegalAccessException */ public static Object getValueByFieldName(Object obj, String fieldName) throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException { Field field = getFieldByFieldName(obj, fieldName); Object value = null; if (field != null) { if (field.isAccessible()) { value = field.get(obj); } else { field.setAccessible(true); value = field.get(obj); field.setAccessible(false); } } return value; } /** * 向对象的某一字段上设置值 * * @param obj * @param fieldName * @param value * @throws SecurityException * @throws NoSuchFieldException * @throws IllegalArgumentException * @throws IllegalAccessException */ public static void setValueByFieldName(Object obj, String fieldName, Object value) throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException { Field field = obj.getClass().getDeclaredField(fieldName); if (field.isAccessible()) { field.set(obj, value); } else { field.setAccessible(true); field.set(obj, value); field.setAccessible(false); } } /** * 从session中获取当前登陆用户 * * @param session * @return */ public static User getLoginUser(HttpSession session) { return (User) session.getAttribute(SystemConst.USER_IN_SESSION); } /** * @Description 设置更新信息后的登陆用户给session * @param user 登陆用户 * @param session session */ public static void setUser(User user, HttpSession session) { session.setAttribute(SystemConst.USER_IN_SESSION, user); } }