第一部分:思考方法

7 阅读46分钟

第一部分:思考方法

第一章 政务场景下的架构决策——不是选最好的,是选最合适的

1.1 一个被忽视的战场

本章核心:五把锁框架——环境、需求、安全、运维、资源,是政务架构决策的分析起点。

坦白说,如果你是在互联网公司做架构,这本书中的一些做法可能并不适用于你的场景。你的场景是高并发、大数据、弹性伸缩,那是一套完全不同的打法。不过你可以看完再喷,也许对你有些启发。

但如果你在做政务系统、国企信息化、或者任何"需求方驱动的定制化项目",你可能会遇到和我们类似的挑战:

• 客户的服务器是十年前买的,4G内存,硬盘转得费劲;

• 部署环境复杂,Tomcat、WebLogic、东方通、中创,你无法预知下一个现场会遇到什么样的运行环境;

• 政策调整往往对应紧急的需求变更,截止日期与政策生效日期挂钩;

• 前端只有一个实习生在写,后端也就你一个人,维护了十几年的系统,几百万行代码,没人敢动。

在这种场景下,你翻开架构书,书上说"微服务、容器化、DevOps"。你抬头看看面前那台还在跑Windows Server 2012的老服务器,再看看架构书上的"最佳实践"——二者之间横亘着一道鸿沟。

不是这些技术不好,是你的场景用不上。

这就是这本书想聊的事:在真实的约束条件下,怎么做出务实的架构决策。不是追求"最佳实践",而是追求"场景最优解"。

1.2 五把锁

政务场景的约束可以归纳为五把"锁":

环境锁——你控制不了基础设施。

政府的IT基础设施采购周期长,服务器往往是三五年甚至十年前的配置。中间件可能是国产化要求下采购的某个你从没听过的产品。"升级服务器"或"更换中间件"通常需要经过漫长的审批流程,在大多数情况下只能在现有框架内寻找方案。

需求锁——你控制不了变更频率。

社会发展快,政策变化是政府工作常态。今天社保缴费比例调整了,明天医保报销范围变了,后天新农保又出了新规。每一项政策变化对应的就是需求变更,而变更的截止日期往往就是政策生效日期。几乎没有协商空间。

安全锁——你承担不起数据出错。

社保系统里的参保人信息、医保系统里的报销记录,每一条数据背后都是真金白银。一个Bug可能导致该领养老金的人领不到,该报销的费用报不了。这不是"用户体验差"的问题,是民生问题,政治问题。同时,政务系统还受等保合规、信创改造、密码法等法规约束——敏感字段必须加密存储(第十四章),数据操作必须留审计日志(第六章、第十章),国产化环境下密码运算必须走国密认证的密码机。这些合规要求不是"安全建议",是硬性红线,不满足就不能上线。

运维锁——出问题时凑齐人都困难。

政务系统的运维往往涉及多家单位——应用开发商、数据库厂商、中间件厂商、网络运营商、安全厂商,各有各的维护人员、各有各的排障流程。出了问题要联合排查时,凑齐人都困难:应用的人说"我这边没问题",数据库的人说"我这边也没问题",中间件的人说"你先查网络"——协调一个联调窗口可能就要两三天。第十六章会讲到"技术问题十分钟,协调落地一周",说的就是这种情况。更不用说系统部署到区县甚至乡镇,当地的技术力量有限,大部分问题还得靠远程排查——我们的运维人员甚至远程过参保人的桌面,帮他们操作系统。所以系统的可观测性必须做在框架里——让问题自己"浮"出来,不需要凑齐人就能定位。

资源锁——你改变不了预算和工期。

政务项目的预算是年初定的、工期是合同签的,这两样东西你都改变不了。预算不够买商用方案?自己搓。工期只给了两周?用最快的方案而不是最好的方案。远程帮办买不起厂家的?WebRTC自己实现(第十八章)。这个约束贯穿了全书:手写IOC是因为Spring的复杂度在小团队、多环境、快排错的场景下是负担,JDBC游标导出是因为没有时间改造MyBatis的核心架构,DLL全局开关是因为工期倒逼来不及为每家医院写独立代码。

这五把锁决定了政务系统架构的几个核心特征:轻量、可控、可查、渐进。

1.3 决策的思考框架

面对一个架构决策,我们的思考路径通常是这样的:

第一步:场景分析。 当前环境有什么约束?需求是什么?安全要求是什么?运维能力如何?预算和工期如何?这五把锁分别锁住了什么?

第二步:方案罗列。 不管多离谱,不要排斥。先列出所有能想到的方案。包括"最佳实践"、包括"土办法"、包括"自己造轮子"。

第三步:约束过滤。 用五把锁去过滤方案。环境不允许的,排除。需求不匹配的,排除。安全不达标的,排除。运维搞不定的,排除。预算和工期不允许的,排除。

第四步:成本评估。 剩下的方案,评估实施成本。包括开发成本、测试成本、上线成本、维护成本。政务系统最怕的是"上线成本"——老系统改造可能影响大量存量功能,回归测试的工作量往往是新功能的几倍。

第五步:选最不差的。 没有完美方案,只有最不差的。在约束条件下,选择风险最低、成本最小、最可控的那个。

三个核心思维贯穿始终:

• "够用就好"在方案罗列时起作用——只加需要的不加不需要的

• "可控优先"在约束过滤时起作用——排除不可控的方案

• "渐进式"在成本评估时起作用——保留回退能力的方案优先

你看,这个框架里没有"技术先进性"这个维度。不是因为技术先进性不重要,而是因为在我们这个场景下,它优先级排不到前面。

拿一个真实的问题来走一遍这五步。某市社保局的老系统,查询参保人信息越来越慢,运维同事反映打开一个参保人详情页要五六秒。有人建议:"上Redis做缓存吧。"听起来合理吗?合理。我们来用五步走一遍。

