第七部分:方法论——十四年沉淀的思考框架
第二十二章 回顾与总结——做技术决策的底层逻辑
22.1 二十一个决策,一个原则
回头看这十四年的十一个核心架构决策、七个实战案例、三个框架基础设施,底层逻辑其实只有一个:
在约束中找到最优解。
不是为了技术先进而用新技术,也不是为了彰显能力而造轮子。每一次决策,都是被逼到墙角之后的务实选择:
| 决策 | 约束 | 务实选择 |
|---|---|---|
| 自研IOC | 部署环境混乱、团队有限 | 只写需要的三百行代码 |
| 新老共存 | 老系统不能停 | null判断兼容两种路由 |
| 统一入参 | 前后端各自为政 | 框架做"翻译层" |
| CGLIB+AOP | 没有Spring/AspectJ | 自己写责任链 |
| ASM读字节码 | 编译环境不可控 | 直接读class文件 |
| ThreadLocal | 业务层不想依赖Servlet | 上下文绑定到线程 |
| 自研ActiveRecord | Hibernate太重、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客服,二十一个决策看似技术跨度极大,但底色始终如一:尊重约束,适配场景,够用就好,可控优先。这不是一套理论,而是一种态度——面对真实的限制,做真实的选择,交付真实的结果。这条线,在前言里埋下,在每一章的决策中生长,到最后收束在这里:在政务信息化的土壤上,她是一种经过验证的技术哲学。
政务信息化是一条独特的赛道。相比于互联网追求的极致高并发,我们更多是在与现实的约束共舞。这种环境虽然限制了技术的"前沿"程度,但却极大地锻炼了开发者在资源受限、环境复杂情况下的系统性思维。在这里积累的"落地能力"与"抗压经验",同样是技术生涯中不可多得的财富。技术没有高低贵贱,只有适不适合当下的土壤。
愿你在现实的镣铐下,亦能舞出最美的舞步。
附录****
本书涉及大量政务信息化领域的专用术语和技术缩写,以下是阅读过程中可能需要查阅的条目。
| 术语 | 全称/含义 | 说明 |
|---|---|---|
| 信创 | 信息技术应用创新 | 国家推动的基础软硬件国产化替代战略,要求政务系统使用国产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) | 内网与外网之间的缓冲区域,放置需要对外提供服务但又需要保护的服务器 |
全书完
后记****
写这本书不是为了证明什么,而是为了记录。记录那些在深夜排错时闪过的灵感,记录那些在约束条件下找到的突破口,记录那些朴素却经过验证的解法。
多年过去了,系统还在线。如果这些记录能帮到哪怕一个正在信息化战场上摸爬滚打的人,便是作者最大的欣慰。