项目中的基础架构文档注释解读

项目的基础架构涉及的东西并不多,涉及到的技术并不复杂。写了一篇架构文档,给项目当做项目的架构文档。自己之前改过里面的一些bug,相对来说还是比较熟悉。

模块概览

base-service

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

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

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,如果存在就不进行后面的登录状态校验。

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

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

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

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,从而导致出现异常。

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

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

    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中,代码如下:

// 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作为输入流,提供给框架读取,上述问题便解决了。代码如下:

@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框架的设计问题。假设有如下业务代码:

@GetMapping("test")
@AnonymousSupport
public String test() {
  	return "test";
}

这时候的返回值如下:

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

if (o instanceof JsonView ) {
    result = o;
    return result;
}
if (o == null) {
    o = EMPTY_DATA;
}
result = new JsonView<>(SysStubInfo.DEFAULT_SUCCESS, o);
return result;

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

报错的堆栈信息如下:

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()方法。

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

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

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

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

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

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,然后返回,如下:

@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注解过的方法,都将执行下面的代码:

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方法,初始化分页信息。

// 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.MyPageHelperafterAll()方法。其中实现如下:

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

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

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注解的方法下,执行会出现问题(出现不必要的分页操作)。但是可以通过组装的形式,完成该项需求。

发布了166 篇原创文章 · 获赞 118 · 访问量 26万+

猜你喜欢

转载自blog.csdn.net/asahinokawa/article/details/92625315