项目架构解释说明

模块概览

base-service

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
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类)
│   ├── SMSConf.java # 将yaml文件中sms相关配置信息写入此类对象中
│   └── Sort.java # 查询时排序方式
├── handler
│   └── MySqlJsonHandler.java # MyBatis如何存储JSONObject、如何读取JSONObject
├── http
│   └── HttpInvoker.java # RestTemplate单例容器
├── interceptor
│   └── sql
│   ├── MyPageHelper.java # 覆盖afterAll后的PageHelper,在MyPageInterceptor中进行调用
│   ├── MyPageInterceptor.java # PageHelper中拦截器PageInterceptor的源码,有自定义修改
│   └── SqlLogHandler.java # 把将要执行的SQL打印出来,集成在MyPageInterceptor中
├── mappers
│   └── BaseMapper.java # 定义了访问数据库的一些基本接口
├── msg
│   └── RetStubMsg.java # 自定义ApplicationException接收的参数
└── service
├── BaseCacheService.java # 定义了一些基本的Redis访问接口
├── BaseService.java # 实现了service基础数据库操作的抽象类
├── CacheService.java # BaseCacheService的具体封装实现
├── DistributedLock.java # 通过redis实现的一种分布式锁
├── MQService.java # 阿里MQ操作封装
├── SMSService.java # 阿里短信操作封装
├── SysThreadPool.java # 自定义线程池
├── UploadService.java # 文件上传,有文件头部信息校验
└── WXPayService.java # 微信支付相关操作封装

common-service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
base-support/common-service
├── pom.xml
└── src
└── main
└── java
└── com
└── haylion
└── realTimeBus
└── common
├── code
│   ├── RetStub.java # ApplicationException所依赖的对象,定义了业务逻辑错误的访问方法
│   └── SysStubInfo.java # 系统默认的错误码信息
├── exception
   └── ApplicationException.java # 自定义异常,用来处理业务逻辑中出现的异常,其中依赖RetStub对象,来描述错误码、错误信息

facade-service

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
base-support/facade-service
├── pom.xml
└── src
└── main
└── java
└── com
└── haylion
└── realTimeBus
└── facade
├── BaseInitializer.java # 自定义Server初始化类,其中加入了拦截器ActionInterceptor,并指定拦截所有的请求
├── advice
│   └── ResponseAdvice.java # 将所有返回的body中的数据,转换成JsonView的形式
├── annotation
│   └── AnonymousSupport.java # 跳过ActionInterceptor中的校验、塞入userId
├── exception
│   └── ExceptionHandle.java # 在请求的处理过程中,出现任何异常,都会调用该类中的处理方法,将错误信息,以JsonView的形式返回。
├── filter
│   ├── RequestWrapper.java # 将请求相关的数据,如headers、queryparams、requestbody、method、url保存到本线程的LogTrace中
│   └── TraceCopyFilter.java # 拦截器,拦截所有的请求,并将请求转换成RequestWrapper
├── intercepter
│   └── ActionInterceptor.java # 所有的请求都会经过该类处理,可以在此类中对类进行自定义操作,如按需鉴定权限、添加userId到header中
├── log
│   ├── HttpTraceLog.java # HTTP请求log类
│   └── LogTrace.java # 利用ThreadLocal获取、设置HttpTraceLog
├── validator
│   └── ValidatorConfiguration.java #
└── view
└── JsonView.java # API统一响应模板

主要功能

拦截鉴权— ActionInterceptor

本项目中鉴定登录状态的方式为:在请求头部加入token,然后在拦截器ActionInterceptor.java中,从请求头部中取出token,并依据token来获取userId,然后将userId插入到头部中;如果上述过程,出现tokennulluserIdnull,那么该请求将被视为非登录状态,将不会传递到Controller层。

如何跳过校验?

在Controller的方法上,加上@AnonymousSupport注解,在ActionInterceptor.java中,会通过Method方法,获取@AnonymousSupport,如果存在就不进行后面的登录状态校验。

1
2
3
4
5
HandlerMethod handlerMethod = (HandlerMethod) o;
AnonymousSupport annotation = handlerMethod.getMethod().getAnnotation(AnonymousSupport.class);
if (annotation != null) {
return true;
}

如何将userId添加到请求的headers中?

通过token在Redis中获取到userId后,如何在headers中添加userId键值对,略微繁杂,但是目的很单纯。设置值的代码如下所示:

1
2
3
4
5
6
7
MimeHeaders mimeHeaders = (MimeHeaders) headers.get(coyoteRequest);
//in case of do not cover the specify key
String header = mimeHeaders.getHeader(key);
logger.info("Original value of <" + key + "> is " + header);
mimeHeaders.removeHeader(key);
// key = "userId", value即token值
mimeHeaders.addValue(key).setString(value);

在前面,有一个对request类型进行判断的语句,主要目的是为了避免NullPointerException,因为后面通过反射获取属性的时候,可能会由于request类型不同,而获取不到对应的Field,从而导致出现异常。

