第七部分:方法论——十四年沉淀的思考框架

3 阅读23分钟

第七部分:方法论——十四年沉淀的思考框架

第二十二章 回顾与总结——做技术决策的底层逻辑

22.1 二十一个决策,一个原则

回头看这十四年的十一个核心架构决策、七个实战案例、三个框架基础设施,底层逻辑其实只有一个:

在约束中找到最优解。

不是为了技术先进而用新技术,也不是为了彰显能力而造轮子。每一次决策,都是被逼到墙角之后的务实选择:

决策约束务实选择
自研IOC部署环境混乱、团队有限只写需要的三百行代码
新老共存老系统不能停null判断兼容两种路由
统一入参前后端各自为政框架做"翻译层"
CGLIB+AOP没有Spring/AspectJ自己写责任链
ASM读字节码编译环境不可控直接读class文件
ThreadLocal业务层不想依赖Servlet上下文绑定到线程
自研ActiveRecordHibernate太重、SQL不可控_t状态机+save()自动路由
MongoDB混合存储不能改老代码Dao加level属性
MyBatis源码改造Oracle硬解析+防SQL注入改一行源码+绑定变量
Excel模板报表报表格式频繁变动Excel当模板
Activiti任意流转不能改流程定义运行时动态改连线
流程图追踪Activiti自带不够用自己解析BPMN画图
外挂会签multiInstance太死板独立子流程+关联表
JDBC游标导出MyBatis全量加载OOM绕开结果集映射
SM4加解密信创改造工期紧注解+DBUtil管道
DLL全局开关工期刚性、医院差异大INI配置驱动,改配置不改代码
远程帮办预算不够买厂家方案WebRTC自主实现,买不起就自己搓
RAG智能客服数据不能出内网本地Embedding+知识库锚定
原子审计日志业务日志必须原子写入commit里调存储过程,同连接同生死
慢SQL检测不上APM但需要性能监控@monitoring+103行TuningUtil
数据库表驱动定时任务不上Quartz但需要定时调度Timer+反射+数据库表

五种主流方案在政务场景下的适用性对比:

方案理想场景政务场景的问题我们的替代
Spring全家桶团队充足、环境标准化依赖重、中间件冲突、排错链长300行自研IOC
Hibernate领域模型复杂SQL不可控、N+1问题、学习成本_t状态机ActiveRecord
Redis缓存高并发热数据未安装、运维不会、数据一致性复杂MongoDB混合存储
APM(SkyWalking)微服务分布式链路需ES、部署重、运维门槛高103行TuningUtil
Quartz定时任务复杂Cron/集群调度依赖重、功能过剩Timer+DB表

每一条替代不是"更好",而是"更匹配场景"。如果团队有10个人、服务器都是新的、部署环境统一——上面五种方案可能都是更好的选择。问题是,我们的场景不是这样。

22.2 务实主义的三个核心思维

前面说"在约束中找最优解"是底层逻辑。但底层逻辑太抽象了,具体怎么操作?回顾十四年的决策,我提炼出三个反复出现的思维模式。它们不是理论,而是从一次次犯错中总结出来的。

思维一:"够用就好"不是偷懒,是最高级的克制

我见过太多"过度设计"的案例。2012年我刚决定自研框架的时候,也差点掉进这个坑。

那时候做技术的人有一种执念——框架要"通用"。所以很多团队写框架,上来就搞插件机制、搞热部署、搞注解扫描,恨不得把Spring的功能抄一遍。结果呢?框架写了大半年还没上线,业务需求已经改了三轮。框架越来越"完善",项目却越来越延后。

我选择了一条反直觉的路:BeanFactory只写三百行。三百行能干什么?反射实例化、单例管理、依赖注入——就这些。没有循环依赖检测,没有三级缓存,没有Profile环境隔离。因为我的场景不需要。

第五章里记录了这个决策的完整过程。当时我也犹豫过——要不要加上Spring那些"高级特性"?后来想想,我连循环依赖都没遇到过,加上去就是给自己挖坑。果然,十四年过去了,三百行的BeanFactory还在跑,从来没因为"不够完善"出过问题。

