数据库结构及数据说明
结构
数据
对应类
接口详细说明
获取分页数据接口
@AnonymousSupport
@GetMapping("get-system-busy")
public Object getPageableUsers(@RequestParam Integer page, @RequestParam Integer size) {
return testService.getSystemBusy(page, size);
}
@PageAble
public Object getSystemBusy(Integer page, Integer size) {
return testMapper.getSystemBusy();
}
@Select("select gender from user")
List<User> getSystemBusy();
根据id获取用户详情
@AnonymousSupport
@GetMapping("get-one-user")
public Object getOneUserById(@RequestParam Integer id) {
return testService.getOneUser(id);
}
public Object getOneUser(Integer id) {
return testMapper.getOneUser(id);
}
@Select("select * from user where id = #{id}")
User getOneUser(Integer id);
获取所有司机列表
@AnonymousSupport
@GetMapping("get-all-drivers")
public Object getAllDrivers() {
return testService.getAllDrivers();
}
public Object getAllDrivers() {
return testMapper.getAllDrivers();
}
@Select("select * from driver")
List<Driver> getAllDrivers();
获取所有用户的名字
@AnonymousSupport
@GetMapping("get-username-list")
public Object getUsernameList() {
return testService.getUsernameList();
}
public Object getUsernameList() {
List<User> users = testMapper.getAllUsers();
List<String> names = new ArrayList<>();
for (User user : users) {
names.add(user.getUsername());
}
return names;
}
@Select("select * from user")
List<User> getAllUsers();
分页相关文件说明
base-service
├── pom.xml
└── src
└── main
└── java
└── com
└── haylion
└── realTimeBus
├── advice
│ └── SystemAdvice.java # 分页注解实现
├── annotation
│ └── PageAble.java # 分页注解
├── bean
│ ├── BaseModel.java # 含有id熟悉的基础bean
│ ├── Condition.java # 条件查询时的条件
│ ├── ResultPageView.java # 被分页注解标记后,改函数返回的对象,将被包装成该对象(一个普通的bean类)
│ └── Sort.java # 查询时排序方式
├── interceptor
└── sql
├── MyPageHelper.java # 覆盖afterAll后的PageHelper,在MyPageInterceptor中进行调用
├── MyPageInterceptor.java # PageHelper中拦截器PageInterceptor的源码,有自定义修改
└── SqlLogHandler.java # 把将要执行的SQL打印出来,集成在MyPageInterceptor中
前言
这个注解主要是对PageHelper
插件的封装,这个插件的工作流程可参考:此链接。
需要做的是在mybatis的配置文件中加入一个拦截器(拦截器的源代码链接)。MyBatis在执行query语句时,会触发该拦截器,然后得到的数据是处理过的分页后的数据。
在上述链接中有一个注意事项,如下:
什么时候会导致不安全的分页?
PageHelper
方法使用了静态的 ThreadLocal
参数,分页参数和线程是绑定的。
只要你可以保证在 PageHelper
方法调用后紧跟 MyBatis 查询方法,这就是安全的。因为 PageHelper
在 finally
代码段中自动清除了 ThreadLocal
存储的对象。
如果代码在进入 Executor
前发生异常,就会导致线程不可用,这属于人为的 Bug(例如接口方法和 XML 中的不匹配,导致找不到 MappedStatement
时),
这种情况由于线程不可用,也不会导致 ThreadLocal
参数被错误的使用。
但是如果你写出下面这样的代码,就是不安全的用法:
PageHelper.startPage(1, 10);
List<Country> list;
if(param1 != null){
list = countryMapper.selectIf(param1);
} else {
list = new ArrayList<Country>();
}
这种情况下由于 param1 存在 null 的情况,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。
上面这个代码,应该写成下面这个样子:
List<Country> list;
if(param1 != null){
PageHelper.startPage(1, 10);
list = countryMapper.selectIf(param1);
} else {
list = new ArrayList<Country>();
}
这种写法就能保证安全。
如果你对此不放心,你可以手动清理 ThreadLocal
存储的分页参数,可以像下面这样使用:
List<Country> list;
if(param1 != null){
PageHelper.startPage(1, 10);
try{
list = countryMapper.selectAll();
} finally {
PageHelper.clearPage();
}
} else {
list = new ArrayList<Country>();
}
这么写很不好看,而且没有必要。
用法
直接在方法上加上@PageAble
注解,并在该方法中传入两个参数,分别为page
和size
,在该方法返回后,会得到一个ResultPageView
封装对象,其中包含分页相关信息。
工作流程
SystemAdvice
定义一个切面,切点是@annotation(com.haylion.realTimeBus.annotation.PageAble)
。也就是说,每个被@PageAble
注解过的方法,都将执行下面的代码:
private static final String PAGE_ABLE = "@annotation(com.haylion.realTimeBus.annotation.PageAble)";
@Around(PAGE_ABLE)
public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
logger.info("execute method : " + proceedingJoinPoint.getSignature().getName());
try {
// 进入被@PageAble注解的方法前的准备工作
prepare(proceedingJoinPoint);
// 执行被@PageAble注解的方法
Object obj = proceedingJoinPoint.proceed();
// 执行被@PageAble注解的方法后,执行扫尾工作
Object result = after(obj);
return result;
} catch (Throwable throwable) {
logger.error("aspect execute error : ", throwable);
throw throwable;
}
}
准备工作:主要是获取page
和size
的值,然后调用PageHelper
的startPage
方法,初始化分页信息。
// PageAble中page和size的默认值分别是1和20
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PageAble {
String pageSizeName() default "size";
String pageNumName() default "page";
int pageSize() default 20;
int pageNum() default 1;
}
// 准备分页
private void prepare(ProceedingJoinPoint point) throws Exception {
Signature signature = point.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method targetMethod = methodSignature.getMethod();
PageAble pageAble = targetMethod.getAnnotation(PageAble.class);
String numName = pageAble.pageNumName();
String sizeName = pageAble.pageSizeName();
// 先获取默认的page和size值
int pageNo = pageAble.pageNum();
int pageSize = pageAble.pageSize();
Object[] paramValues = point.getArgs();
String[] paramNames = methodSignature.getParameterNames();
int length = paramNames.length;
// 遍历该方法中的所有参数,如果有page和size信息,那么就覆盖默认值为用户传入的值
for (int i = 0; i < length; i++) {
if (paramNames[i].equals(numName)) {
pageNo = (Integer) paramValues[i];
} else if (paramNames[i].equals(sizeName)) {
pageSize = (Integer) paramValues[i];
}
}
// 该方法利用ThreadLocal在本线程中插入一个分页信息的对象Page
PageHelper.startPage(pageNo, pageSize);
}
// startPage()方法的最终实现
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
Page<E> page = new Page<E>(pageNum, pageSize, count);
page.setReasonable(reasonable);
page.setPageSizeZero(pageSizeZero);
//当已经执行过orderBy的时候
Page<E> oldPage = getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
setLocalPage(page);
return page;
}
protected static void setLocalPage(Page page) {
LOCAL_PAGE.set(page);
}
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
进入SQL拦截器(即MyPageInterceptor
):这个拦截器中主要是PageHelper执行分页的步骤,相关步骤可分为:
-
判断是否需要进行分页。判断的条件为
!dialect.skip(ms, parameter, rowBounds)
,其实现为:@Override public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) { if(ms.getId().endsWith(MSUtils.COUNT)){ throw new RuntimeException("在系统中发现了多个分页插件,请检查系统配置!"); } Page page = pageParams.getPage(parameterObject, rowBounds); if (page == null) { return true; } else { //设置默认的 count 列 if(StringUtil.isEmpty(page.getCountColumn())){ page.setCountColumn(pageParams.getCountColumn()); } autoDialect.initDelegateDialect(ms); return false; } }
也就是说,通过判断
Page
是否为空来决定是否进行分页,Page
则从本线程中获取,如下:// PageHelper.java Page page = pageParams.getPage(parameterObject, rowBounds); //PageParams.java public Page getPage(Object parameterObject, RowBounds rowBounds) { Page page = PageHelper.getLocalPage(); ... } // PageMethod.java public static <T> Page<T> getLocalPage() { return LOCAL_PAGE.get(); } protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
-
获取数据的总条数。在进入此项前,会进行判断是否需要进行总数查询。这里假设进行总数查询。从源SQL解析出获取数据总条数的代码调试如下:
log如下所示:
2019-06-14 09:37:31.475 DEBUG [http-nio-8880-exec-1] o.a.i.l.j.BaseJdbcLogger - ==> Preparing: SELECT count(0) FROM advertising 2019-06-14 09:37:31.490 DEBUG [http-nio-8880-exec-1] o.a.i.l.j.BaseJdbcLogger - ==> Parameters: 2019-06-14 09:37:31.507 DEBUG [http-nio-8880-exec-1] o.a.i.l.j.BaseJdbcLogger - <== Total: 1 2019-06-14 09:37:31.508 INFO [http-nio-8880-exec-1] c.h.r.i.s.SqlLogHandler - com.haylion.realTimeBus.dao.AdvertisingMapper.getByConditionList_COUNT: select id, advertising_name, advertising_start_time, advertising_end_time, advertising_position, images_url, advertiser_url, advertiser_name, advertiser_id, settlement_type, settlement_price, create_time, create_user, audit_status, audit_opinion, audit_time, advertising_type from advertising <cost time is :45 ms > 2019-06-14 09:37:31.512 DEBUG [http-nio-8880-exec-1] o.a.i.l.j.BaseJdbcLogger - ==> Preparing: select id, advertising_name, advertising_start_time, advertising_end_time, advertising_position, images_url, advertiser_url, advertiser_name, advertiser_id, settlement_type, settlement_price, create_time, create_user, audit_status, audit_opinion, audit_time, advertising_type from advertising LIMIT ? 2019-06-14 09:37:31.512 DEBUG [http-nio-8880-exec-1] o.a.i.l.j.BaseJdbcLogger - ==> Parameters: 1(Integer) 2019-06-14 09:37:31.519 DEBUG [http-nio-8880-exec-1] o.a.i.l.j.BaseJdbcLogger - <== Total: 1 2019-06-14 09:37:31.520 INFO [http-nio-8880-exec-1] c.h.r.i.s.SqlLogHandler - com.haylion.realTimeBus.dao.AdvertisingMapper.getByConditionList: select id, advertising_name, advertising_start_time, advertising_end_time, advertising_position, images_url, advertiser_url, advertiser_name, advertiser_id, settlement_type, settlement_price, create_time, create_user, audit_status, audit_opinion, audit_time, advertising_type from advertising LIMIT 1 <cost time is :8 ms > 2019-06-14 09:37:31.591 INFO [http-nio-8880-exec-1] c.h.r.f.a.ResponseAdvice - Trace log is ====> {"url":"/advertising/getAdvertisingList","httpMethod":"GET","reqHeader":{"host":"192.168.12.39:8880","content-type":"application/json","user-agent":"curl/7.54.0","accept":"*/*","token":"fe20027352f8250571436f471a988b4d"},"reqParams":"page=1&size=1","requestBody":"","respParams":"{\"code\":200,\"message\":\"success\",\"data\":{\"total\":9,\"current\":1,\"pageCount\":9,\"list\":[{\"settlementType\":0,\"imagesUrl\":\"xxxxxxx\",\"advertisingName\":\"hello kitty 111\",\"advertiserName\":\"暁\",\"advertiserId\":0,\"createTimeymdhm_Str\":\"2019-06-10 17:27\",\"advertisingType\":0,\"createTime\":1560158854000,\"advertisingPosition\":0,\"auditStatus\":4,\"createUser\":1,\"id\":0,\"advertiserUrl\":\"xxxxx\",\"createTimeStr\":\"2019-06-10 17:27:34\"}]}}","startTime":1560476250978,"spendTime":592}
获取完总数后,会进行判断是否有分页的必要。
-
分页查询。这里假设有分页的必要。
//调用方言获取分页 sql String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey); @Override public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) { String sql = boundSql.getSql(); Page page = getLocalPage(); //支持 order by String orderBy = page.getOrderBy(); if (StringUtil.isNotEmpty(orderBy)) { pageKey.update(orderBy); sql = OrderByParser.converToOrderBySql(sql, orderBy); } if (page.isOrderByOnly()) { return sql; } // 这是一个抽象方法,会根据具体的数据库,调用不同的实现方法,来在原SQL语句上,加上对应的分页语句 return getPageSql(sql, page, pageKey); }
具体支持的数据库如下:
Oracle的分页实现如下:
// @Override public String getPageSql(String sql, Page page, CacheKey pageKey) { StringBuilder sqlBuilder = new StringBuilder(sql.length() + 120); sqlBuilder.append("SELECT * FROM ( "); sqlBuilder.append(" SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( "); sqlBuilder.append(sql); sqlBuilder.append(" ) TMP_PAGE WHERE ROWNUM <= ? "); sqlBuilder.append(" ) WHERE ROW_ID > ? "); return sqlBuilder.toString(); }
MySQL的分页实现如下:
@Override public String getPageSql(String sql, Page page, CacheKey pageKey) { StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14); sqlBuilder.append(sql); if (page.getStartRow() == 0) { sqlBuilder.append(" LIMIT ? "); } else { sqlBuilder.append(" LIMIT ?, ? "); } pageKey.update(page.getPageSize()); return sqlBuilder.toString(); }
-
保存分页查询后的结果。
// resultList是分页查询后的数据列表 // afterPage的返回值是有两种情况,但是都可以被转成List return dialect.afterPage(resultList, parameter, rowBounds); // dialect.afterPage()方法 @Override public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) { //这个方法即使不分页也会被执行,所以要判断 null AbstractHelperDialect delegate = autoDialect.getDelegate(); if(delegate != null){ return delegate.afterPage(pageList, parameterObject, rowBounds); } return pageList; } // delegate.afterPage()方法 @Override public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) { Page page = getLocalPage(); if (page == null) { return pageList; } page.addAll(pageList); if (!page.isCount()) { page.setTotal(-1); } else if ((page.getPageSizeZero() != null && page.getPageSizeZero()) && page.getPageSize() == 0) { page.setTotal(pageList.size()); } else if(page.isOrderByOnly()){ page.setTotal(pageList.size()); } return page; }
其实这里有一个问题是,如果delegate不为空,那么返回的是
Page
,但是我们在调用xxxxxMapper
的查询方法之后,返回值基本上是List
,与我们的常识并不符合。那Page
是什么呢?它不只是包含分页信息的基本类,它继承自ArrayList。public class Page<E> extends ArrayList<E> implements Closeable { // ... }
在return后,还会执行finally中的处理代码,即
com.haylion.realTimeBus.interceptor.sql.MyPageHelper
的afterAll()
方法。其中实现如下:// com.haylion.realTimeBus.interceptor.sql.MyPageHelper.afterAll() // 这个方法是我们自定义的方法,用来处理执行完前面所述的切点后,保留分页信息,进行再次封装 @Override public void afterAll() { Page<Object> localPage = getLocalPage(); // 删除分页信息 super.afterAll(); // 设置回本线程中 setLocalPage(localPage); } // super.afterAll()。这个方法可以简单理解成,清楚掉本线程中的分页信息 @Override public void afterAll() { //这个方法即使不分页也会被执行,所以要判断 null AbstractHelperDialect delegate = autoDialect.getDelegate(); if (delegate != null) { delegate.afterAll(); autoDialect.clearDelegate(); } clearPage(); } // 移除本地变量 public static void clearPage() { LOCAL_PAGE.remove(); }
经过上述的过程,
MyPageInterceptor
执行完毕,分页信息存储在本线程中,然后回到切面处理。
切面收尾工作(回到SystemAdvice
):
private Object after(Object obj) {
// ...
PageInfo<?> pageInfo;
Page<Object> localPage = PageHelper.getLocalPage();
long total = localPage.getTotal();
int pageNum = localPage.getPageNum();
int pages = localPage.getPages();
List<?> list = (List<?>) obj;
// ...
pageInfo = new PageInfo((list));
ResultPageView<?> resultPageView = new ResultPageView<>(total, pageNum, pages, pageInfo.getList());
return resultPageView;
}
至此,备注解方法将返回ResultPageView
对象,经过包装后,也就是我们常看到的分页格式:
{
"code": 200,
"message": "success",
"data": {
"total": 17,
"current": 1,
"pageCount": 9,
"list": []
}
}
局限
这就限定了在一个被PageAble
注解了的方法上,只能执行一条查询。如果对于一个到来的请求,需要进行两次或以上的查询,并且某一条查询需要分页的情况,如果所有的查询都放在被PageAble
注解的方法下,执行会出现问题(出现不必要的分页操作)。但是可以通过组装的形式,完成该项需求。
bug说明
-
当一条线程,执行被PageAble注解过的方法时,线程中会保存Page信息。
-
如果切面没有执行完,会导致Page在处理完改请求后,继续留存在该线程中。
-
当一条含有Page对象的线程,处理某个不分页、但需进行查询的请求时,会导致该查询进行分页,并且会将Page对象中的之前查询得到的数据一并返回。
四个请求的各自功能
-
在线程中留下分页标志。
curl -s localhost:8880/test/get-system-busy\?page=1\&size=2 | jq --indent 4
-
在分页后的数据列表中,加入数据。
curl -s localhost:8880/test/get-all-drivers | jq --indent 4
-
出错情况1:TooManyResultsException。
curl -s localhost:8880/test/get-one-user\?id=1 | jq --indent 4
-
出错情况2 & 3:ClassCastException & 数据累积。
curl -s localhost:8880/test/get-username-list | jq --indent 4
PPT内容
略