1
2
3
4
5
6
7
8
9
10
11
if (request instanceof StandardMultipartHttpServletRequest) {
// 文件上传时的类型是这个
StandardMultipartHttpServletRequest standardMultipartHttpServletRequest = (StandardMultipartHttpServletRequest) request;
RequestWrapper requestWrapper = (RequestWrapper) standardMultipartHttpServletRequest
.getRequest();
request = (HttpServletRequest) requestWrapper.getRequest();
} else if (request instanceof RequestWrapper) {
// 通常情况下是这个,因为我们在Filter中对request进行过包装,详情见下面请求日志部分
RequestWrapper requestWrapper = (RequestWrapper) request;
request = (HttpServletRequest) requestWrapper.getRequest();
}

请求日志

主要的任务是将请求所有的参数(如:url中的参数、方法、headers、requestBody等)都以直观的方式打印出来。它的主要流程有两处:

  1. 拦截器TraceCopyFilter初始化RequestWrapper时,将请求所有的信息都保存到HttpTraceLog中,并通过LogTrace保存在当前线程中(利用ThreadLocal)。

    1
    2
    3
    4
    5
    6
    7
    LogTrace.get().setStartTime(System.currentTimeMillis());
    LogTrace.get().setHttpMethod(request.getMethod());
    LogTrace.get().setUrl(requestURI);
    LogTrace.get().setReqParams(request.getQueryString());
    LogTrace.get().setReqHeader(getHeaderMap(request));
    // ...
    LogTrace.get().setRequestBody(sb.toString());
  2. ResponseAdvice包装完返回信息之后,会在finally中将所有的请求信息,通过log的形式打印出来。打印完成后,会通过LogTrace将请求的信息从本线程中移除掉(利用ThreadLocal)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    try {
    LogTrace.get().setSpendTime(System.currentTimeMillis() - LogTrace.get().getStartTime());
    LogTrace.get().setRespParams(objectMapper.writeValueAsString(result));
    log.info("Trace log is ====> " + objectMapper.writeValueAsString(LogTrace.get()));
    } catch (Exception e) {
    log.error("Trace log error : ", e);
    } finally {
    LogTrace.clearAll();
    }

其中获取body时,直接使用IO流,把数据保存到变量requestBody中,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// request == null 是一个标志位
this.request = null;
// IO读取
try (InputStream inputStream = request.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))){
char[] charBuffer = new char[128];
int bytesRead;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
sb.append(charBuffer, 0, bytesRead);
}
} catch (IOException ex) {
ex.printStackTrace();
}
// 保存到内存中
requestBody = sb.toString();

但是收到POST请求时(参考链接),会产生两个TCP数据包(并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次),即浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。所以在拿POST请求中body时,如果直接读出来,可能导致后面框架再去读的时候,出现读不了的情况,所以在RequestWrapper中,重写了getInputStream方法,在request不为空,即没有读取过body(这种情况就是在上传文件的情况),直接以requestBody作为输入流,提供给框架读取,上述问题便解决了。代码如下:

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
@Override
public ServletInputStream getInputStream() throws IOException {
if (this.request != null) {
// 不为空,说明没有读取过body,即为文件上传请求,此时直接返回request.getInputStream()
return request.getInputStream();
}
// 以requestBody作为输入流
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(requestBody.getBytes());
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}

@Override
public boolean isReady() {
return false;
}

@Override
public void setReadListener(ReadListener readListener) {
}

@Override
public int read() throws IOException {
// 以requestBody作为输入流
return byteArrayInputStream.read();
}
};
}

SQL日志

SqlLogHandler在SQL拦截中进行调用,它主要做的工作是把SQL中的?替换成实际的值,并打印出执行时间。

API统一的返回数据

ResponseAdvice中,会将数据都转化成JsonView的形式。

其中有一个问题,就是当返回的类型是String的时候,不能包装String类型,只能以String的形式返回。这是由于整个SpringMVC框架的设计问题。假设有如下业务代码:

1
2
3
4
5
@GetMapping("test")
@AnonymousSupport
public String test() {
return "test";
}

这时候的返回值如下:

因为是以String的类型直接返回了,上述的返回格式也是理所当然。但是如果将String包装成JsonView,然后返回会怎么样?修改ResponseAdvice如下:

1
2
3
4
5
6
7
8
9
if (o instanceof JsonView ) {
result = o;
return result;
}
if (o == null) {
o = EMPTY_DATA;
}
result = new JsonView<>(SysStubInfo.DEFAULT_SUCCESS, o);
return result;

这时候如果再次访问,程序会报错如下:

