# 最野AOP实现:他连AOP这个词都没听过
最野AOP实现他连AOP这个词都没听过非科班野生程序员深耕政务信息化20年。2008年接了个某市社保三级审核的需求。不知道AOP是什么概念但用PowerBuilder干了一件现在回头看就是AOP的事——不改老业务代码一行在DataWindow的Update环节把数据切走审核完了再写回来。这篇文章讲的就是这个故事。最后感谢豆包、智谱、OpenCode决策是我做的代码是我搓的文字是他们总结的。背景不改代码加审核2008年某市社保系统要上三级审核。老系统几百个窗口、上千个DataWindow业务逻辑都在里面。正常做法是一个一个窗口改——每个窗口加审核状态字段、加审核流程、加审核页面。这样干不仅成本高得吓人更致命的是质量差——几百个窗口逐个改改漏一个就是线上事故。翻代码翻不起。改业务表改不起。更关键的是逐个改质量没法保证。唯一的活路不改老业务系统的一行代码从外面切进去。PowerBuilder的DataWindow先交代一下PB的DataWindow。PB的DataWindow是整个框架的核心——一个可视控件绑定一条SQL自动渲染成表格/表单用户编辑完后调dw_1.Update()PB自动根据行的状态生成INSERT/UPDATE/DELETE语句直接提交到数据库。用户编辑 → DataWindow → dw_1.Update() → 数据库行状态是PB自动维护的New!— 新插入的行DataModified!— 修改过的行删除缓冲区— 用户删除的行调Update()时PB自动根据状态生成对应的SQL。我的做法劫持Update关键一步重写uo_datawindow的update事件不调用父类的Update()而是自己遍历行状态拼SQL。用户编辑 → DataWindow → update_user()我重写的→ 拼SQL → 写到审核中间表核心代码逻辑public function integer update_user(string as_exsql); // 获取表名 ls_Table this.Describe(DataWindow.Table.UpdateTable) // 获取列信息 ll_ColumnsCount long(this.Describe(datawindow.Column.Count)) for ll_i 1 to ll_ColumnsCount // 只处理标记为update的列 ls_temp this.Describe(#String(ll_i).update) if lower(ls_temp) yes then ls_ColumnName[ll_m] this.Describe(#String(ll_i).name) ls_ColumnType[ll_m] this.Describe(ls_ColumnName[ll_m].coltype) ll_ColumnRow[ll_m] ll_i end if // 记录主键列 ls_temp this.Describe(#String(ll_i).key) if lower(ls_temp) yes then ls_Key[ll_n] this.Describe(#String(ll_i).name) ls_KeyType[ll_n] this.Describe(ls_Key[ll_n].coltype) ll_KeyRow[ll_n] ll_i end if next然后按状态拼SQL// 遍历每一行根据状态拼不同的SQL for ll_i 1 to ll_RowCount l_status this.GetItemStatus(ll_i, 0, Primary!) if l_status New! or l_status NewModified! then // 新增 → 拼 INSERT INTO ... VALUES (...) ls_sql ls_sql Insert into ls_Table ( // ...遍历列拼列名和值 // 按类型处理string加引号number直接写date用to_date() else if l_status DataModified! then // 修改 → 拼 UPDATE ... SET ... WHERE ... // 只更新变更过的列 for ll_j 1 to UpperBound(ll_ColumnRow) l_status_Modified[ll_j] this.GetItemStatus(ll_i, ls_ColumnName[ll_j], Primary!) next // 只拼状态为DataModified!的列 end if next // 删除区的行 → 拼 DELETE FROM ... WHERE ... for ll_i 1 to ll_deleteRowCount ls_sql ls_sql Delete from ls_Table where // 用主键拼WHERE条件 next最后用PL/SQL块包起来批量执行// 每 il_pageCount 条拼一个 begin ... end; 块 for ll_i 1 to ll_times lsa_sql[ll_i] begin ~r~n for ll_j 1 to ll_a lsa_sql[ll_i] lsa_sql[ll_i] ids1.object.a[(ll_i-1)*il_Countll_j] next lsa_sql[ll_i] lsa_sql[ll_i] end ;~r~n next this.resetupdate() // 重置行状态 return 1切到哪里去数据不写到原来的业务表而是写到审核中间表。审核中间表的结构和业务表一模一样外加几个审核字段审核状态待审核 / 已通过 / 已退回审核级别一级 / 二级 / 三级审核人、审核时间、审核意见业务表叫kc22审核表叫kc22_audit。update_user()把SQL里的表名从kc22替换成kc22_audit数据就切到了审核表。审核通过后再从审核表写回业务表。老业务代码一行没改。现在回头看这就是AOPAOP概念我的实现切面Aspectuo_datawindow重写的update事件切入点Join Pointdw_1.Update()这个调用通知Advice拦截后拼SQL写到审核表而不是原表织入Weave继承uo_datawindow所有窗口用子类代替父类不碰业务逻辑在保存这个动作的前后插入自己的逻辑。这就是AOP。但2008年我不知道AOP是什么。我就是觉得改老代码太贵了我付不起那个成本只能从外面切。后来的影响这个决定影响了我后面十几年的框架设计。Row的状态机PB版用GetItemStatus()检查行状态是PB内置的。后来做Java版PB不在了我就自己实现状态机// Java版 Row.java — 2008年PB思路的延续publicvoidsetItemValue(Objectkey,Objectvalue){if(map.get(key)null){if(_t0)_t1;// 新增}else{if(_t0){_t3;// 修改_o.put(key,map.get(key));// 保存原始值}}map.put(key.toString(),value);}_t0无变化、_t1新增、_t3修改——和PB的NotModified!、New!、DataModified!一一对应。_o存原始值对应PB里 DataModified 时能拿到旧值的能力。RowSet的三区ListRowprimarynewArrayList();// 主缓冲区正常数据ListRowdeletenewArrayList();// 删除缓冲区ListRowfilternewArrayList();// 过滤缓冲区PB的DataWindow有三个缓冲区Primary、Delete、Filter。Java版用三个List模拟。完全对应。DataStore的 save/update/insert/deleteJava版的DBUtil.updateWithDataStore()、insertWithDataStore()、deleteWithDataStore()遍历RowSet根据每行的状态自动生成INSERT/UPDATE/DELETE——和2008年PB版update_user()的逻辑一模一样。算不算原创在Update环节拦截数据变更这个想法PB社区里有人做过类似的。但把行状态遍历、SQL自动拼接、审核中间表切走、审核通过后写回这一整套做下来而且不改老业务代码一行——至少在我当时的圈子里没人这么干过。更重要的是这个思路后来成了我整个框架的基因。从PB到Java到C#到JavaScriptDataStore/RowSet/Row这套东西一脉相承根都在2008年那个含着泪干的项目里。小结有时候最好的架构决策不是因为学了什么理论而是因为付不起改代码的成本。几百个窗口不能动。逼出了不改代码加审核的方案逼出了自研状态机逼出了DataStore体系逼出了后来十几年一脉相承的框架设计。2014年接某海关C#项目把这套东西从Java搬到C#看起来是狂想。其实根在2008年就已经埋下了。你见过最野的AOP实现是什么评论区聊聊。标签#AOP #PowerBuilder #DataWindow #政务信息化 #状态机 #自研框架 #社保 #三级审核 #拦截器