一、概述
场景示例
图1.1 数据系统架构设计
系统主要分为4部分:
- 网站:提供给用户交互界面
- 业务系统:用户服务、报表服务、财务服务,用于业务处理和查询
- 数据服务:大数据定时任务加工处理平台,执行结果回写入数据库
- 数据库:提供数据存储、检索,业务系统和数据系统的存储区
系统主要角色分为3个:
- 用户:系统使用者。不了解业务系统、数据服务
- 数据开发:操作数据平台的开发人员。不了解业务系统、只熟悉SQL、HIVE
- 业务开发:编写代码开发各业务逻辑
问题分析
这种设计产生了一个问题,当服务正式上线后,随着时间推移改变,系统必然会出现数据BUG,定位的方式一般是:
访问前端——>获得接口——>后端定位SQL——>数据定位SQL表对应的加工逻辑
因为数据开发不懂如何定位后端服务中的SQL,就必须后端开发人员介入,但后端开发一般并不需要调整,这会导致数据开发的需求无法及时响应和后端开发的碎片化。
解决思考
所以我们需要将问题SQL定位调整为:
前端——>查看请求——>获得执行SQL
我们可以通过前端访问接口的形式,获得后端执行的SQL,同时要考虑安全性,不同环境安全策略,以及可快速配置集成。
二、概要设计
命名
先给准备一个名字,嗯...一个好名字,能让人知道这可能是干什么,或者自己喜欢的个性名字,你才能更主动的构建和维护它,让我们拥有一个匠人的心。这个框架主要用来返回接口SQL,同时也算做一种日志采集,所以应该有单词log,然后让后端人员一直重复这无聊的工作很繁重,就叫slog好了,希望它能减少这项重复繁重的工作。
技术设计
图1.2 技术设计
我们首先思考如何去做,它需要在Http响应中获得SQL,所以需要Servlet Filter;关系到数据的读取和写入,需要用到字节流;需要在执行Mybatis获得SQL,需要使用Mybatis Filter;每个用户请求返回的SQL都可能不一样,所以跟线程存储相关,需要用到ThreadLocal;SpringBoot环境需要自动集成,需要Springboot stater。我们将这些串联起来,就是图1.2的技术设计图,流程如下:
- 通过@Bean集成至SpringBoot
- 用户请求Http接口
- 检查请求头判断触发条件(前置过滤)
- 符合条件,包装重写输出流(前置过滤)
- 初始化ThreadLocal(前置过滤)
- 执行SQL拦截器,记录至ThreadLocal(Mybatis拦截器)
- 将SQL写入Body体重,清除ThreadLocal(后置过滤)
项目结构设计
Springboot官方starter以spring-boot-starter-xxx的方式命名。官方建议自定义的starter使用xxx-spring-boot-starter命名规则,用来区分springboot生态提供的starter。我们定义如下的项目结构:
slog-spring-boot
——slog-spring-boot-starter
——slog-spring-boot-autoconfigure
——slog
对各部分作出说明:
- slog-spring-boot:父类项目,用于统一依赖版本
- slog-spring-boot-starter:定义springboot依赖,其他引用该jar可以自动集成
- slog-spring-boot-autoconfigure:通过@Bean、@ConfigurationProperties自动加载springboot配置类、配置文件
- slog:核心逻辑处理类,这里面不包含springboot相关引用和代码。可以看做是一个不依赖springboot环境也可以运行的类库,如果在非springboot环境,我们可以引用它来手动集成slog。
合理的项目工程化构建有利于后期的维护。比如slog是处理SQL的,slog-mybatis是可以直接作用于mybatis的,slog-hibernate是可以直接作用于hibernate的,因为使用方技术不是固定的,他们可以根据业务技术选型不同选择合适的依赖,同时编写mybatis处理时,不需要考虑hibernate的兼容问题。
maven命名
<groupId>com.v2hoping</groupId>
<artifactId>slog-spring-boot</artifactId>
<version>1.0.0-SNAPSHOT</version>
三、关键技术点
Servlet Filter
这里我们思考一下为什么使用Servlet的过滤器,而不使用SpringMvc拦截器?
因为第一我们需求对最终的结果进行修改,所以要获得输出流;第二点Servlet作为Springmvc的内核,如果不使用Springmvc也仍然可用,更通用。
我们实现javax.servlet.Filter接口的doFilter接口,调用下面责任链连执行逻辑,在该语句前为前置过滤,后则为后置过滤,最大区别是后置过滤时Reponse流中会保存结果。
chain.doFilter(request, filterResponseWrapper);
HttpServletRespnse输出流
这里我们思考一个问题,在执行完成chain.doFilter后,springmvc会将方法执行的结果自动写入输出流,我们该如何在拦截器中修改呢?
所以我们需要用到HttpServletResponseWrapper包装类,在调用chain.doFilter之前将原有的输出流包装成我们自定义的HttpServletResponseWrapper类,它的主要作用是在内部保存一个ByteArrayOutputStream,实际上是一个byte[],然后springmvc写入的数据会被暂存在该对象,之后我们可以使用自定义的getResponseData获得字符串。
mybatis Filter
图1.3 mybatis执行流程
如图1.3所示,执行流程如下:
- Executor:拦截执行器的方法。
- ParameterHandler:拦截参数的处理。
- ResultHandler:拦截结果集的处理。
- StatementHandler:拦截Sql语法构建的处理。
我们可以对各个节点的各个方法进行拦截,例如以下注解就是对StatementHandler的查询、修改、批量操作进行拦截
@Intercepts({@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
@Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
@Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})})
因为我们要获得SQL、参数,所以选择StatementHandler对象拦截,从流程图中可以看出该对象用于构建SQL。
ThreadLocal
ThreadLocal是JDK提供的本地线程变量,每个线程访问这个变量都是独占数据,互不影响。主要使用以下3个方法
private static final ThreadLocal<SlogContext> CONTEXT_THREAD_LOCAL = new ThreadLocal<>();
SlogContext slogContext = new SlogContext();
CONTEXT_THREAD_LOCAL.set(slogContext);//初始化线程变量
CONTEXT_THREAD_LOCAL.get();//获得线程变量
CONTEXT_THREAD_LOCAL.remove();//删除线程变量,防止内存溢出
四、结语
至此概要设计和技术要点就介绍完毕了,主要是给大家介绍如何遇到重复繁琐的工作时,通过技术优化工作,我觉得作为开发人员要具有工具化思维,如果一个东西重复用到3次以上,就应该考虑工具化,可能别人也会需要。如果一段代码重复修改3次以上,就应该考虑使用设计模式、代码重构优化它,让该段代码达到最小修改和复用。最后附上源码供大家参考。
github地址源码:https://github.com/v2hoping/slog-spring-boot