报错的堆栈信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java.lang.ClassCastException: com.haylion.realTimeBus.facade.view.JsonView cannot be cast to java.base/java.lang.String
at org.springframework.http.converter.StringHttpMessageConverter.getContentLength(StringHttpMessageConverter.java:43)
at org.springframework.http.converter.AbstractHttpMessageConverter.addDefaultHeaders(AbstractHttpMessageConverter.java:259)
at org.springframework.http.converter.AbstractHttpMessageConverter.write(AbstractHttpMessageConverter.java:210)
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:275)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:180)
at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:82)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:119)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:877)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:783)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:991)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:866)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:635)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at com.haylion.realTimeBus.facade.filter.TraceCopyFilter.doFilter(TraceCopyFilter.java:52)

上面的错误简单理解,类型转换错误,也就是需要一个String,但是却收到了一个JsonView。需要的String是我们在Controller中返回的类型,然而实际收到的JsonView是在ResponseAdvice中包装后返回的。为什么这样的原因是:与ResponseAdvice执行的时机有关。在AbstractMessageConverterMethodProcessor.java文件的writeWithMessageConverters()方法中,调试数据如下:

确定返回类型:

确定可用的转换器,然后执行ResponseAdvice

ResponseAdvice执行前,SpringMVC会根据Controller的返回类型,确定一个AbstractHttpMessageConverter,由于在Controller中返回类型为String,所以这里为StringHttpMessageConverter,也就是说,它是用来转换一个String类型的转换器。等转换器确定好了之后,会执行ResponseAdvice中的处理方法,将String转换成JsonView

写入返回数据:

忽略掉其他代码,直接进入出现错误的代码,在AbstractHttpMessageConverter中的addDefaultHeaders()方法中,需要在头部获取整个请求的大小,即调用getContentLength()方法。

它是一个期望被子类覆盖的方法,默认的实现如下:

1
2
3
protected Long getContentLength(T t, @Nullable MediaType contentType) throws IOException {
return null;
}

这时候应该直接看StringHttpMessageConverter中的getContentLength()方法如下:

1
2
3
4
5
@Override
protected Long getContentLength(String str, @Nullable MediaType contentType) {
Charset charset = getContentTypeCharset(contentType);
return (long) str.getBytes(charset).length;
}

然后再将转换后的JsonView作为抽象函数getContentLength()(这时就是StringHttpMessageConverter的该函数)的第一个参数,如下:

1
2
3
4
protected Long getContentLength(String str, @Nullable MediaType contentType) {
Charset charset = getContentTypeCharset(contentType);
return (long) str.getBytes(charset).length;
}

第一个参数为String,但是实际上是JsonView。因此,ClassCastException在所难免。在ResponseAdvice中,将String直接返回,可以避免出现这种不太好修复的错误。

替代办法:

但是如果非要返回String类型,并且需要包装成JsonView形式,可以考虑直接在Controller中将String包装成JsonView,然后返回,如下:

1
2
3
4
5
@GetMapping("test")
@AnonymousSupport
public Object test() {
return new JsonView<>(SysStubInfo.DEFAULT_SUCCESS, "test");
}

结果:

异常处理

参看ExceptionHandle具体实现及写法、以及相关源码注释。

分页–PageHelper

用法

直接在方法上加上@PageAble注解,并在该方法中传入两个参数,分别为pagesize,在该方法返回后,会得到一个ResultPageView封装对象,其中包含分页相关信息。

工作流程

SystemAdvice定义一个切面,切点是@annotation(com.haylion.realTimeBus.annotation.PageAble)。也就是说,每个被@PageAble注解过的方法,都将执行下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
} finally {
PageHelper.clearPage();
}
}

准备工作:主要是获取pagesize的值,然后调用PageHelperstartPage方法,初始化分页信息。

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
// 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),其实现为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @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则从本线程中获取,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 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如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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}

    获取完总数后,会进行判断是否有分页的必要。

  • 分页查询。这里假设有分页的必要。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    //调用方言获取分页 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的分页实现如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //
    @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的分页实现如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @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();
    }
  • 保存分页查询后的结果。

    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
    // 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。

    1
    2
    3
    public class Page<E> extends ArrayList<E> implements Closeable {
    // ...
    }

    在return后,还会执行finally中的处理代码,即com.haylion.realTimeBus.interceptor.sql.MyPageHelperafterAll()方法。其中实现如下:

    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
    // 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

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}

至此,还有最重要的一个步骤,是在切面处理完成后,将分页信息从本线程中删除,没有此操作,后续操作会出现莫名其妙的错误。也就是finally语句中的PageHelper.clearPage();

1
2
3
4
5
6
7
8
9
10
11
try {
prepare(proceedingJoinPoint);
Object obj = proceedingJoinPoint.proceed();
Object result = after(obj);
return result;
} catch (Throwable throwable) {
logger.error("aspect execute error : ", throwable);
throw throwable;
} finally {
PageHelper.clearPage();
}

局限

这就限定了在一个被PageAble注解了的方法上,只能执行一条查询。如果对于一个到来的请求,需要进行两次或以上的查询,并且某一条查询需要分页的情况,如果所有的查询都放在被PageAble注解的方法下,执行会出现问题(出现不必要的分页操作)。但是可以通过组装的形式,完成该项需求。