通用查询项目总结


通用查询项目总结

1. 初接项目

在2020-06-01确定下通用查询的项目,通用查询,主要是查询整个系统中的一些数据库表,并在界面中展示出来。通用查询的业务是非常简单的,查询都是一些简单表,关联表也不会太多。

通用查询的特点在于,其界面查询条件、查询结果,不是固定开发,而是通过一个xml定义界面的展示,定义查询的SQL,然后查询数据并返回。在传统界面开发中,一个简单的界面,也需要一定的工作量,特别是需要查询的数据比较多,需要做很多的重复开发。所以,使用xml定义界面的一切,然后程序解析xml,动态画出界面。这就是通用查询的原理,基于这个模式,开发一个通用查询界面,将时间缩短至半天,界面拥有15个以上各式各样的查询条件,查询结果可定义,查询逻辑,可以直接复用plsql的sql脚本。整个开发是非常快捷的。

OK,在回到项目,这个项目其实不算一个新的项目,通用查询功能在旧系统中就已经实现了,使用的是java swing实现界面处理,jdbc处理sql查询,整个架构基于spring MVC。

所以,我要做的事情,就是将通用查询从旧系统复制到新系统。

嗯,咋一看,没什么难度嘛。所以立项的时候,什么都没有,只有一句话,通用查询从老系统迁移到新系统。什么需求文档,界面原型,统统没有。

当时大家都没有细想,觉得很简单,大概就是做一个远程调用就行了,然后给的时间大概是3天左右开发。

接手这个项目,又不能立即开发,也就没有多想,领导怎么分配,我就安心怎么干活就完了。

就这样,这个项目接了下来。可没有想到,这个完全就是一个大坑。

2. 着手项目

接下项目不久,就慢慢开始投入,当然,不是100%投入,当时前一个项目还在收尾阶段,在联调测试阶段,所以时不时就需要做联调,看数据,做调整。投入率一度甚至低于30%。

2.1 第一版

刚开始这个项目,我也觉得挺简单的,远程调用么,写个controller,做个适配,然后将数据传输给界面,剩下的就没有我的事情了。

我就很天真的,直接搞了远程调用。

当时花了2个周左右,将远程调用的接口调试完毕。主要是旧系统代码打包,打包的时候需要抽离代码,然后将旧系统中的数据结构集成到新系统中,最后使用postMan验证。

OK,第一个版本完成,还没等我提交项目,突然传来噩耗,新系统与老系统尽可能解耦,不能直接远程调用。

这不完蛋了吗,F***.

2.2 第二版

前面的工作前功尽弃,但是无奈,这两个周,30%的投入率,领导也没说啥,继续以30~40%的投入,继续开发吧。

既然不能远程调用,那么将老系统的逻辑全部拿过来不就行了吗?

于是在原来的基础上,第一版已经将数据结构打包在新系统中使用了,接下来将老系统的逻辑该打包的打包,该调用的调用。

又花了2个周吧,其实也就三到四天,完成了第二版,整个没有远程调用,但是大量的使用老系统的逻辑,同时与界面完成数据定义。

这样,好歹第二版完成了,找领导验收。

性能差的要命,每次界面数据查询需要3秒,然后查询数据更慢,即使查询结果为空,也需要大概10来秒。

领导认为这个性能太差,而且后调了一个迭代周期,让我跟着下一个迭代一起发布。下一个迭代需要的时间大概是2个半月,舒服,我又多了2个多月,这应该能很快完成了吧。

2.3 第三版

无奈,投入比例有所上升,但是还是在50~60%左右,而且还需要时不时的做一些项目外,迭代内的活。和项目负责人一起讨论后,认为优化的地方在于两个:1.使用redis缓存;2.摒弃旧系统中的一些操作。

基于此,花费大概3天时间,过了遍旧系统的代码(别问我为啥,旧系统啥文档都没有),重新整理了旧系统的设计。整理了旧系统通用查询的数据流图,数据结构er,调用时序图,调用流程图等。

理清旧系统的设计后,开始设计基于redis缓存的通用查询。将xml数据分成界面、SQL、结果部分,每个部分有自己的特点,使用不同的redis结构存储。花费2天看了下redis的5种数据结构,然后根据特点,每部分的数据使用不同的结构存储。

设计完成后,找到总负责人,审核设计,审核通过。

那就开始开发吧,可是不懂redis怎么办?

没啥说的,和玩游戏刷副本一样。

利用下班时间,每天2.5小时,用了22天,系统的过了一遍Redis(这个其实从刚接受这个项目就开始了,所以,在开发第一版和第二版的过程中 ,我自己已经将redis完全的过了一遍)。