再看MongoDB混合存储(第九章)。我们没有把所有Dao都做MongoDB缓存,只缓存查询慢的那些。为什么不全量?因为全量缓存意味着全量维护——数据一致性、缓存失效策略、内存管理……每多缓存一个Dao,运维成本就涨一截。我们的原则很简单:谁慢谁加缓存,不慢不管。结果只有5%的Dao加了缓存——主要是参保人基本信息查询、缴费记录查询、待遇核定查询这三个高频场景——但系统整体P95响应时间从秒级降到了百毫秒级。加缓存前,参保人信息查询1.2秒(120万条数据全表扫描),加缓存后45毫秒。三个高频查询优化后,用户体感的"系统速度"直接翻了一倍。

SM4加解密(第十四章)也是一样。只加密身份证号、银行账号这些敏感字段,不加密整张表。有人问我"万一以后要加密其他字段呢?"我说"到时候再加就是了,提前加密是提前给自己找麻烦。"

克制的反面是什么?是"提前设计"。提前设计不是不好,但在政务场景下,你的需求三个月一变,提前设计的那些"扩展性",大概率永远不会用到。反而是那些为了扩展性引入的抽象层,在需求变更时变成了沉重的负担。

我接过一个中途接盘的政务系统,堪称"过度设计"的教科书案例。系统基础信息约1000万条,核心业务事件表每天新增几百条,一年也就十几万。项目组上了分库分表框架。

接手后发现什么?看板统计模块慢得令人发指。统计报表需要关联事件表、任务表、类型表、流转表做多维度聚合——跨库JOIN直接报错,复杂聚合SQL路由失败。为了在框架限制下跑出结果,原来的实现把一个统计接口拆成了几十个单表单指标查询,Java层拼接计算。一个方法里十几次Mapper调用,5-6层if-else嵌套,每个分支里又是重复的查询和计算逻辑。一个看板页面加载十几秒。

而事实是什么?事件表一天几百条,一年十几万。这个量级单表单库完全扛得住,MySQL处理百万级聚合查询也就毫秒到秒级。上分库分表是纯粹的技术炫技——把一条SQL能搞定的事变成了灾难。

最终怎么救的?和第十三章JDBC游标导出一样的思路——绕开框架,直接JDBC连物理库。多条单指标查询合并成一条完整统计SQL,同比环比通过子查询一次性搞定。接口响应从十几秒降到毫秒级。

这个案例完美诠释了"够用就好"的反面:1000万基础信息听着吓人,但那是用户维度的事。真正高频的业务事件表,数据量根本没到需要分库分表的阈值。过早优化没有带来性能提升,反而杀死了复杂查询的能力。如果让我从一开始做技术决策,业务事件表和统计相关表一定走单库。分库分表解决的是单表数据量过大的问题,但它同时杀死了复杂查询的能力。在数据量没到那个量级之前,别给自己挖坑。

克制的前提是对需求的准确判断——你真的知道你需要什么吗? 这话说起来简单,做起来极难。它需要你深入理解业务,理解部署环境,理解团队的能力边界。这种判断力不是靠读几本书就能获得的,它只能从一次次的"过度设计然后后悔"中积累。

所以,"够用就好"不是偷懒。偷懒是什么都不做。"够用就好"是你清楚知道边界在哪里,做到边界就停手。这比"做到极致"难得多——因为极致有标准(比如性能指标),但"够用"没有客观标准,它完全取决于你对场景的理解深度。第一章里那套F5+RAID 5+数据库缓存的架构就是最好的注脚——没有一项技术是新的,但每一项都吃准了8:1读写比的业务特征。够用,稳当,省钱。这就是最高级的克制。

思维二:"可控性"比"功能性"重要一百倍

2016年冬天,下午出了问题,一直追到深夜——生产环境挂了。

登录服务器一看,报错信息指向一个第三方jar包的内部方法。我打开源码(幸好是开源的),跟踪了几个小时,发现是对方的一个边界条件没处理好。我改不了那个jar包,只能绕——但绕的方式很丑陋,加了一层try-catch吞掉了异常。问题暂时解决了,但我知道这颗定时炸弹还在。那次从下午追到深夜,通宵才定位清楚。

那次排错让我彻底理解了一件事:对我们这种小微企业,出了问题必须能快速定位、快速修复。框架越强大,排查时需要理解的上下文就越多。

这就是为什么我们选择自研IOC而不是用Spring。Spring好不好?当然好,功能比我们的BeanFactory强大一万倍,在团队充足、技术栈统一、部署环境标准化的场景下,Spring是更好的选择。但我们的场景不是这样——项目组就两三个人,部署环境复杂,出了问题必须五分钟定位。Spring的功能越强大,它内部的调用链就越长,排查问题时需要理解的上下文就越多。不是说理解不了,而是说在追问题追到深夜、脑子已经发木的时候,排查三百行自己写的代码和排查Spring一千多行的AbstractApplicationContext.refresh()调用链,效率是数量级的差距。框架的每一行代码都要能说清来龙去脉。BeanFactory三百行,我闭着眼睛都能告诉你第几行在做什么。出了问题,我五分钟定位,十分钟修复。这种"掌控感"在资源有限的团队里是无价的。

