mybatis精通之路之插件分页(拦截器)进阶
这里详细讲解了几种分页方式的原理和优缺点,适合于初学者,很容易理解,不清楚的同学可以回去瞟上几眼。。
任务分析:当然,这并不是我们这篇博客讲解的重点。记得在上一篇中,我们只是实现了最简单的插件分页实现,还非常简陋,功能也还不够完善,日常使用起来也还不够简便。所以在这里,我们对插件分页的实现原理进行一下详细的介绍,并且实现一个功能完善的分页插件。
原理剖析:
//注解拦截器并且签名
@Intercepts(@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}))
1
2
和StatementHandler服务类中prepare方法相对应。
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException
1
自定义的插件类,都需要使用@Intercepts注解,@Signature是对插件需要拦截的对象进行签名,type表示要拦截的类型,method表示拦截类中的方法,args是需要的参数,这里的参数在后面也可以获取到。
StatementHandler:数据库会话器,专门用于处理数据库会话,statement的执行操作,是一个接口。
MetaObject:mybatis工具类,可以有效的读取或修改一些重要对象的属性,基本思想是通过反射去获取和设置对象的属性值,只是MetaObject类不需要我们自己去实现具体反射的方法,已经封装好了。
通过MetaObject.getValue()和MetaObject.setValue(name,value)方法去获取对象属性值和设置对象属性值。
通过MetaObject属性的获取流程:
MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement")
1
上面代码是怎么获取到MappedStatement对象的??这里的metaStatementHandler是一个MetaObject对象。
首先通过metaStatementHandler.getValue(“delegate”)拿到真正实现StatementHandler接口的服务对象。
public class RoutingStatementHandler implements StatementHandler {
//delegate属性来自这里,是一个实现了StatementHandler接口的类
private final StatementHandler delegate;
//通过这里给delegate属性赋值
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
switch(RoutingStatementHandler.SyntheticClass_1.$SwitchMap$org$apache$ibatis$mapping$StatementType[ms.getStatementType().ordinal()]) {
case 1:
this.delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case 2:
this.delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case 3:
this.delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
拿到具体的服务对象(处理逻辑的StatementHandler实现类)后,再获取mappedStatement属性,我们再来看mappedStatement属性的定义:
public abstract class BaseStatementHandler implements StatementHandler {
protected final Configuration configuration;
protected final ObjectFactory objectFactory;
protected final TypeHandlerRegistry typeHandlerRegistry;
protected final ResultSetHandler resultSetHandler;
protected final ParameterHandler parameterHandler;
protected final Executor executor;
//定义在这里
protected final MappedStatement mappedStatement;
protected final RowBounds rowBounds;
protected BoundSql boundSql;
}
1
2
3
4
5
6
7
8
9
10
11
12
可以看出是定义在BaseStatementHandler中的属性,三个具体的服务对象都会继承BaseStatementHandler。这里有很多和执行数据库操作相关的属性,如果我们需要的话,都可以通过上述方式获取,如果相获取下层对象的属性,按照这个写法一次获取也可以拿到。
RoutingStatementHandler:不是真正的服务对象,它通过适配器模式找到正确的StatementHandler去执行操作。通过invocation.getTarget()获取到的是一个RoutingStatementHandler代理对象,再通过MappedStatement中不同的类型,找到具体的处理类。
真正实现StatementHandler接口的服务对象有:SimpleStatementHandler,PreparedStatementHandler,CallableStatementHandler都继承BaseStatementHandler,它们分别对应是三种不同的执行器:SIMPLE:默认的简单执行器;REUSE:重用预处理语句的执行期;BATCH:重用语句和批量更新处理器。
BoundSql: 用于组装SQL和参数,使用插件时需要通过它拿到当前运行的SQL和参数及参数规则。它有如下几个重要属性:
public class BoundSql {
private String sql;
private List<ParameterMapping> parameterMappings;
private Object parameterObject;
private Map<String, Object> additionalParameters;
private MetaObject metaParameters;
}
1
2
3
4
5
6
7
parameterObject:是参数本身,调用方法时传递进来的参数。可以是pojo,map或@param注解的参数等。
parameterMappings:它是一个List,存储了许多ParameterMapping对象。这个对象会描述我们的参数,参数包括属性、名称、表达式、javaType、jdbcType等。
sql:我们书写在mapper.xml文件中的一条sql语句。
MappedStatement:存储mapper.xml文件中一条sql语句配置的所有信息。
Connection:连接对象,在插件中会依赖它去进行一些数据库操作。
Configuration:包含mybatis所有的配置信息。
ParameterHandler:接口,对预编译语句进行参数设置。即将参数设置到sql语句中。它有两个重要方法:getParameterObject()用于获取参数对象和
setParameters(PreparedStatement var1)用于设置参数对象。
在对自定义分页插件中会使用到的各个参数有了理解后,我们就来具体实现这个分页插件。
在插件中我们使用了一个辅助类,来封装分页时会用到的一些参数,定义如下:
package com.cbg.interceptor;
/**
* Created by chenboge on 2017/5/14.
* <p>
* Email:[email protected]
* <p>
* description:实现分页的辅助类,用于封装用于分页的一些参数
*/
public class PageParam {
private Integer defaultPage;
// 默认每页显示条数
private Integer defaultPageSize;
// 是否启用分页功能
private Boolean defaultUseFlag;
// 是否检测当前页码的合法性(大于最大页码或小于最小页码都不合法)
private Boolean defaultCheckFlag;
//当前sql查询的总记录数,回填
private Integer totle;
// 当前sql查询实现分页后的总页数,回填
private Integer totlePage;
public Integer getDefaultPage() {
return defaultPage;
}
public void setDefaultPage(Integer defaultPage) {
this.defaultPage = defaultPage;
}
public Integer getDefaultPageSize() {
return defaultPageSize;
}
public void setDefaultPageSize(Integer defaultPageSize) {
this.defaultPageSize = defaultPageSize;
}
public Boolean isDefaultUseFlag() {
return defaultUseFlag;
}
public void setDefaultUseFlag(Boolean defaultUseFlag) {
this.defaultUseFlag = defaultUseFlag;
}
public Boolean isDefaultCheckFlag() {
return defaultCheckFlag;
}
public void setDefaultCheckFlag(Boolean defaultCheckFlag) {
this.defaultCheckFlag = defaultCheckFlag;
}
public Integer getTotle() {
return totle;
}
public void setTotle(Integer totle) {
this.totle = totle;
}
public Integer getTotlePage() {
return totlePage;
}
public void setTotlePage(Integer totlePage) {
this.totlePage = totlePage;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
当需要使用到分页功能时,我们只需要将分页参数封装到PageParam对象中,并且作为参数传递到查询方法中,插件中就会自动获取到这些参数,并且动态组分页的Sql查询语句。下面就是我们自定义的分页插件类实现:
package com.cbg.interceptor;
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.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import javax.security.auth.login.Configuration;
import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;
/**
* Created by chenboge on 2017/5/14.
* <p>
* Email:[email protected]
* <p>
* description:插件分页
*/
@Intercepts(@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}))
public class PageInterceptor implements Interceptor {
// 默认页码
private Integer defaultPage;
// 默认每页显示条数
private Integer defaultPageSize;
// 是否启用分页功能
private boolean defaultUseFlag;
// 检测当前页码的合法性(大于最大页码或小于最小页码都不合法)
private boolean defaultCheckFlag;
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = getActuralHandlerObject(invocation);
MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
String sql = statementHandler.getBoundSql().getSql();
// 检测未通过,不是select语句
if (!checkIsSelectFalg(sql)) {
return invocation.proceed();
}
BoundSql boundSql = statementHandler.getBoundSql();
Object paramObject = boundSql.getParameterObject();
PageParam pageParam = getPageParam(paramObject);
if (pageParam == null)
return invocation.proceed();
Integer pageNum = pageParam.getDefaultPage() == null ? defaultPage : pageParam.getDefaultPage();
Integer pageSize = pageParam.getDefaultPageSize() == null ? defaultPageSize : pageParam.getDefaultPageSize();
Boolean useFlag = pageParam.isDefaultUseFlag() == null ? defaultUseFlag : pageParam.isDefaultUseFlag();
Boolean checkFlag = pageParam.isDefaultCheckFlag() == null ? defaultCheckFlag : pageParam.isDefaultCheckFlag();
//不使用分页功能
if (!useFlag) {
return invocation.proceed();
}
int totle = getTotle(invocation, metaStatementHandler, boundSql);
//将动态获取到的分页参数回填到pageParam中
setTotltToParam(pageParam, totle, pageSize);
//检查当前页码的有效性
checkPage(checkFlag, pageNum, pageParam.getTotlePage());
//修改sql
return updateSql2Limit(invocation, metaStatementHandler, boundSql, pageNum, pageSize);
}
@Override
public Object plugin(Object o) {
return Plugin.wrap(o, this);
}
// 在配置插件的时候配置默认参数
@Override
public void setProperties(Properties properties) {
String strDefaultPage = properties.getProperty("default.page");
String strDefaultPageSize = properties.getProperty("default.pageSize");
String strDefaultUseFlag = properties.getProperty("default.useFlag");
String strDefaultCheckFlag = properties.getProperty("default.checkFlag");
defaultPage = Integer.valueOf(strDefaultPage);
defaultPageSize = Integer.valueOf(strDefaultPageSize);
defaultUseFlag = Boolean.valueOf(strDefaultUseFlag);
defaultCheckFlag = Boolean.valueOf(strDefaultCheckFlag);
}
// 从代理对象中分离出真实statementHandler对象,非代理对象
private StatementHandler getActuralHandlerObject(Invocation invocation) {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
Object object = null;
// 分离代理对象链,目标可能被多个拦截器拦截,分离出最原始的目标类
while (metaStatementHandler.hasGetter("h")) {
object = metaStatementHandler.getValue("h");
metaStatementHandler = SystemMetaObject.forObject(object);
}
if (object == null) {
return statementHandler;
}
return (StatementHandler) object;
}
// 判断是否是select语句,只有select语句,才会用到分页
private boolean checkIsSelectFalg(String sql) {
String trimSql = sql.trim();
int index = trimSql.toLowerCase().indexOf("select");
return index == 0;
}
/*
获取分页的参数
参数可以通过map,@param注解进行参数传递。或者请求pojo继承自PageParam 将PageParam中的分页数据放进去
*/
private PageParam getPageParam(Object paramerObject) {
if (paramerObject == null) {
return null;
}
PageParam pageParam = null;
//通过map和@param注解将PageParam参数传递进来,pojo继承自PageParam不推荐使用 这里从参数中提取出传递进来的pojo继承自PageParam
// 首先处理传递进来的是map对象和通过注解方式传值的情况,从中提取出PageParam,循环获取map中的键值对,取出PageParam对象
if (paramerObject instanceof Map) {
Map<String, Object> params = (Map<String, Object>) paramerObject;
for (Map.Entry<String, Object> entry : params.entrySet()) {
if (entry.getValue() instanceof PageParam) {
return (PageParam) entry.getValue();
}
}
} else if (paramerObject instanceof PageParam) {
// 继承方式 pojo继承自PageParam 只取出我们希望得到的分页参数
pageParam = (PageParam) paramerObject;
}
return pageParam;
}
// 获取当前sql查询的记录总数
private int getTotle(Invocation invocation, MetaObject metaStatementHandler, BoundSql boundSql) {
// 获取mapper文件中当前查询语句的配置信息
MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
//获取所有配置Configuration
org.apache.ibatis.session.Configuration configuration = mappedStatement.getConfiguration();
// 获取当前查询语句的sql
String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
// 将sql改写成统计记录数的sql语句,这里是mysql的改写语句,将第一次查询结果作为第二次查询的表
String countSql = "select count(*) as totle from (" + sql + ") $_paging";
// 获取connection连接对象,用于执行countsql语句
Connection conn = (Connection) invocation.getArgs()[0];
PreparedStatement ps = null;
int totle = 0;
try {
// 预编译统计总记录数的sql
ps = conn.prepareStatement(countSql);
//构建统计总记录数的BoundSql
BoundSql countBoundSql = new BoundSql(configuration, countSql, boundSql.getParameterMappings(), boundSql.getParameterObject());
//构建ParameterHandler,用于设置统计sql的参数
ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, boundSql.getParameterObject(), countBoundSql);
//设置总数sql的参数
parameterHandler.setParameters(ps);
//执行查询语句
ResultSet rs = ps.executeQuery();
while (rs.next()) {
// 与countSql中设置的别名对应
totle = rs.getInt("totle");
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (ps != null)
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
return totle;
}
// 设置条数参数到pageparam对象
private void setTotltToParam(PageParam param, int totle, int pageSize) {
param.setTotle(totle);
param.setTotlePage(totle % pageSize == 0 ? totle / pageSize : (totle / pageSize) + 1);
}
// 修改原始sql语句为分页sql语句
private Object updateSql2Limit(Invocation invocation, MetaObject metaStatementHandler, BoundSql boundSql, int page, int pageSize) throws InvocationTargetException, IllegalAccessException, SQLException {
String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
//构建新的分页sql语句
String limitSql = "select * from (" + sql + ") $_paging_table limit ?,?";
//修改当前要执行的sql语句
metaStatementHandler.setValue("delegate.boundSql.sql", limitSql);
//相当于调用prepare方法,预编译sql并且加入参数,但是少了分页的两个参数,它返回一个PreparedStatement对象
PreparedStatement ps = (PreparedStatement) invocation.proceed();
//获取sql总的参数总数
int count = ps.getParameterMetaData().getParameterCount();
//设置与分页相关的两个参数
ps.setInt(count - 1, (page - 1) * pageSize);
ps.setInt(count, pageSize);
return ps;
}
// 验证当前页码的有效性
private void checkPage(boolean checkFlag, Integer pageNumber, Integer pageTotle) throws Exception {
if (checkFlag) {
if (pageNumber > pageTotle) {
throw new Exception("查询失败,查询页码" + pageNumber + "大于总页数" + pageTotle);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
这次的分页插件的实现相比上一遍讲到的简单拦截器来说,进行了更强一步的封装,功能性也变得更强,使用起来也更加方便,完全适用于日常数据库常见的分页场景。
总结:现在几乎所有的互联网项目都会用到插件分页的技术,这里只是提出了一些简单的实现思路和逻辑,如果你有更多的想法或者更好的方式,不妨提出来打架一起探讨下。最后,在实现这个分页插件的时候,顺便整理了一下ssm开发中需要的基础框架(项目结构、配置和需要的包整理),可以直接在上面进行二次开发,也实现了数据的简单增删改查逻辑和分页插件。对分页插件有不清楚的,或者初学者都可以借鉴一下,可能会有所帮助:项目地址ssm基础配置和分页插件