场景分析: 服务器4G内存,Oracle 11.2.0.4数据库,Tomcat 6,没有运维人员会装和维护Redis。参保人数据涉及隐私,缓存层的数据一致性怎么保证?性能优化没有截止日期,可以慢慢来。

方案罗列: 方案一:上Redis。方案二:加内存、升级服务器。方案三:优化SQL和索引。方案四:用MongoDB做缓存层(我们已经在用的)。方案五:前端分页+懒加载,减少一次查询的数据量。

约束过滤: 方案一(Redis),老服务器装Redis有运维风险,运维人员不会维护。方案二(升级硬件),要走采购审批,周期三个月起步,排除。方案五只能缓解不能根治。剩下方案三和方案四。

成本评估: 方案三需要DBA配合优化索引,排查慢SQL,可能还要改MyBatis Mapper,回归测试量大。方案四——我们框架已经有MongoDB混合存储的设计(第九章),在Dao上加一个level属性就行,业务代码零改动。

选最不差的: 方案四。加一行配置,不改一行业务代码。风险最低、成本最小、最可控。

你看,Redis是更好的缓存方案吗?当然是。但它在这个场景下不是最合适的方案。这就是五步思考框架的价值——帮你在约束条件下找到真正可行的最优解,而不是理论上最优但实践中无法落地的方案。

1.4 一个真实的场景——四把锁卡死

上面的五步走还是偏理论。让我讲一个真实的场景,你就知道这些约束不是纸上谈兵。

2016年,某省城乡居民养老保险业务系统上线主动缴费业务。据测算峰值要达到每分钟4000笔。我们的数据库实测TPS只有160+。政策规定了执行时间,倒计时已经开始了。接口提供给银行调用,银行的应用对外——安全也是硬约束。

需求先卡上来。 政策规定的上线日期不能拖,主动缴费是便民工程,延期影响的是老百姓的切身利益。

环境是最大的技术瓶颈。 每分钟4000笔,每笔业务涉及查参保信息、校验缴费基数、写缴费记录、更新账户余额——保守估计一笔至少5次数据库操作。4000笔就是每分钟20000次,折合TPS要330+。数据库实测只有160+,差了一倍。

安全横在中间。 接口给银行调用,银行端对外,传输的数据涉及参保人身份信息和缴费金额。身份证号泄露是隐私事故,缴费金额算错是资金事故。接口必须加密、防篡改、防重放。

资源把退路堵死。 买硬件的预算没有,只能利旧。项目组就那几个人,手里还有其他需求在并行。

四把锁卡在一起,TPS差一倍,没有新硬件,怎么扛?

答案是MongoDB。交易时直接写MongoDB——读写都不碰Oracle,TPS瓶颈直接绕开。MongoDB的写入性能扛每分钟4000笔绰绰有余。

Oracle也没丢。我们启用了独立的进程,把交易数据串行写入Oracle。不影响主交易的响应速度——MongoDB扛实时,Oracle异步补。这样即使MongoDB出了问题,Oracle那边的异步进程可能已经写进去了。就算极端情况两边都没写成功,银行那边的扣款记录是铁证——每天晚上和银行对账,按银行记录补数据,一分钱不会少。三层保障:MongoDB实时、Oracle异步、银行对账兜底。

服务启动时先热缓存,把参保人基本信息从Oracle预加载到MongoDB。启动完成后再开放接口,避免冷启动时大量请求打到Oracle上。

上线那天,峰值扛住了。MongoDB扛住了每分钟4000笔的读写,Oracle在后面慢慢串行写,两边各干各的。晚上对账,一分钱不差。

回头看这个场景,四把锁卡死的时候,救我们的不是什么高深技术,而是"用MongoDB扛实时、Oracle异步补、银行对账兜底"这个三层保障。关键在于敢不敢只写MongoDB——敢,因为真正的兜底不在数据库,在银行的对账记录。这个认知不是拍脑袋想出来的,是对业务流程理解到骨子里之后才有的底气。

1.5 " 适配场景"而非"追逐最佳实践"

这句话听起来像废话,但在实际工作中,大多数架构决策的失误往往源于违反了这个原则。

举个例子:很多互联网架构师来政务项目做咨询,第一句话就是"你们应该微服务化"。微服务好不好?好。但你的系统只有两个后端开发人员维护,微服务化之后运维复杂度增加十倍,你确定搞得定?

再举个例子:有人说"你们应该用Redis做缓存"。Redis好不好?好。但你的老系统有几百个MyBatis Mapper,每个Mapper都是手写SQL,要用Redis就得把所有Mapper改成先查Redis再查数据库的模式。这个改造的工作量和风险,你有算过吗?

"最佳实践"是针对"典型场景"的最优解。但政务场景不是典型场景,它是约束特别多的"非典型场景"。在非典型场景下,照搬最佳实践可能反而增加系统复杂度。

这本书记录的就是我们在非典型场景下的那些"非典型决策"。每一次决策,都是"五把锁"过滤后的结果。不敢保证是最优的,但希望是务实的。

这就是务实主义的内核:不在理论上追求最优解,而在约束中寻找最合适的解。 不是否定最佳实践的价值,而是承认最佳实践有其适用边界。务实主义不是"不思进取",而是"在正确的战场上打正确的仗"。从这个意义上说,"适配场景"就是务实主义的核心原则——后面每一章里的决策,都可以看作是这条原则在不同场景下的具体展开。

下面是一个完整的例子,用来说明此理。

一套政务系统的架构选型,没有任何新技术,但跑了几年稳如老狗。

业务场景很普通:典型的8:1读写比——浏览多、操作少。绝大多数政务业务系统都是这个比例,审批系统、查询系统、统计报表系统,概莫能外。

