一、概述
本文主要是通过Mybatis拦截器,实现一个简易的分页插件。本实例旨在学习Mybatis拦截器的用法,所以在实现分页插件的过程中,更重视的是对拦截器使用方法的分析,而非实现的分页插件本身。
二、环境
- Windows开发环境
- Eclipse开发工具
- MySQL数据库
三、分页插件代码
分页插件实现的源码下载 https://gitee.com/hsh2015/mybatis_source_learning
- 拦截器类 PaginationInterceptor
package com.hsh.test.modules.interceptor;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.collections.MapUtils;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
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;
@Intercepts({
@Signature(type = StatementHandler.class,
method = "prepare",
args = {Connection.class,Integer.class}
)
})
public class PaginationInterceptor implements Interceptor {
private int DEFAULT_PAGE_SIZE = 10;//默认一页10条数据
private int DEFAULT_PAGE_NUM = 1;//默认第一页
private String page = "page"; //当前页面参数对应的key
private String rows = "rows"; //pageSize对应的key
public static final String COUNT_SQL_REG = "^SELECTCOUNT\\(";
public static final String BLANK_REG = "\\s*|\\t|\\r|\\n|\\\\n";
private static Pattern pattern = Pattern.compile(COUNT_SQL_REG, Pattern.CASE_INSENSITIVE);
@Override
public Object intercept(Invocation invocation) throws Throwable {
if (!(invocation.getTarget() instanceof RoutingStatementHandler)) {
return invocation.proceed();
}
RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
Object param = boundSql.getParameterObject();
if (param == null) {//无参数时不做分页处理
return invocation.proceed();
}
Map<String, Object> pagination = this.parsePaginationParam(param);
if(pagination == null || pagination.isEmpty()) {
return invocation.proceed();
}else {
return this.processPagination(invocation, boundSql, pagination);
}
}
/**
* 处理分页
* @param invocation
* @param boundSql
* @param param
* @return
* @throws Exception
*/
private Object processPagination(Invocation invocation, BoundSql boundSql, Map<String, Object> param) throws Exception {
String paginationSql = this.buildPaginationSql(boundSql.getSql().trim(),param);
try {
Field field = boundSql.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, paginationSql);
Object result = invocation.proceed();
return result;
}catch (Exception e) {
throw new Exception("myBatis执行SQL失败!(" + paginationSql + ")", e);
}
}
/**
* 构建分页查询SQL
*
* @param sql
* @param param
* @return
*/
private String buildPaginationSql(String sql, Map<String, Object> param) {
Integer start = MapUtils.getInteger(param, "page");
Integer limit = MapUtils.getInteger(param, "rows");
String checkSql = sql.replaceAll(BLANK_REG, "");
Matcher matcher = pattern.matcher(checkSql);
if(matcher.find()){//排除sql count查询
return sql;
}else {
String paginationSql = sql;
if(limit != null && start != null && start > 0 && limit > 0){
start = (start-1) * limit;
paginationSql = paginationSql + " limit " + start.toString() + "," + limit.toString();
}
return paginationSql;
}
}
/**
* 处理分页参数page、rows
* @param param
* @return
*/
private Map<String, Object> parsePaginationParam(Object param) {
Map<String, Object> pagination = null;
if(param instanceof Map) {//Map类型的参数
Map<String, Object> paramMap = (Map<String, Object>) param;
if(paramMap.containsKey(this.page) && paramMap.containsKey(this.rows)) {
pagination = new HashMap<String, Object>();
pagination.put(this.page, MapUtils.getInteger(paramMap, this.page, this.DEFAULT_PAGE_NUM));
pagination.put(this.rows, MapUtils.getInteger(paramMap, this.rows, this.DEFAULT_PAGE_SIZE));
}
return pagination;
}else {//非Map类型的参数,作为PO对象处理
Class clazz = param.getClass();
try {
if(clazz != Object.class) {
pagination = new HashMap<String, Object>();
Field startField = clazz.getDeclaredField(this.page);
startField.setAccessible(true);
Field limitField = clazz.getDeclaredField(this.rows);
limitField.setAccessible(true);
pagination.put(this.page, (Integer) startField.get(param));
pagination.put(this.rows, (Integer) limitField.get(param));
}
}catch (Exception e) {
pagination = null;
}
return pagination;
}
}
@Override
public Object plugin(Object target) {
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
this.page = properties.getProperty("page", "page");
this.rows = properties.getProperty("rows", "rows");
}
}
- 修改Config配置文件
在mybatis-config.xml文件中,添加插件的配置信息,代码如下:
<plugins>
<plugin interceptor="com.hsh.test.modules.interceptor.PaginationInterceptor">
<property name="page" value="page" />
<property name="rows" value="rows" />
</plugin>
</plugins>
- Mapper映射文件
这里演示一个普通的查询操作,对应的Mapper配置。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hsh.test.modules.mapper.api.TestMapper">
<select id="queryList" resultType="map">
SELECT * FROM test
<!-- WHERE field1 = #{field1} -->
</select>
</mapper>
- 数据库表及测试数据
CREATE TABLE `test` (
`id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL ,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
`field1` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci
ROW_FORMAT=DYNAMIC
;
INSERT INTO `test` VALUES ('1', '1', 'a1');
INSERT INTO `test` VALUES ('2', '2', 'a2');
INSERT INTO `test` VALUES ('3', '3', '3');
INSERT INTO `test` VALUES ('4', '4', '4');
INSERT INTO `test` VALUES ('5', '5', '5');
INSERT INTO `test` VALUES ('6', '6', '6');
INSERT INTO `test` VALUES ('7', '7', '7');
INSERT INTO `test` VALUES ('8', '8', '8');
INSERT INTO `test` VALUES ('9', '9', '9');
- 测试方法
@Test
public void testMapper() {
SqlSession sqlSession = mySqlSessionFactory.openSession(true);//自动提交
TestMapper testMapper = sqlSession.getMapper(TestMapper.class);
Map<String, Object> param = new HashMap<String, Object>();
//param.put("field1", "a1");
param.put("page", 2);
param.put("rows", 5);
List<Map<String, Object>> list = testMapper.queryList(param);
System.out.println(list.toString());
}
四、Mybatis中Plugin插件工作流程分析
1、Mybatis-config.xml文件中关于插件配置的加载
在《Mybatis启动时的初始化过程》中,我们知道在Mybatis启动的过程中,XMLConfigBuilder类的parseConfiguration(XNode root)方法是Mybatis-config.xml配置文件的解析入口,其中pluginElement(root.evalNode(“plugins”));是用来解析Plugin插件的,具体实现如下:
//XMLConfigBuilder.java
/**
* 解析配置文件中的插件(plugin),并注册到interceptor中
* @param parent
* @throws Exception
*/
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
//Configuration.java
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
通过上面的代码,我们可以知道,针对plugin的初始化工作,主要是:1、解析配置文件中定义的插件实现类,比如上面实例中的PaginationInterceptor;2、然后把解析得到的类通过Configuration.addInterceptor()方法添加到拦截器链中,即添加到Configuration的InterceptorChain变量中,在InterceptorChain实例中其实就是一个List<Interceptor>对象在存储所有的拦截器。
2、执行查询操作时–根据目标对象生成代理对象
这里以上面的测试方法为例,跟踪整个流程的运行过程。首先是在执行查询时,到了SimpleExecutor的doQuery()方法。代码如下:
//SimpleExecutor.java
/**
* 查询操作(返回类型List<E>),真正操作由StatementHandler接口的实现类完成
*/
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
//Configuration.java
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
在上面的代码中,是真正实现数据库查询的方法,其中configuration.newStatementHandler()方法用来生成代理对象StatementHandler实例,即通过拦截器加强StatementHandler对象。实际上就是通过interceptorChain.pluginAll()方法来实现,具体过程如下:
首先,通过调用interceptorChain.pluginAll()方法。在该方法中,interceptors变量是在第一步初始化的时候,已经完成初始化的参数,即Configuration.addInterceptor()方法中进行了初始化。然后循环调用Interceptor接口的plugin()方法实现目标对象的动态代理对象生成,即在原有目标对象上添加了拦截器功能,然后生成目标的代理对象。
//InterceptorChain.java
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
在上面代码中,实际上就是执行了PaginationInterceptor的plugin()方法,即如下面代码所示,这里是通过Plugin.wrap()方法实现,在《Mybatis的Plugin模块基础学习》中已经分析了该方法,这里不再重复介绍。
//PaginationInterceptor.java
@Override
public Object plugin(Object target) {
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
3、执行查询操作时–根据生成的代理对象执行查询
在上述doQuery()方法中,通过configuration.newStatementHandler()方法实现了目标对象生成动态代理对象的逻辑。
首先,目标对象生成的动态代理对象如下所示:
然后,上述实例中,我们拦截器注解配置如下:
@Intercepts({
@Signature(type = StatementHandler.class,
method = "prepare",
args = {Connection.class,Integer.class}
)
})
根据上述注解的配置,我们可以确定,我们拦截的方法是StatementHandler类中的prepare方法。即在上述doQuery()方法中,执行prepareStatement()方法时,进行了拦截,并进行了功能增强。代码如下:
//SimpleExecutor.java
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
根据注解可以发现,其实在调用handler.prepare()方法的时候,被定义的拦截器进行了拦截,即在执行该方法的时候,实际上是执行了Plugin类的invoke()方法,代码如下:
//Plugin.java
@Override
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);
}
}
如果符合条件,就会执行interceptor.intercept(new Invocation(target, method, args));代码,这里实际上就是执行了下面代码:
//PaginationInterceptor.java
@Override
public Object intercept(Invocation invocation) throws Throwable {
if (!(invocation.getTarget() instanceof RoutingStatementHandler)) {
return invocation.proceed();
}
RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
Object param = boundSql.getParameterObject();
if (param == null) {//无参数时不做分页处理
return invocation.proceed();
}
Map<String, Object> pagination = this.parsePaginationParam(param);
if(pagination == null || pagination.isEmpty()) {
return invocation.proceed();
}else {
return this.processPagination(invocation, boundSql, pagination);
}
}
//PaginationInterceptor.java
private Object processPagination(Invocation invocation, BoundSql boundSql, Map<String, Object> param) throws Exception {
String paginationSql = this.buildPaginationSql(boundSql.getSql().trim(),param);
try {
Field field = boundSql.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, paginationSql);
Object result = invocation.proceed();
return result;
}catch (Exception e) {
throw new Exception("myBatis执行SQL失败!(" + paginationSql + ")", e);
}
}
上述代码主要是添加了拦截器需要增强的功能的代码逻辑,执行完成后,然后在通过Object result = invocation.proceed();方法来实现被代理对象需要执行的方法。代码如下:
//Invocation.java
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
4、总结
在上述过程执行完成后,其实就是一个拦截器的从初始化,到拦截对应的方法,然后应用的全过程。如果存在多个拦截器,本质上没有变化,只是在对应的时间节点进行拦截即可。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/68888.html