开发redis服务,开发本地服务如何与redis服务配合,实现功能,拆分旧系统,摒弃旧系统中的代码等等。

接着就是与界面人员一起联调,定数据结构,联调redis服务器,等等。

本地缓存的使用、本地缓存的实现等等。

就这样又过去了1个月(60%投入)

2.4 第四版

第三版完成,界面的查询性能有了数量级的提升。

同样的环境下,旧系统数据查询在5~10秒

同样的环境下,新系统数据查询,第一次在1.5~2秒之间;后续查询时间在20毫秒至100毫秒之间。(因为有本地缓存)

查询性能平均提升了100倍。当然,使用缓存,也引入新的缺点:内存占用过大(本地缓存),系统更加复杂(redis),微服务启动更慢(redis数据预热)等等。

除此之外,还有一些隐含的,在第三版中完全没有考虑,比如:日志、异常等。

所以,第四版的目的主要是解决redis数据预热耗时过长、缺失日志和统一的异常这三个问题。

这次到是挺快的,大概过了2个周(中途还夹杂了两个其他的任务,实际就是7天左右),完成了第四版。

截止目前,终于有一个界面可以看了,能够实际操作了。

2.5 复杂条件开发

前面主要是打通整个项目,将调用链打通,日志,异常等开发完成。

但是还有一些复杂的条件,甚至单独靠一个接口都无法完成的查询条件(一般都是弹出框),这些复杂的条件还未完成呢,这些条件大概有十多个,每一个大概需要半天左右的时间进行设计,开发,自测。有些复杂的是1整天。

这样,又过去了2个周,终于开发完毕。(可怜的是还未联调。。)

2.6 总结

整个项目从2020-06-01开始,到2020-09-17结束。

前1个月,投入比例为30%左右,折合7个工作日。

中途2个月,投入比例为60%左右,折合26个工作日。

最后半个月,投入比例100%,折合12个工作日。

总计45个工作日,2个月左右。完成了新旧系统的一个完整的模块的迁移,迁移中间使用了很多的新技术(相对旧系统)。

从设计、开发、联调等都是独立开发(期间项目总负责人和我一起审核设计,给出了几个非常棒的优化方案)。

中间也是各种各样的坑。

这里附一下我为什么做了这么多版本:

这里采用的是敏捷开发的设计思想,每次做出的一个版本,都是在一定程度上可用的。我需要随时止损,比如说,我可能完全没有能力将通用查询从旧系统迁移到新系统,那么第一个版本就不会实现,而第一个版本只花费了4~5天。

按照最坏的结果,我浪费了5天时间,也就是一周,发现我可能没有能力完成这个迁移过程,那么我可以及时止损,一方面找人给我做个紧急培训,或者换个人继续。否则,我可能要到第30多个工作日,才会发现,哦,我完全没有能力实现这个项目,那这样就浪费的时间太多了。

敏捷开发的优缺点,有很多,也是目前流行的项目管理方式,所以,我也是在敏捷开发的思想指导下,一次次迭代,一次次优化完成的。

如果一次性完成,可能会比迭代开发节省时间,但是,一次性开发的风险比较大,为了安全,就没有使用一次到位的方式,而是一次次来,一点点的实现。

3. 如何快速有效的分析现有代码逻辑

面对旧系统中,好几十个类,相互调用,各种嵌套,想要理清旧系统中通用查询的逻辑,是整个通用查询迁移项目的基础。所以,在接手项目后,第一时间就开始梳理旧系统中通用查询的逻辑。但是,面对这好几十个错综复杂的类,又该如何入手呢?

3.1 数据模型

数据模型,一定是通用查询旧系统中最核心的内容。数据模型在一定程度上体现了旧系统中通用查询的设计。

基于这个原因,我第一步就是梳理旧系统中通用查询的数据模型。梳理数据模型说简单也很简单,就是找到旧系统中,和通用查询相关的一个实体,然后以这个实体为圆心,不断的查看这个实体的父类和子类。如果这一条线断了,需要换一个圆心继续,然后使用UML语言,描述出整个关系:

image-20200917091209240

比如这个就是我梳理的数据模型。可以很明显的看出,最核心的就是中央的蓝色的类。

说道这里,说一个小技巧,画出的UML实体关系,一眼很难看出这是个啥,或者说,我们对颜色更加敏感,对于图形的小小的差异,反而不是非常的敏感。

所以,在这个数据模型中,绿色就是接口,蓝色就是抽象类,黄色是普通类,粉色是枚举类。

这样是不是非常的清晰,一眼望去,这个数据模型中,有几个接口,分别处于什么位置,通过颜色就很清楚的。