架构长什么样?F5做负载均衡,4台应用服务器,关系数据库开着缓存,存储用RAID 5配10000转机械硬盘。没有网关,没有服务注册发现,没有容器编排,没有配置中心。

放在今天的技术选型评审会上,这套架构大概会被批得体无完肤——"太老了""没有弹性伸缩""没有服务治理"。

但它跑得非常好。为什么?因为吃透了业务特征。

8:1的读写比意味着磁盘读取是绝对主力。RAID 5的强项就是并行读取快,同时磁盘空间利用率比RAID 1高——这个选型直接吃准了业务规律。关系数据库本身都有缓存机制,高重复度的SQL执行计划、热点数据页都会被缓存,命中率一上去,效率自然就高了。不管是Oracle、MySQL还是PostgreSQL,原理都一样。10000转机械硬盘确实不是全闪存,但8:1读写比加上数据库缓存、应用缓存的双重加持,磁盘I/O根本不是瓶颈。4台应用服务器对这个并发量来说是杀鸡用牛刀,但政务系统图的就是稳——硬件成本比人力成本低得多,多一台服务器比多一次宕机事故划算。

如果在同样的场景下上微服务,能得到什么?部署复杂度翻十倍,运维成本翻十倍,出了问题排查链路从一台机器变成一条调用链,新人来了半个月搞不清系统结构。得到的全是复杂度,失去的全是简单性。

微服务解决的是真问题——团队几十号人同时改代码、模块需要独立演进、某个接口要扛10万QPS、不同模块用不同技术栈。但很多政务项目根本不满足这些条件。团队三五个人,流量就那么点,一个war包部署上去稳稳当当跑几年。

能用简单方案解决的问题,就不要用复杂方案去解决。 这不是保守,这是工程思维。有时候,最好的架构决策就是不做多余的架构决策。

1.6 一个最极端的例子——让C#和Java代码长得一模一样

上一节我们说"适配场景就是务实主义的核心原则"。你可能觉得这话说得漂亮,但终究是理论。那我们来看一个真实的、极端的例子。

2014年,我接了一个海关的项目。三重约束同时卡死:上面绑定了.NET技术栈没得选;预算只有正常的一半,赔不起;团队全是Java程序员,没人会C#。

我选了第四条路:把Java框架的核心设计,一字不差地搬到C#里。类名一样、方法名一样、字段名一样、状态机逻辑一样。业务代码写出来,除了文件后缀,看不出是Java还是C#。

它证明了一件事:编程语言只是语法糖,数据结构和业务逻辑才是核心。 完整过程——从某软看到 DataStore,到2008年社保项目逼出行状态机,到2014年海关项目的代码平移细节、完整的类映射表、以及十几年的回看——我把它放在第二章中展开。

1.7 全书导览

前言已经列出了全书七个部分的结构。每一章都可以独立阅读,但从头到尾读,会看到一条清晰的主线:从"如何思考"到"如何落地",从"框架设计"到"极限生存"再到"跨界突破",最后收束为"十四年沉淀的方法论"。

本章小结

• 政务场景的约束可归纳为"五把锁":环境锁、需求锁、安全锁、运维锁、资源锁。

• 架构决策的五步思考框架:场景分析→方案罗列→约束过滤→成本评估→选最不差的。

• 核心原则:适配场景而非追逐最佳实践。"技术无高下,只有场景之别"。

• 四把锁卡死的真实场景中,"MongoDB实时+Oracle异步+银行对账兜底"三层保障是核心策略。

决策洞察:约束不是敌人,是帮你排除错误选项的筛子。五把锁锁死的越紧,可选方案越少,决策反而越快。

第二章 跨语言平移——让C#和Java代码长得一模一样

本章核心:DataStore/RowSet/Row三件套是跨语言统一的基因,设计模式比语言重要。

第一章提到我用C#平移Java框架的极端案例。这一章展开讲完整的决策过程。

2.1 缘起:从某软看到,到自己干出来

在某软的那几年,我是项目经理。接触到了大量 PowerBuilder 项目,看到了 DataStore 的威力,也看到了某软自己做的 Java 版实现。

但看和干是两回事。在某软我只管进度和质量,不用操心技术选型。离开某软后自己扛架构师这个角色,才发现以前觉得"理所当然"的东西,每一行都得自己写出来、跑通、扛住生产环境的压力。

2008年,我接了个某市社保三级审核的项目。老系统几百个窗口不能逐个改,改漏一个就是线上事故,质量没法保证。我干了一件当时不知道叫AOP的事:重写 DataWindow 的 Update,拦截保存操作,把数据切到审核中间表,审核通过后写回老业务表。老代码一行没改。(详见第六章"前史:2008年那个不知道叫AOP的AOP")

就是在那个项目里,我实现了自己的行状态机——遍历每行的状态,自动拼INSERT/UPDATE/DELETE的SQL。

几年后,正式开始写 Java Web 框架(browise),核心就是照着这个思路来:DataStore、RowSet、Row、DataCenter。DataStore 的概念是某软看到的,但 Row 的状态机(_t=0/1/3)、RowSet 的三区(primary/delete/filter)、Update 的拦截机制——这些是2008年逼出来的原创。

这套东西在 Java 端跑了几年,很顺。然后2014年,某海关的项目来了。

2.2 场景还原:三把锁同时锁死

这个项目没得选。海关总署的技术栈绑定了微软,我们必须用 .NET——ASP.NET Web + IBatisNet(iBATIS 的 .NET 版)+ Spring.NET + WinForms。不是我们选了 C#,是客户强制要求,改不了。需求锁。

更要命的是,这个项目是用预算的一半抢下来的。不想办法省成本,就赔死。资源锁。

团队都是写 Java 的,没碰过 C#。按常规做法,要么招 C# 开发(加人加钱,赔更多),要么团队现学(工期拖延,也赔)。环境锁。