再看其他决策:

前后端解耦(第五章),我们选框架自动翻译参数而不选让业务方法自己解析。为什么?因为每个方法自己解析参数会产生大量重复代码,而且前端需要背一张巨大的接口参数表。框架做翻译层,前端和后端各自用自己最舒服的方式工作,功能上更灵活,维护成本更低。

流程图追踪(第十二章),我们选自己解析BPMN画图而不选Activiti自带的流程图组件。为什么?因为Activiti的图不好看,而且不好定制。更重要的是,它的渲染逻辑我们看不懂——出了显示bug我们改不了。自己画,Java2D的每一笔都是我们写的,想怎么调就怎么调。

Activiti任意流转(第十一章),我们不改流程定义,而是在运行时动态修改连线。为什么不直接改BPMN?因为改了BPMN就改了"源码",出了问题回退不了。运行时改连线,重启服务就恢复原状。功能性一样,但可控性天差地别。

原子审计日志(第六章),我们在commit()里调存储过程写审计日志,存储过程内部用自治事务。为什么不用两个普通事务分别写?因为业务回滚了日志就丢了,在政务审计里是不可接受的。自治事务保证了业务回滚日志也在,几十行代码加一个存储过程,解决了分布式事务才能解决的问题。

慢SQL检测(第六章),我们用@monitoring+TuningUtil把慢SQL自动入库,而不是部署SkyWalking或Pinpoint。为什么?因为政务内网部署APM需要Elasticsearch/HBase这些重型中间件,光搭环境就够运维喝一壶。我们的方案:一张TUNINGEVENT表,运维查表就知道哪个SQL慢。上线第一天就抓到一个8秒的查询。

有人会说"你这样什么都自己写,不是重复造轮子吗?"我的回答是:在可控性和功能性之间,我永远选可控性。 因为政务系统的第一要求不是"功能多",而是"稳定运行"。功能多了可以加,系统挂了没法交代。

可控性的另一个维度是"简单"。我们选择的方案,没有一个是复杂的——三百行IOC、直连JDBC、INI配置文件、注解驱动的加解密……每一个方案都是"一看就懂"的。这不是因为我们能力不够写不出复杂方案,而是因为我们深知:复杂的方案在故障时就是灾难。通宵排错的时候,你希望看到的是三行清晰的逻辑,还是三十层嵌套的抽象。

简单的方案容易理解,容易理解的方案容易排错,容易排错的方案才稳定。 这条链条是可控性的本质。

思维三:"渐进式"是政务系统改造的唯一安全路径

2015年,我们做了一个错误的决策:一次性把老系统的前端全部从JSP改成前后端分离。

当时觉得很酷——前后端分离是行业趋势,老系统早该升级了。我们花了三个月,把所有JSP页面全部重写成HTML+AJAX。上线那天信心满满,结果第一天就出了十几个bug——前端参数传递格式不一致、接口兼容性问题、老数据的展示异常……焦头烂额地修了一个月才算稳住。

那次教训让我刻骨铭心:政务系统不能搞"大爆炸"式切换。

还有一个教训比这次更早,至今没修完。

万能表单引擎——第十一章博客里讲过的onlyFrom.jsp,一个47KB的JSP文件扛起所有流程的所有表单。JSP顶部有一段Java代码,直接在JSP里查数据库拿表单元数据。写的时候图快,没走框架的DBUtil管道,直接用JDBC:

<%
Connection conn = ...;
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select ...");
while(rs.next()) { ... }
// 没有在finally里关闭conn
%>

这是我写的。不是新来的同事,不是别人,是我自己。

写的时候觉得"就查一次元数据,能有什么问题"。后来项目里其他JSP也开始这么干——复制粘贴最省事,工期紧的时候谁都想走捷径。最严重的一个项目,十几个JSP文件全是直连数据库。

后果不是立刻出现的。开发环境并发低,问题看不出来。上线跑了几个星期,用户开始报"获取连接失败"。运维登录中间件控制台一看——连接池过载。JSP里开的Connection没有走框架的AppContextContainer管理,delSession()的finally兜底管不到JSP里的连接。连接泄漏像慢性毒药,一个一个漏,连接池慢慢被耗尽。