还有一个问题,我可能一开始选择的不是最中央的类,那么,我怎么确定我选择的入口的类是正确的呢?

别想那么多,只要你选择的类和你需要了解的内容有关系,那么就一层一层的跟下去,最终,核心类会自动浮出水面的。

善用颜色,善用图形描述复杂的关系。

3.2 动态模型

通过查看通用查询的数据模型,我们了解到通用查询的静态数据是怎么样的,核心是什么,重点是什么。

那么,通用查询是怎么动起来的呢?

这就需要查看通用查询的动态模型了,查看动态模型,最快的手段应该时序图(个人观点),查看时序图,是一个不错的选择。

有两种方式借助时序图了解动态模型:1.手绘时序图;2.查看时序图

看到这里,可能有人问了,前辈什么都没有留下,哪来的时序图可以看?

你遇到的和我一模一样,我也是只有代码,没有任何文档。

那么就只剩下一条路了,手绘时序图。这是一个相当大的工作量。如果你没有很多的时间,那么这一条路,也走不下去了。

怎么办呢?有办法,借助工具。

在idea中安装一个插件

sequencediagram

使用这个插件,就可以生成时序图了

选择方法

image-20200917093630396

选择粒度

image-20200917093647076

等待生成时序图,生成的时序图里面有很多是jdk的方法,这个时候,就可以右键,remove掉这个节点。

image-20200917093611855

生成的时序图,就是这个方法完整的时序图了,而且,在idea中,双击一个小过程,代码编辑界面会直接跳转到对应的方法。

有了这个神器,可以让工作效率事半功倍。

3.3 软件工程

说实话,一般情况下,了解了一个模块的静态模型和动态模型,这个模块也就基本上没有什么秘密可言了。

根据软件工程的理论指导,我们有更多的手段可以帮助我们快速的分析现有逻辑。

比如:数据流图可以让我们非常的清晰,在系统中,数据的转变过程。

流程图则可以帮助我们理解某一小块的算法,或者一些比较难理解的核心大方法。

大学时,学习软件工程,总是觉得软件工程这门课程,有些泛泛而谈,当时觉得,了解软件工程这些比较空泛的理论,还不如去刷几个算法呢。

但是真没有想到,这些理论知识,随时随地在指导着我,在工作中能发挥这么大的作用。

说实话,有些后悔当初没有好好学习这门课程,学了个半吊子,只会一些基本的,只记住了一些考试的基本内容。

4. 快速的,系统的了解一个框架

通用查询从旧系统迁移到新系统中,增加了redis缓存的使用。

在接受通用查询项目之前,我完全没有了解过redis相关的知识。

而现在,竟然要我设计redis缓存,你是在为难我吗?

我当时也很头大,redis是个什么玩意,怎么设计,如何入手?

为了能够设计redis缓存,我首先需要了解redis的知识。

redis的知识,说少也不少。

那么,怎么快速的了解呢?

我依然是采用软件工程的两板斧:静态模型和动态模型。

首先我花费了8天时间,了解了redis的数据结构。这是redis的静态模型。

Redis–入门

Redis–数据结构–String

Redis–数据结构–List

Redis–数据结构–Hash

Redis–数据结构–Set

Redis–数据结构–Sorted Set

Redis–数据结构–命令汇总

Redis–数据结构–内部结构

接下来就是了解redis的动态模型了:

Redis 发布/订阅

Redis事务

Redis 锁

Redis 内存回收策略

Redis持久化

redis 安全security

redis的哨兵模式

Redis集群命令解析与应用

缓存穿透与布隆过滤器

redis–在spring中使用

微服务中使用redis

Redis–微服务中的高级用法

微服务使用缓存注解

经过这22篇文章的总结不敢说完全了解redis,但也至少是系统的了解了redis相关的知识,能够完成一些设计和应用。

5. xml的资源的读取

这个是在做第三个版本的时候遇到的。

在idea上开发完成,直接就在本地启动,自测,然后一点问题都没有,于是我就很自信的发布了,程序被打成jar包发布到服务器上,启动后,异常了。

读取资源失败。

我当时就很纳闷,本地好好的,怎么到了服务器就异常呢?

因为idea没搞远程调试,所以,只能打日志了。

最终发现,因为本地是文件系统启动,所以使用ClassPathResource读取xml文件,一点问题都没有。

但是在服务器上,xml被打入了jar包,就不能使用ClassPathResource读取xml文件了,而是需要将xml看成一个jar包,然后读取xml文件。

分析到这里,就变成了,如何从jar包中读取资源了。