我没犹豫,选了第三条路:把 browise 框架的核心搬到 C# 里。

不是重写,不是"参考",是平移。

2.3 看看代码

核心是 Row 类——状态机追踪每一行数据的变化:

// com.browise.core.util.ds.Row
public class Row {
private int _t = 0;  // 0=不变,1=新增,3=修改
private HashMap<String,Object> map = new HashMap<>();
private HashMap _o = new HashMap();  // 保存修改前的值

    public void setItemValue(Object key, Object value) {
if(map.get(key)==null){
if(_t == 0) _t = 1;  // 标记为新增
}else{
if(_t == 0){
_t = 3;           // 标记为修改
_o.put(key, map.get(key));
}
}
map.put(key.toString(), value);
}

    public boolean isModefiy() { return _t == 3; }
public boolean isInsert()  { return _t == 1; }
// isDelete()见第八章
}

C# 版的 Row.cs:类名一样、方法名一样、字段名一样、_t=0/1/3 的状态机逻辑一字不差,连 _o 存原始值的设计都一样。唯一区别是 HashMap 换成了 Hashtable。不是相似,是一模一样。

DataStore 的使用方式也完全相同:

// 不管是 Java 还是 C#,写法一样
DataStore ds = new DataStore("table");
Row row = ds.getRowset().add();
row.setItemValue("name", "张三");
String json = ds.toJson();

除了文件后缀名,看不出区别。

2.4 完整搬了什么

不只是 DataStore。整个核心层都搬了:

Java (com.browise.core)C# (com.bluepoint)说明
DataCenterDataCenter顶层容器
HeaderHeader消息头
BodyBody消息体
DataStoreDataStore数据集
RowSetRowSet行集(primary/delete/filter三区)
RowRow行(状态机追踪变更)
DaoDao基类(ResultToMap/MapToDao)
ListPropertyListProperty反射工具
AppContextAppContext上下文接口
AppContextContainerAppContextContainer线程存储
AppContextImplAppContextImpl上下文实现
DBUtilSqlMapperExtension数据库工具(IBatisNet封装)

连 WebService 的统一入口都一样——Java 版是 Servlet 路由 BusinessAction,C# 版是 interface.asmx 的 BusinessAction,都是接收 DataCenter 的 JSON,反射调用业务方法,返回 DataCenter 的 JSON。

前后端协议也统一了——前端不管跟 Java 后端还是 C# 后端通信,发的都是 DataCenter JSON,收的也是 DataCenter JSON。

平移过程中踩过几个坑。C#端反序列化再序列化回来,跟Java端的JSON逐字节比对,抓出了三个Bug:中文编码问题、null值处理不一致、小数精度差异。IBatisNet的Mapper文件大部分直接复制改个XML头就行,少数涉及数据库方言差异的SQL(比如Oracle的NVL要改成SQL Server的ISNULL)人工排查。

2.5 教训、代价与反思

回头看这次跨语言平移,最大的教训不是技术层面的,而是认知层面的:架构的真正价值不在于用什么语言实现,而在于能否脱离语言独立存在。

DataStore/RowSet/Row这套设计,本质上是对"数据变更追踪"这个问题的抽象。它可以用Java实现,可以用C#实现,甚至可以用Python实现——因为它的核心是_t=0/1/3这个状态机,是RowSet的primary/delete/filter三区管理,是setItemValue()时自动判断新增还是修改的逻辑。这些跟语言无关,跟数据结构有关。

如果当初我把设计跟Java绑死——比如用了Java特有的序列化机制,或者依赖了某个Java框架的API——平移到C#就不可能这么顺。正是因为设计从第一天起就是"裸"的(纯数据结构+纯逻辑),跨语言才成为可能。

当然有代价。C#有Property语法,本应该写成row.Name = "张三",我硬是用Java的getter/setter风格row.setItemValue("name", "张三")。C#有LINQ,有泛型协变逆变,有var关键字——这些语言优势全放弃了。C#的Hashtable不是泛型的,有装箱拆箱开销,我也认了。因为统一性比语言惯用性更重要。

反过来说,如果一开始就意识到架构可能跨语言复用,我会在某些地方设计得更通用——比如用接口而非具体类来定义Map操作,这样C#端就不必硬用Hashtable,可以用Dictionary<string,object>获得类型安全。但这个认识是事后才有的,当时只想着"照着抄最快",顾不上那么远。

这条经验后来影响了整个框架的设计风格:核心层尽量只用语言最基础的能力(集合、反射、字符串操作),不用任何语言的高级特性或框架特定API。这使得框架至今仍然可以在JDK 6上运行——不是因为不想升级,是因为有些老客户的服务器只能跑JDK 6。环境锁,始终在。

2.6 为什么敢这么干

三个原因。

第一,团队。 政务项目人力不稳定,一个开发可能上半年在 Java 项目组,下半年调到 C# 项目组。框架统一了,调岗不用培训。getItemValue / setItemValue / getRowset / isInsert / isModefiy,会一套就全都会。降低的不是学习成本,是项目的人力风险。

第二,我已经验证过了。 Java 版的 DataStore 已经跑了好几年,各种边界情况都踩过坑。搬到 C# 不是试错,是复制一个已经被证明可行的设计。风险可控。

第三,C# 不争气。 2014年,.NET Framework 只能跑 Windows。语言设计比 Java 优雅得多,但生态封闭、部署受限。政务项目大量 Linux 服务器,Java 不可替代。既然两个语言都要用,不如让它们共用同一套编程模型,减少心智负担。

2.7 项目落地了

这不是 PPT。某海关加工贸易监管系统,覆盖了完整的业务链条:

• 账册管理:经营账册、贸易账册、简易加工账册的备案、审批、变更

