0x00 概述
这两天在整理框架,发现自己手里大约有小十套不同的后端框架了,有mvc的,有cloud的,有netty的,有vertx的,还有手写的servlet的,虽然久远了点,但是试了下还能用...现在spring是主流,顺手就整理了一波相关的,升级下军火库,捎带着记录下一些感觉有意思的封装,其实都大同小异。
前后端分离是目前的比较基础的架构需求,分离后,主要是充分利用HTTP的无状态,同时结合幂等,TOKEN等,可轻松实现扩展性,移植性,复用性,动态平衡等等,进一步可实现函数化服务,服务网格,serverless等,从领域角度,更低的耦合,更高的内聚,更丰富的组合。
0x01 结构
HTTP实现无状态的两个主体,即request和response,在进行前后端分离框架设计的时候,不可避免的需要对其进行封装,一个作用是框,包括req和resp的结构,body的结构,状态码的含义规范等,一个作用是架,包括顺序的执行流程,发生异常错误时的处理,过程信息日志的记录等。
框的部分,最终成果物会是一系列规范,目前实践情况最多的就是rest接口规范的各种妥协后的变种,这一块各家有各家的习惯,主要几个点,动词的使用,URI结构,JSON的使用,分页等状态参数的使用,我手里有一套我习惯的,后续整理下再分享。
架的部分,最终成果物会是一堆代码,这一块就更是五花八门了,不可多说,就只说一下,我个人基于Spring mvc,对request/response做的一些简单的封装,用来满足前后端分离,自定义扩展,结构规范,日志记录等几个简单的点。
先上个简单的图示
先大致说明下,我选了一些奇奇怪怪的颜色大致区分了下,
- 最少的那两个,用了不好描述的颜色的,是框架本身,这里即SpringMVC
- 三个的绿色的,是主要操作对象,request,response
- 另外三个不是那么鲜艳的紫红色的,是利用的Spring特性,Filter,Aspect,Handler还有个这里没出现的Interceptor,搞架构的兄弟姐妹最好能区分清楚,有疑惑的可以发红包问我,我可以给你仔细讲讲。
- 其他的天蓝,海蓝,梦蓝的,就是所有的需要的封装逻辑类
- 这是一套很基础的实现,具体情况,可以根据框架和业务需求进行变化,但这条逻辑主线实际都差不离。
0x02 Request 封装
最左边是关于request的拦截并封装,主要需要实现的有两个:
- 增加额外的属性字段
- 解决BODY部分的多次读取问题
使用Filter拦截
通过filter拦截原始Request,然后进行相关的操作,保证真正进入到主体程序里面的request是符合要求的
增加额外字段
拦截的同时,根据需要设置额外的request attributes,常见的:
- 唯一的请求标识ID,用于追踪
- 请求进入的时间,用于计算耗时
- 请求地址URI和http方法,用于后续统计和限流
- .......
不要增加业务相关的,不要增加业务相关的,不要增加业务相关的,要不然你会发现,越加越多
业务相关的字段或者判断,请使用interceptor,交给spring来管理,多种业务的可以分开创建,常见的业务如,身份验证,权限验证等等,后续如果有时间写权限的话,再细说。
HttpServletRequest httpRequest = (HttpServletRequest) (request);
String requestId = IdHelper.getId24bit();
MDC.put(ApiConstants.REQUEST_ID, requestId);
request.setAttribute(ApiConstants.REQUEST_ID, requestId);
request.setAttribute(ApiConstants.REQUEST_START_TIME, System.nanoTime());
String requestUri = urlPathHelper.getOriginatingRequestUri(httpRequest);
request.setAttribute(ApiConstants.REQUEST_URL, requestUri);
String method = httpRequest.getMethod();
request.setAttribute(ApiConstants.REQUEST_METHOD, method);
此处使用了slf4j的MDC,用于日志系统追踪唯一的ID
提供BODY的多次读取
这个主要是因为本身Servlet Request设计的时候,body部分通过流形式读取,只能够读取一次。
一般情况下,这一次的读取是发生在controller部分,读取到某个特定的类,然后进行判断,转化为配置好的对象使用,这里原始的设计又能引申到一个约定和配置的关系问题。暂时就说这样设计的结果,会导致所有需要使用请求bod体y的逻辑,都要发生在controller读取之后,而controller往往又是业务开始的地方,这样很容易就会造成业务耦合。
最常见的就是日志需求,系统希望能够记录每个请求的具体内容并打印到日志中,最直接的做法,是在每个controller里面的mapping方法,增加一行输出,明显的不好。
这里通过Filter实现的思路是这样的,主要利用servlet的request wrapper,改写读取body流的两个方法,引入一个临时的字节数组,第一次读取后,将body内容存在数组中,后续的读取,都是通过这个数组。
/** Body Buffer */
private final byte[] requestBodyBuffer;
public RequestWrapper(HttpServletRequest request) {
super(request);
this.requestBodyBuffer = RequestUtil.getByteBody(request);
}
/**
* getReader
*
* <p>Override getReader, point to getInputStream
*
* @author Created by ivan at 上午11:28 2020/1/13.
* @return java.io.BufferedReader
*/
@Override
public BufferedReader getReader() {
ServletInputStream inputStream = getInputStream();
return Objects.isNull(inputStream)
? null
: new BufferedReader(new InputStreamReader(inputStream));
}
/**
* getInputStream
*
* <p>Override getInputStream, get request body from buffer
*
* @author Created by ivan at 上午11:28 2020/1/13.
* @return javax.servlet.ServletInputStream
*/
@Override
public ServletInputStream getInputStream() {
if (ObjectUtils.isEmpty(requestBodyBuffer)) {
return null;
}
final ByteArrayInputStream bais = new ByteArrayInputStream(requestBodyBuffer);
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() {
return bais.read();
}
};
}
0x03 待续
这样对Request做了简单的封装,保证一些基本的属性,每个request都具备,主要用于服务后续的系统使用,例如权限拦截器,日志系统等。
接下来,request会通过拦截器等,最终进入到mapping的controller,然后进行相关的逻辑处理,最后Response回去。
这里主要是返回,牵扯到正常的返回,异常的返回,返回内容的结构,可读性等问题,同时response,代表了一次请求周期的完成,这时候还应有一些基本的日志的输出,或者满足一些service matrix,监控等需求的输出实现。
此部分,待续...
本文由 Ivan Dong 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Jul 7, 2023 at 04:51 am