运维的应急手段是重置连接池。重置的瞬间,所有正在执行的请求全断——包括那些走框架的正常请求。别的功能也跟着受影响。用户不懂什么连接池,只知道"系统又挂了"。

为什么到现在没修完?因为万能表单在线上跑着——改它等于动核心。而框架的route.java里delSession()已经能兜底关闭大多数泄漏的连接,问题从"频繁出事"降到了"偶尔出事"。偶尔出事就不紧急,不紧急就排不上优先级。技术债就是这样越积越厚的——不疼就不还。

这个教训比2015年那次更让我难受。2015年是"想做对但方法错了",这次是"知道不该这么干但还是这么干了"。框架的每一层设计——连接管理、事务控制、异常兜底——都有存在的理由。我写了这些设计,然后自己绕开了它们。

自己定的规矩,自己先破了。这比不懂规矩更危险。

为什么?因为政务系统的利益相关方太多了。业务处的人在用、领导在看、基层单位在操作。你一次性全改了,哪怕只有一个功能出了问题,都会被放大成"系统不好用"。而且政务系统往往涉及真实资金——社保待遇、医保结算、行政审批——出了问题就是民生事故,容不得半点闪失。

从那以后,所有改造我们只走一条路:渐进式。第五章新老路由的null判断,第九章MongoDB的level属性,第十四章SM4的usageCode全局开关,第十五章DLL的INI配置——每一处改造都保留了"回退到原状态"的能力。保留回退能力,等于给自己买了保险。

"够用就好"决定了我们不做多余的事;"可控性"保证了做的每一件事都能随时调整;"渐进式"确保了调整过程不会造成全局影响。三个思维合在一起——不贪多,不失控,不冒险。

但必须诚实地说:渐进式是有代价的,代价叫技术债。

第五章新老路由共存,route.java里那个null判断——if (action != null)——至今还在。第九章MongoDB的level属性,让同一个框架里并存三种数据访问路径。第六章审计日志的存储过程,是在Oracle上的自治事务,换个数据库就得重写。第九章改MyBatis 3.1.1源码那一行,锁死了MyBatis的版本——十几年没升级,不是因为不想升,是因为升了那行补丁就没了。上一节讲的JSP直连数据库——我自己写的,至今没修完。

每一个"最小改动"的决定,都在系统里留了一根线头。线头多了,新接手的人会困惑:"这里为什么有两套路由?""这个null判断是干什么用的?""为什么分页不走MyBatis标准流程?""为什么万能表单不走DBUtil?"

我们的做法是在代码里写注释——不是废话注释,是决策注释:// 2012年改的,为了绑定变量防SQL注入,详见第九章。同时在框架文档里维护一份"决策记录"——每个看起来"奇怪"的设计,都有WHY。

技术债就像房贷——你可以背,但要知道背了多少、利率多少、什么时候还。最危险的不是有技术债,是不知道自己有技术债。 我们的选择是:接受技术债作为渐进式改造的必然代价,但要求每一笔债都有记录、有原因、有偿还计划。

什么时候该推倒重来?我们的经验是:当一个模块的需求变更频率超过每月一次,而且每次变更都牵扯到多处补丁代码时,就该考虑重构了。第五章的老路由共存,坚持了三年,最终在新项目里统一迁移完毕——因为老系统的业务已经冻结,不再变更了。第九章的MyBatis源码补丁,至今还在跑——因为分页逻辑稳定,没有变更需求,债虽在但不疼。

但JSP直连那笔债是疼的——连接池过载的问题还会偶尔出现。之所以还没还,不是因为不疼,是因为动万能表单的风险太大:它在线上跑着几十个流程,改它等于动核心。这恰恰说明了技术债最危险的特性:越大的债越不敢还,越不敢还利息越高。

不疼就不还,疼了也不一定能还——但永远别假装债不存在。

这大概就是务实主义的全部了。

22.3 技术会过时,思维不会

这二十多年里,我们用的技术栈一直在变——从VC到PB到Java,从C/S到B/S,从单机到分布式。框架的具体实现会过时,ASM的字节码读取方式在新版本Java中可能要调整,Activiti 5.13的ActivityImpl在Activiti 7中可能已经不存在了。

但"基于场景做取舍"的思维方式不会过时。