• 报关单管理:报关单录入、清单管理、草稿箱

• 仓储管理:仓库、库位、泊位、地磅、出入库

• 数据报核:与海关 H2000 系统对接

• MSMQ 消息传输:大文件分片、报文生成、回执处理

C# 版框架 27 个业务模块,54 个 SQL 映射文件,ASP.NET Web 前端 + WebService + WinForms MQ 接收端。实打实的生产系统。

2.8 决策背后的WHY

有人说这是反模式。 确实。正统观点是"每种语言应该用自己的惯用写法"。我的做法违反了这个原则。但政务信息化有个特点:人力成本远大于技术成本。 一个程序员离职,新人接手要两周。如果框架不同,接手时间翻倍。十个项目、二十个开发、每年流动三四个人——这个成本算下来,远比"代码不够地道"严重得多。

DataStore这套设计跟语言无关。 概念来自 PB,我在某软看到的;但 Row 的状态机、RowSet 的三区、Update 的拦截机制,是2008年那个68万的社保项目逼出来的。PB版用 GetItemStatus() 检查行状态,Java版用 _t=0/1/3 自己跟踪,C#版一模一样。三套代码,同一套设计图纸,根都在2008年。它不是 Java 的,也不是 C# 的,它是数据的。

十几年后的回看。 Java 版从最初的核心 ds 包几个类,发展到现在 120 多个类、14 个模块。C# 版因为项目下线了,停留在 2014 年的 23 个类。但那套设计没有下线——它活在 Java 版里,继续迭代。如果当年没有跨语言统一这个决策,三条路都比"直接平移"贵。

有时候最好的架构决策,不是选择什么技术,而是消除选择的必要。

本章小结

• DataStore/RowSet/Row的设计与语言无关——概念来自PB,状态机是2008年逼出来的。

• 跨语言平移的核心收益:统一编程模型,降低项目人力风险,不是追求代码"地道"。

• 编程语言只是语法糖,数据结构和业务逻辑才是核心。

决策洞察:当团队会被随时调配到不同语言的项目时,统一编程模型比追求语言地道性能降低十倍的人力风险。

第三章 框架全貌——一个自洽的技术体系

3.1 十一个决策的统一原则

本章核心:一个请求从浏览器到数据库,经过IOC→路由→参数解析→AOP→业务→DBUtil六层管道。

这个框架不是一个一个功能堆上去的,它是一个自洽的体系。十一个核心架构决策,背后是同一个原则:

在约束中找到最优解,不追求优雅,只追求落地。

决策原则
自研IOC只加需要的不加不需要的
注解路由+参数兼容不破坏老系统
统一入参解析前后端解耦
CGLIB+AOP用注解的方式
ASM参数读取自己的问题自己解决
ThreadLocal上下文业务层不依赖Servlet
自研ActiveRecord状态机追踪,SQL可控
MongoDB混合存储零侵入,渐进式采用
改MyBatis源码分页第一天解决的事用了十几年
Excel模板报表用业务人员熟悉的工具
Activiti任意流转不能改变框架时就找到"洞"绕过去

你看,没有一个决策是"因为技术先进所以用"。每一个都是"因为场景需要所以做"——同时也都付出了相应的代价。

3.2 框架全景

所有请求进来只有一个入口:route.java。这个Servlet做了所有事情——登录校验、路由分发、参数解析、异常处理、连接管理。

往下是BeanFactory,一个轻量IOC容器,管理所有业务对象的实例化和依赖注入。通过CGLIB代理+责任链模式,实现事务、日志、监控等横切关注点。

再往下是业务层,开发人员写业务代码的地方。

![文本框:

图-3框架全景图

]()最底层是DBUtil,一个统一的ORM入口。MyBatis负责标准CRUD,MongoDB负责缓存加速,JDBC直连负责大数据导出,SM4负责加解密。所有数据操作都经过这个统一的管道。

3.3 为什么是自洽的

"自洽"是我们对技术体系最重要的要求。

什么是自洽?就是出了问题,我们能从入口一步步追踪到数据库,中间没有难以理解的黑盒。从route.java的service()方法开始,到BeanFactory.getBean()获取实例,到DBUtil.getDao()执行查询,到MyBatis的MappedStatement映射SQL——每一行代码我们都能说清来龙去脉。

这个框架没有Spring的依赖注入链路追踪(因为Spring内部的注入机制较为复杂,追踪成本较高),没有MyBatis-Plus的自动CRUD包装(因为自动生成的SQL可能不符合我们的优化需求),也没有任何第三方框架的"魔法"。

每一行代码要么是我们自己写的,要么是我们读过源码、理解了原理、确认了行为的。

这就是为什么这个框架能稳定运行14年。因为当任何问题出现时,不需要过多猜测,不需要搜索,不需要翻文档——直接打开源码,从入口开始,一步步看下去,问题一定能在某个地方暴露出来。

在政务场景下,这种"可追踪性"往往比"开发效率"更重要。因为政务系统出错不是"体验不好",可能是"老百姓的钱发错了"。

十一个核心决策能够同框出现在一张表里,正是因为它们都服从同一个逻辑——用"自洽"来保障可控。这种对"可追踪性"的坚持,是务实主义在政务场景下最朴素也最坚定的表达。

3.4 配置外置的极限——连界面都外置

配置外置到了极致是什么样?连界面都可以配置。

我们做过一个可视化表单设计器——左边控件树拖拽,中间工作区画表单,右边属性面板设置字段名、数据类型、是否必录、二级代码。画好的表单存到t_from表里,开工时status=3就指向一个JSP文件路径。

但这不是纯低代码平台。设计器可以导出JSP——导出成真实的JSP文件,放到项目里继续改。为什么这么做?因为低代码平台的问题所有人都知道:80%的需求5分钟搞定,剩下20%搞不定。纯低代码平台卡死在那20%里——你想加一段自定义逻辑,平台不支持,你就得绕,绕着绕着就崩了。