需要使用Jar相关的类了:

image-20200917105540282

6. 业务逻辑在代码中保持清晰

通用查询从旧系统中迁移到新系统中,因为使用了redis,所以需要做数据预热。

数据预热,需要根据前面的redis数据设计,来进行不同的操作。整个大概分为七个到八个比较大的模块。

每一个模块里面,又有很多的逻辑。

有人肯定会想到,对于这些步骤,抽取方法,这样一个步骤就是一个方法,一个步骤内的操作集中在一起,这样就很清晰了。

没错,这是一个非常好,也是非常有效的方式。

但是随着时间的推移,就容易出现忘记这个方法,或者说这个步骤是干什么的了,怎么办?

加注释,加上注释,不管过去了多长时间,也不会忘记。

但是这样还有一个问题,加了注释,根据注释,大概能知道这个步骤是干什么的,但是细节呢?

加详细的注释。总之,在写代码,加注释的时候,尽可能将自己认为重要的信息写上去。

加了注释,这些步骤可能逻辑性还不是很清晰。

注释加序号。

1234…

几乎所有人都知道,1后面是2,2后面是3.。。。

连续的数字与逻辑关联,我认为没有比连续的数字更能体现逻辑性了。

就类似一本书籍,加上不同的章节,大章节,中章节以及小章节等等,整本书的逻辑性就出来了。没人想看一团好几万字的文字。

当然,这些是我个人总结出的,比较好的方式,你可能不一定认同。

image-20200917110527883

image-20200917110540234

这样逻辑性就非常的强,而且我还可以划分模块,将类似的操作放在一起。

7. 规范化,在规范化

有句话说的好,程序员讨厌别人不写文档,不写注释;程序员更讨厌自己写文档,写注释。

当然,这只是一句调侃的话,至少我认识的程序员,大多还是很乐意写文档,写注释的。有了这些东西,自己后面再看的时候,不会蒙圈,其他人在接手的时候,也不会难受。

所以,能写注释,一定要写注释。

image-20200917111100843

写注释只是一个规范,还有比如一个方法不宜过长,最好一页能够展示完毕。

image-20200917111157965

清晰的缩进:

image-20200917111228028

这些都是一个个非常微小的点,甚至都可以不做,程序也能正确的运行。

但是好的代码风格,不正是一点点的小的地方积累而成的吗。

我不敢说自己写的代码有多好,但是我可以说,在我力所能及的地方,我做到了最好。

或许我写的代码很烂,但是,我尽自己所能,做到了最好。

8. 多线程

数据预热在微服务启动的时候做,但是数据预热,涉及到文件的读取,数据的组装,以及数据的传输等等,整个数据越热是很花时间的,特别是随着时间的流逝,业务的增加,需要处理的xml文件也是增加的。所以,数据预热的时间也会慢慢增加。数据预热会影响到微服务的启动,进而影响到服务的可用性。

所以,数据预热的时间必须压缩,需要控制到一定的时间内。而且这个时间不能随着时间的流逝而显著的增加。

使用多线程就能很好的解决这个问题。

主线程读取资源,然后判断哪些资源应该参与数据预热(需要考虑分布式),然后启动子线程去完成数据的组装,计算,传输等等耗时的过程。

但是启动多个线程,也是很耗费时间的,因此使用线程池,提交不同的任务。这样主线程可以在很短的时间内完成数据预热的线程数据准备和线程池启动。

然后服务就启动了,剩下交给线程池线程慢慢处理就行了。

image-20200917112406527

9. 统一处理

这部分包含了统一日志和统一异常。

9.1 日志统一格式

统一日志不用说,关键操作开始和结束都应该打印日志,而不那么重要的操作的开始或结束,应该打印低级别的日志。

所以,在上述代码中,日志打印随处可见。

既然做了统一的日志处理,不妨一次到位,很多时候,自己打印的日志,几乎只有自己能很快的查看,换个人来看,就稍微有点繁琐,也不是不能看,而是有些繁琐,需要猜测你这一句日志是什么意思。

为什么会出现这样的问题呢?

因为他打印的日志是一些简单的单词拼接起来的。先不说单词使用准确不准确,这堆单词没有一些语法等等,所以,看起来就比较麻烦。

既然我们是在spring中开发,ApplicationContext实现了MessageSource接口。为什么不把日志也国际化了呢?

所以,我在代码中的日志,也是一堆单词,但是我做了国际化,这样日志不管是谁看,都很容易了。

image-20200917113027554

说完日志,接下来说说异常。

9.2 异常统一定义

很少有程序员主动处理自己的代码中的异常。