几年后,也许AI能自动写代码,也许量子计算能解决所有性能问题,也许区块链能解决所有安全问题。但如何在一个充满约束的真实场景下做出务实的决策——这个问题永远不会消失。

因为约束永远存在。预算永远不够,工期永远太紧,环境永远不如人意,需求永远在变。

框架浮沉,思维长存。

而贯穿所有这些思维的,正是全书反复验证的那条主线——务实主义。从手写IOC到RAG客服,二十一个决策看似技术跨度极大,但底色始终如一:尊重约束,适配场景,够用就好,可控优先。这不是一套理论,而是一种态度——面对真实的限制,做真实的选择,交付真实的结果。这条线,在前言里埋下,在每一章的决策中生长,到最后收束在这里:在政务信息化的土壤上,她是一种经过验证的技术哲学。

政务信息化是一条独特的赛道。相比于互联网追求的极致高并发,我们更多是在与现实的约束共舞。这种环境虽然限制了技术的"前沿"程度,但却极大地锻炼了开发者在资源受限、环境复杂情况下的系统性思维。在这里积累的"落地能力"与"抗压经验",同样是技术生涯中不可多得的财富。技术没有高低贵贱,只有适不适合当下的土壤。

愿你在现实的镣铐下,亦能舞出最美的舞步。

附录****

附录A 术语表****

本书涉及大量政务信息化领域的专用术语和技术缩写,以下是阅读过程中可能需要查阅的条目。

术语全称/含义说明
信创信息技术应用创新国家推动的基础软硬件国产化替代战略,要求政务系统使用国产CPU、操作系统、数据库、中间件
等保网络安全等级保护(等保2.0)国家强制性的信息安全保护制度,政务系统一般要求等保三级,涵盖物理安全、网络安全、应用安全等
密评密码应用安全性评估依据《密码法》对信息系统密码应用的合规性评估,要求使用国密算法(SM2/SM3/SM4)
国密中国国家密码算法SM2=非对称加密(类似RSA),SM3=哈希(类似SHA256),SM4=对称加密(类似AES)
网闸安全隔离网闸物理隔离两个网络之间的安全设备,只允许单向或受控的数据摆渡,政务内外网之间常见
KC22/KC21/AB01等人社部标准编码人力资源和社会保障部的统一数据编码体系。K=医疗,C=个人,A=基本,B=单位。如KC22=医疗费用明细表,KC21=就诊等级信息表,AB01=参保单位基本信息表,AAC001=个人编号
五把锁政务场景五大约束环境锁、需求锁、安全锁、运维锁、资源锁——定义见第一章1.2节
IOC控制反转(Inversion of Control)对象的创建和依赖管理由容器负责,而非业务代码自行创建。Spring是最常见的IOC容器
AOP面向切面编程(Aspect-Oriented Programming)将事务、日志、监控等横切关注点从业务代码中分离出来统一管理
ORM对象关系映射(Object-Relational Mapping)将数据库表映射为程序对象。常见框架有Hibernate、MyBatis
DataStore数据容器本书框架的核心数据结构,源自PowerBuilder的DataStore概念,包含行集(RowSet)和行(Row),每行携带状态(_t)和旧值(_o)
RAG检索增强生成(Retrieval-Augmented Generation)大模型回答前先从知识库检索相关内容,让回答锚定在真实数据上,降低"幻觉"风险
OOM内存溢出(OutOfMemoryError)Java应用内存耗尽时的崩溃错误,政务系统常见于大数据导出场景
BPMN业务流程建模标记语言(Business Process Model and Notation)工作流引擎使用的标准流程定义语言,以XML格式描述流程节点和连线
游标数据库游标(Cursor)逐行读取查询结果集的机制,避免一次性加载全部数据到内存
DLL动态链接库(Dynamic-Link Library)Windows平台的共享库文件。本书第十七章用一个DLL通过配置适配几十家医院的接口差异
TPS每秒事务数(Transactions Per Second)衡量系统吞吐量的核心指标。政务系统TPS通常不高,但单笔业务涉及操作多
DMZ隔离区(Demilitarized Zone)内网与外网之间的缓冲区域,放置需要对外提供服务但又需要保护的服务器

全书完

后记****

写这本书不是为了证明什么,而是为了记录。记录那些在深夜排错时闪过的灵感,记录那些在约束条件下找到的突破口,记录那些朴素却经过验证的解法。

多年过去了,系统还在线。如果这些记录能帮到哪怕一个正在信息化战场上摸爬滚打的人,便是作者最大的欣慰。