所以我们的方案是:用工具生成80%,导出成代码手工调20%。 开发效率比纯手工快,灵活性比纯低代码高。最终跑在生产上的不是配置数据,是真实的代码——出了问题能打断点、能加日志、能改逻辑。

这跟全书的思路一脉相承:工具辅助人,不替代人。

3.5 一个请求的完整旅程

光看架构图还是抽象的。让我们跟踪一个具体的请求——用户点击"查询参保人信息"——从浏览器走到数据库再回来。

第一步:前端发请求。 用户输入身份证号,点击查询。前端把表单数据封装成DataCenter——Body里放一个DataStore"queryCondition",字段sfzh(身份证号)= "230102****1234"。序列化成JSON,POST到/person/query。

第二步:route.java接收。 框架唯一的Servlet,映射/*。先从Session取用户信息——空就跳登录页。取出UserContext(用户ID、部门、角色列表、行政区划编码),后面要用。

第三步:路由分发。 praserHandler(req)从路径/person/query解析出bean id = person,method name = query,在handlerMap(ConcurrentHashMap)里查找对应的MethodMap。没找到则走老路由兼容逻辑(第五章详述)。

第四步:BeanFactory获取实例。 BeanFactory.getBean("person")——启动时@bean注解的类已实例化,@property字段已注入。如果类上标了@aoppoint,拿到的是CGLIB代理对象。

第五步:设置上下文。 AppContextContainer.setAppContext(new AppContextImpl(req, userContext)),内部是ThreadLocal。此后业务代码任何位置调getAppContext()即可拿到用户信息,方法签名里不需要HttpServletRequest。

第六步:参数解析。 praserInParameter()用ASM字节码技术读取class文件获取方法参数名(JDK 8之前编译后参数名会丢失),再把JSON里对应数据转换成业务方法声明的参数类型——Dao对象、DataCenter、List或基础类型。

第七步:AOP代理链。 业务方法query()上标了@Trans @Logger。CGLIB代理进入doProxy(),责任链模式执行:TransInterceptor.before()获取连接、关闭自动提交;LogInterceptor.before()记录方法名、入参、时间戳。然后才调真正的业务方法。

第八步:业务代码执行。 PersonAction.query(PersonDao dao, DataCenter dc)——开发人员写的业务逻辑。从dao取出sfzh,调DBUtil.getDao("personMapper.queryBySfzh", dao)。DBUtil找MyBatis的SqlSession,执行SQLSELECT * FROM KC22 WHERE SFZH = #{sfzh},结果转成DataStore的Row集合。

第九步:DBUtil管道处理。 返回前自动检查:@myCode注解→SM4解密(数据库存密文,返回要明文);@myMac注解→SM2验签;level属性→查MongoDB缓存(第九章)。业务代码无感。

第十步:结果返回与收尾。 LogInterceptor.after()记录返回值和耗时。TransInterceptor.after()提交事务(commit前先原子写审计日志)。finally块:清理ThreadLocal,关闭数据库连接。DataCenter序列化成JSON写回浏览器。

十个环节,每一个环节的类名、方法名我都写得出来,源码我都看得见。没有Spring的AbstractAutowireCapableBeanFactory那种几千行的黑盒。route.java的service()方法往下走,每一个方法调用都有源码,都能打断点,都能说清楚"为什么这么做"。

本章小结

• 框架的全景:route.java唯一入口→BeanFactory IOC→业务层→DBUtil统一ORM→数据库,层层清晰。

• "自洽"意味着出了问题能从入口一步步追踪到数据库,中间没有难以理解的黑盒。

• 十一个核心决策服从同一个原则:在约束中找到最优解,不追求优雅,只追求落地。

• 一个请求从浏览器到数据库经过十个环节,每个环节的类名、方法名都可追踪、可定位。

决策洞察:框架的价值不是功能多,是出了问题你能从入口一步步追踪到数据库,中间没有你看不懂的黑盒。****

**
**

第四章 政策驱动开发——需求锁下的生存法则

本章核心:政策就是需求,生效日期就是截止日期——框架用配置外置应对政策变化。

第一章讲了"五把锁"。这一章展开讲需求锁——不是泛泛地讲"需求会变",而是讲政务需求的一个本质特征:政策就是需求,生效日期就是截止日期。

理解了这个特征,才能理解框架为什么要设计成"可配置"而非"硬编码"。

4.1 政务需求的三个特征

政务需求和企业需求有本质区别。企业需求来自产品经理或业务部门,有协商空间——可以砍需求、可以排优先级、可以推迟上线。政务需求来自政策文件,三个特征决定了它不可协商:

第一,政策就是需求。 不是产品经理拍脑袋想出来的功能,是红头文件规定的业务规则。"从2024年1月1日起,城乡居民基本医疗保险个人缴费标准调整为每人每年380元"——这一句话就是一个需求:系统里的缴费参数必须从350改成380,涉及参保登记、缴费核定、补缴补退等多个模块。

第二,生效日期就是截止日期。 政策说1月1日实施,系统就必须在12月31日之前上线。不是"尽量赶上",是"必须赶上"。因为1月1日之后,全省几百万参保人按新政策办事,系统还是旧逻辑——该收380的还在收350,差的钱谁补?

第三,需求变更是常态不是意外。 社保缴费比例今年调了,明年可能还调。医保报销范围今年扩大了,明年可能会进一步扩大。新农保今年出了新规,明年可能扩展。每一次政策变化对应的就是系统变更。这不是Bug,这是Feature——政务系统就是要不断适配新政策。

这三个特征加在一起,构成了"需求锁"的本质:变更是确定的,时间点是死的,影响面是全省甚至全国的。

4.2 " 用工"字段背后的时代密码

需求锁不只锁住系统,还锁住数据。政务系统的数据字段承载着时代的密码——不理解业务历史,就读不懂数据库设计。

第十六章会讲到接盘老项目时的"猜谜大赛"。这里展开讲YGMC这个字段。

YGMC——你能猜到是"用工名称"吗?不是"员工名称"。YG不是Employee,是用工。而且"用工"这个词本身就很古老——它属于一个已经远去的时代。

2004年,我在北方某市做失业保险系统。翻看数据的时候,我第一次真正理解了什么叫"大下岗"。系统里的数据清清楚楚:那时候的人分三种——固定工(铁饭碗,正式编制)、合同工(签了合同的,到期可能不续)、临时工(随时可以走人的)。三种用工身份,对应的社保待遇、医保报销、养老金计算方式完全不同。

系统里的YGMC字段存的不是一个人的名字,而是这个人属于哪一种"用工制度"。这个字段的设计逻辑来自90年代的用工政策——而那个时代已经过去了,但字段留了下来,数据还在跑。

现在你理解为什么YGMC不是"员工名称"了吧?因为"员工"是互联网时代的说法,"用工"是那个时代的说法。你要是把YGMC理解成"员工名称",后面的业务逻辑你就看不懂了——为什么一个"员工名称"字段会影响养老金计算公式?

不理解业务的历史,就不理解数据的结构。不理解数据的结构,就不理解系统的设计。

大下岗的浪潮、用工制度的变化、社保体系从无到有的建立——这些是后来做政务系统的业务底色。2004年在失业保险系统里看到的那些数据,比任何教科书都生动。

4.3 KC22 不是拼音——部委标准编码

除了拼音首字母字段,政务系统还有另一类"看不懂"的编码:部委标准。

KC22——看起来像拼音?不是。这是人社部(人力资源和社会保障部)的统一标准编码。K表示医疗,C表示个人,22是表序号。KC22就是"医疗费用明细表"。类似的还有KC21(就诊等级信息表)、AB01(参保人员基本信息表)、AC01(个人信息表)、AAC001(个人编号)——每一张表、每一个字段在人社部的标准文档里都有定义。

这意味着什么?意味着政务系统的数据结构不是你设计的,是部委定的。你不需要"设计"数据结构,你需要"理解"数据结构。理解的前提是:你得知道标准文档在哪、怎么看、怎么对照。

新人接手政务项目,最痛苦的不是技术,是业务。技术可以百度,业务百度不到——"KC22是什么意思"这个问题,只有翻人社部标准文档才能回答,而且你得知道该翻哪个文档。

4.4 框架如何应对政策变化——三个实战案例

理解了政务需求的特征,再看框架的决策设计,就能看到一条清晰的主线:让政策变化不需要改代码。

案例一:SM4加解密(第十四章)——信创政策突然来了。

信创(信息技术应用创新)是国家级政策,要求政务系统敏感数据必须加密存储。政策有明确的实施时间节点,各省执行节奏不同。我们用注解驱动+全局开关的设计应对:@myCode标注需要加密的字段,usageCode=1控制是否启用。同系统部署在不同省份,有的省份已经要求加密了就开,有的还没要求就不开。政策来了改配置,不改代码。

案例二:DLL全局开关(第十五章)——跨省异地就医政策落地。

2016年国家推跨省异地就医直接结算。我们高标准达到了国家部委的技术要求,几十家医院同时对接,国家试点的时间节点是死的。INI配置外置差异,每家医院的适配只改一个文本文件。加一家新医院?加一个配置文件。政策要求的工期就是这么赶过来的。

案例三:分库分表反例(第二十一章)——技术驱动,不是政策驱动。

中途接盘了一个政务系统,项目组主动做了分库分表——不是政策要求的,是技术团队觉得"数据量大应该分"。结果呢?统计报表全废了。一天几百条的业务事件表,根本不需要分库分表。这是"技术驱动"而非"政策驱动"的改动——没有明确的场景边界,就容易过度设计。

对比这三个案例:前两个是政策驱动的改动,有明确的场景边界和刚性时间节点,框架的可配置设计让"改配置"替代了"改代码"。第三个是技术驱动的改动,没有边界约束,结果做了不该做的事。

4.5 " 活口"设计——框架层的可配置性

回头看框架里的可配置设计,它们不是独立存在的,而是在应对同一个问题:政策变化时,怎么让改动最小化。

设计位置应对的政策变化
usageCode全局开关SM4加解密(第十四章)不同省份加密要求不同
INI配置文件DLL全局开关(第十五章)不同医院接口差异
level属性MongoDB混合存储(第九章)不同地市性能要求不同
@myCode注解SM4加解密(第十四章)不同字段的加密要求变化
@responseMapping注解路由映射(第五章)新老系统迁移节奏不同

这些设计的共同原则是:把政策差异外置到配置,把业务逻辑固化在代码。

当政策变化时——缴费比例调了?改配置参数,不改代码逻辑。加密范围扩大了?加一个@myCode注解,不改业务方法。新开一家医院?加一个INI文件,不改DLL。

这就是"活口"设计的本质:不是把所有可能的变化都预测到、都写成代码,而是承认变化的不可避免性,然后让变化发生时只需要动配置不需要动代码。

4.6 第二个实战案例:养老金计发办法变更

前面讲了医保报销比例调整。再讲一个影响面更大的案例——养老金计发办法变更。

2020年,某省发布《关于建立城乡居民基本养老保险待遇确定机制和正常调整机制的实施意见》,要求2020年10月底前落实到位。

这个文件改了什么?地方基础养老金的计算公式从固定金额变成了一个与缴费水平、缴费年限挂钩的动态公式:

地方基础养老金=90元+本人各年度缴费指数之和×上年度全省农村居民人均可支配收入×计发系数

其中缴费指数=参保居民当年缴费额÷(上年度全省农村居民人均可支配收入×4.8%),上限为1。计发系数更是逐年变化:2020年0.3‰、2021年0.6‰、2022年0.8‰、2023年1‰、2024年起调整为1.2‰。

还有正常调整机制:每年按省统计部门公布的最新收入数据和年度计发系数重新核定地方基础养老金,差额补发。65岁及以上参保居民在与缴费指数挂钩的部分加发10%。丧葬补助金按死亡当月国家基础养老金最低标准的12倍发放,遗体火化的提高到36倍。

不同人群还有不同的过渡规则:文件印发前已达到待遇领取条件的参保居民,2020年1月1日之前的缴费指数之和均视同为11;文件印发后达到待遇领取条件的,2020年1月1日之前每实际缴费1年,缴费指数视同为1;从城镇职工基本养老保险转入的,累计缴费年限每满1年,缴费指数视同为1。

这个变更对系统意味着什么?原来的地方基础养老金是一个固定数字,查配置表直接返回。现在变成了一个公式,公式里有三个动态参数(缴费指数之和、人均可支配收入、计发系数),每个参数每年都在变。而且涉及待遇核定、养老金发放、待遇调整、丧葬抚恤四个核心模块。如果计算公式是硬编码的,这次变更就是灾难——不是改一个数字,是改一套算法。硬编码意味着四个模块都要改,改漏一处就是有人退休金算错了。

我们的框架怎么应对的?

很简单——数据库里配。计发系数按年度存储在配置表里:2020年0.3‰、2021年0.6‰、2022年0.8‰……政策变了改配置数据,不改代码。缴费指数的计算规则也是配置驱动的:视同缴费指数11、实际缴费指数按年算、转移缴费指数每满一年算1,三种规则各一条配置记录。65岁加发10%?一个配置开关。计算逻辑写在存储过程里,四个核心模块都调同一个存储过程,改一个地方全更新。

这次变更,我们把新增的公式逻辑写进计算服务,配置表插入新的年度计发系数和过渡规则,回归测试用文件里的示例数据验证。一周上线。

回头看,这个案例是五重约束同时生效的典型:文件要求10月底前落实(需求)、退休金算错是重大民生事故(安全)、不能指望升级服务器来提速(环境)、没有额外预算和人力(资源)。框架的"配置外置+计算服务独立"设计,让这次变更的成本从"一个月改四个模块"降到了"一周改一个服务"。

4.7 政策变化的类型与框架应对

做了十多年政务系统,我发现政策变化可以归纳为几种类型,每种类型在框架里有对应的应对模式。这不是事后总结——是在一次次赶工中逐步沉淀出来的。

政策变化类型典型案例框架应对机制改代码?
参数调整型缴费比例350→380、起付线500→600SYS_PARAM配置表否,改数据
开关切换型信创要求加密存储、某省启用新接口usageCode/INI全局开关否,改配置
算法变更型养老金计发办法改革、报销公式调整策略模式+配置表驱动是,加新策略类
流程变更型审批层级从三级改两级、新增会签环节工作流引擎BPMN配置否,改流程定义
数据标准型人社部数据标准升级、字段增减DAO层字段映射+注解部分,加字段映射
对接扩展型跨省异地就医、新医院接入DLL+INI适配、接口抽象层否,加配置文件

你看,六种类型里有四种不需要改代码——改配置或改数据就行。这正是框架设计的核心目标:让最常见的政策变化以最低的成本落地。

算法变更型需要改代码,但改的是"加新策略类",不是"改旧代码"。新策略类写好、配置表配好,旧的策略不动、旧的分支不走——这意味着旧逻辑的行为不会因为新代码的加入而意外改变。回归测试只需要覆盖新策略,不需要重新测试旧逻辑。

这是框架应对政策变化的总原则:增量式修改,非侵入式扩展。 新政策来了,加新代码、加新配置,不动旧代码、不改旧行为。政务系统最怕的就是"牵一发而动全身"——改一个地方,三个模块出Bug。框架的设计目标就是把"牵一发"的影响范围缩到最小。

4.8 决策背后的WHY

为什么不在代码里硬编码政策规则? 因为政策会变。今天380元/年,明年可能400元。硬编码意味着每次政策变化都要改代码、编译、测试、上线——走完整的开发流程。把参数外置到配置表,改配置即时生效,不需要重启。

为什么用注解而不是继承或接口? 因为灵活。一个Dao类里的字段,哪些要加密、哪些不需要,只有业务知道。用注解让每个字段独立声明,不需要为每种加密策略创建一个子类。

一句话:政策驱动开发的本质是——承认变化的不可避免性,然后把变化的代价降到最低。不是"不变",是"变了也不怕"。

需求锁下最务实的策略:不试图预测政策怎么变,而是让系统"变起来成本低"。这是面对不确定性时的核心态度——不求不变,只求好变。

本章小结

• 政务需求的三个特征:政策就是需求、生效日期就是截止日期、需求变更是常态。

• 不理解业务历史就不理解数据结构——YGMC是"用工名称"不是"员工名称",KC22是人社部标准不是拼音。

• 框架的可配置设计(注解、全局开关、INI文件、level属性)都在应对同一个问题:让政策变化只改配置不改代码。

• 跨省异地就医直接结算——政策驱动的极限案例,高标准达到国家部委要求,框架可配置设计是按时交付的关键。

• 政策变化六种类型中四种不需改代码;框架总原则是"增量式修改,非侵入式扩展"。

决策洞察:不试图预测政策怎么变,而是让系统"变起来成本低"。框架的可配置设计不是为了灵活,是为了在政策变的时候不慌。