怎么说呢,我个人感觉是对异常避而远之,只要我写的代码不出现异常,就完事大吉了。

只要我不捕获,我的代码就不会出现异常这个意思。

代码出现异常是很正常的事情,甚至java将异常分为两类:可检查异常,运行时异常。

对于运行时异常,java都没有啥好的办法,在运行时异常出现时,只能是尽可能的保持服务可用。

所以我们也没啥办法。

我想说的是,我们针对业务中,可能出现的检查异常,这些检查异常一般都是调用jdk方法而声明有可能抛出,我们在调用的时候,不得不捕获,或者继续向上抛出。

继续向上抛出也是一个非常好的方式,但是,在抛出前,如果能做转换,则一定要做转换,因为距离异常抛出点越近,异常的原因越清晰。

所以,对于可检查异常,我们最好能够定义自己的异常,在向上抛出前,尽可能做转换:

image-20200917113740440

这里抛出的文档读取异常,在我们的业务中来说,就是xml转Document异常。所以,我们将DocumentException转换为CommonQueryXmlParseException.

当然,我们自定义的exception中,还有异常信息的输出相关的处理。

image-20200917113949712

在这里就用到了内部持有的数据:

image-20200917114121824

image-20200917114137697

帮助我们打印更加向详细的信息。

10. 高效的工具使工作更高效

在这部分,我主要说的是两个方面:1.自己的工作效率;2.与别人协作的效率。

10.1 自己的工作效率

一个完整的系统,不是一个微服务实现。

一个完整的系统,往往有多个微服务,而这些微服务之间相互依赖,相互调用,相互影响,所以,更改了一段代码,往往需要重新编译全部的微服务,然后重启全部的微服务。

这个过程相当的耗时,往往重启一次,半个多小时就过去了。

甚至,我可能就是加个符号,就需要半个多小时,才能验证加的对不对。

这个效率就太低了。

工作效率怎么提升,网上有很多的资料。

我这里只说2个我认为可以大大提升效率的方式。

使用热更新。idea最新的版本,不断在优化热更新的功能,所以,我们应该好好的利用这个功能。修改了代码,做个热更新,可能不到1分钟就完成了。

在调试状态下写代码。有时候,不确定数据格式,不能动手写,此时,我们可以打个断点,在调试窗口验证自己的代码,然后将代码从调试窗口拷贝到代码窗口,然后使用热更新,使代码生效。

原本做这一个流程可能需要1个小时左右,现在可能需要3分钟就OK了。效率提升非常明显。

所以,我们应该不择手段的提升自己的工作效率。(当然这个不择手段只是形容哦)

10.2 与别人协作的效率

自己的工作完成了,可以与同事联调了。但是同事被其他的事情打扰了,导致没有按照约定的时间进行联调。

那么,怎么办呢,是等待呢还是等待呢。

等待一定是最浪费时间的。

我的选择是将自己自测的结果记录下来,然后将记录提供给同事,然后我就可以做其他的事情了,同事需要联调的时候,重放我的记录,就实现了联调。

以最常见的前后端模式为例:

前端快于后端:

定义好服务调用,构造假数据,前端根据假数据进行开发,后端开发的时候,将假数据替换即可。这是对后端比较舒服的场景,开发完可以立刻看到效果。

后端快于前端:

这个需要后端记录好服务调用,然后使用工具记录自测,前端在开发的时候,可以重放,进行调试。

所以我的选择是将自测过程的请求的url地址,参数等等使用postMan记录,然后将postMan项目分享给同事。如果自测通过,将程序发布到服务器,同时,更新postMan中的请求地址。

这样前端在需要调试的时候,点一下发送,就能够复现整个服务调用过程。

完全不需要后端参与。

(这部分我认为其他公司可能有比这更好的方案,只是自己的公司的人员不太喜欢使用新工具…)

image-20200917120606109

相当于是把同步改成异步了。

11. 处处留名,多做记录

这个主要是说,在代码中多写注释,只要你绝的有用,就可以用注释标注。不过一些无用的信息,和代码逻辑无关的,千万不要写。(和个人理解有关,合适就行)

多做记录是指,即使写文档是一件讨厌的事情,但是改写的还是要写。有了文档其他人更好理解你的想法了。

image-20200917133400458

12. 说明

当然这些不一定对,只是我在做这三个多月的项目,得到的一点体会。

迭代开发,使用软件工程理论指导,分析旧代码,设计新代码。业务清晰,编码风格规范,合理使用多线程,使用异步。统一日志、异常,积极使用新工具。多些文档,少写bug。

猜你喜欢

转载自blog.csdn.net/a18792721831/article/details/108640587
今日推荐