从‘修修补补’到‘推倒重来’:一个Java工程师亲历的遗留系统改造全记录(含踩坑心得)
2026/5/6 0:36:55 网站建设 项目流程

从‘修修补补’到‘推倒重来’:一个Java工程师亲历的遗留系统改造全记录(含踩坑心得)

那天下午,CTO把我叫进会议室,指着屏幕上那个运行了12年的老系统说:"这个审批系统现在每天要处理3000多笔业务,但上周又因为EJB连接池爆满瘫痪了两次。客户投诉电话已经打到董事会了——我们需要一个能撑到2030年的方案。"作为团队里唯一接触过Spring Cloud的人,我接下了这个烫手山芋。没想到,这次改造就像给一架飞行中的飞机换引擎,稍有不慎就是系统级事故。下面分享这段从技术债泥潭里爬出来的真实经历,包含那些教科书不会写的实战细节。

1. 技术评估:改造还是重写?

面对一个用EJB 2.0+Oracle Forms构建的庞然大物,我们花了三周时间做了深度体检。关键发现:

  • 数据库层:287张表,其中40%没有主键约束,存储过程包含超过5000行PL/SQL代码
  • 服务层:EJB的远程调用平均响应时间达800ms,事务隔离级别全部是SERIALIZABLE
  • 前端:Oracle Forms生成的界面需要IE兼容模式才能运行

改造可行性矩阵

评估维度改造方案重写方案
开发周期6-8个月(渐进式)12个月+
风险等级中(需保证接口兼容)高(全新代码)
团队适应成本低(逐步替换)高(全新技术栈)
长期收益中等(技术债部分清除)高(彻底现代化)

最终选择渐进式改造路线,核心考量是业务连续性要求——系统停摆超过4小时就会触发监管报备。我们划定了三个改造阶段:

  1. 数据层解耦:将Oracle迁移到PostgreSQL,建立数据同步通道
  2. 服务层重构:用Spring Boot逐步替换EJB组件
  3. 前端现代化:Vue.js渐进替换Oracle Forms

关键教训:不要迷信"彻底重写"的诱惑,特别是当系统承载着关键业务流程时。我们后来发现某些存储过程包含未经文档化的业务规则,直接重写会导致合规风险。

2. 数据迁移:那些教科书没教的坑

迁移287张表听起来只是ETL工具配置的问题?实际执行时我们遇到了这些意外情况:

2.1 数据类型的地雷

Oracle的NUMBER类型在PostgreSQL中对应NUMERIC,但处理超大数字时表现迥异。某个审批流水号字段在Oracle中存储为NUMBER(38),迁移后超过PostgreSQL的NUMERIC(1000)精度限制。解决方案:

-- 最终采用的类型映射方案 CREATE DOMAIN oracle_number AS NUMERIC(1000);

2.2 隐式转换的代价

老系统中大量存在这样的SQL:

SELECT * FROM approvals WHERE request_id = '10086' -- request_id实际是数字类型

在Oracle中会自动转换,但PostgreSQL会直接报错。我们开发了静态分析工具扫描所有SQL:

// 使用ANTLR解析SQL语句检测类型不匹配 public class TypeChecker extends SQLBaseListener { @Override public void exitComparisonPredicate(SQLParser.ComparisonPredicateContext ctx) { String leftType = getExpressionType(ctx.left); String rightType = getExpressionType(ctx.right); if (!isCompatible(leftType, rightType)) { logWarning("Type mismatch at line " + ctx.start.getLine()); } } }

2.3 数据一致性验证方案

为确保迁移后的数据准确,我们设计了双写比对机制:

  1. 在Oracle和PostgreSQL之间建立CDC通道
  2. 关键表启用触发器记录变更
  3. 开发比对工具检查差异:
def verify_table(source_cursor, target_cursor, table_name): source_cursor.execute(f"SELECT checksum(*) FROM {table_name}") target_cursor.execute(f"SELECT checksum(*) FROM {table_name}") return source_cursor.fetchone() == target_cursor.fetchone()

3. 服务层改造:从EJB到Spring Cloud的渐进之路

3.1 接口兼容层设计

为了让新老系统共存,我们开发了EJB到REST的适配器:

@Stateless public class ApprovalServiceAdapter implements ApprovalServiceRemote { @Inject private RestTemplate restTemplate; public ApprovalResult submit(ApprovalRequest request) { // 将EJB调用转换为REST调用 return restTemplate.postForObject( "http://new-system/approvals", convertToDTO(request), ApprovalResult.class); } }

3.2 事务边界难题

原系统使用EJB的容器管理事务(CMT),新服务采用Spring的@Transactional。混合环境下的解决方案:

  1. 对于跨系统操作,引入Saga模式
  2. 关键业务流添加补偿机制:
public void approveOrder(Long orderId) { try { sagaCoordinator.startSaga() .step(() -> legacySystem.approve(orderId)) .step(() -> newSystem.updateInventory(orderId)) .withCompensation(() -> legacySystem.reject(orderId)) .execute(); } catch (SagaException e) { metrics.counter("saga_failure").increment(); } }

3.3 性能优化实战

监控发现某些审批流响应时间从原来的2秒恶化到8秒。通过Arthas跟踪发现是XML序列化瓶颈:

# Arthas诊断命令 watch com.example.Converter convertToXML '{params, returnObj}' -x 3

最终采用Protocol Buffers替代XML,吞吐量提升4倍:

message Approval { int64 id = 1; string requester = 2; repeated string approvers = 3; Status status = 4; enum Status { PENDING = 0; APPROVED = 1; REJECTED = 2; } }

4. 灰度发布与监控体系建设

4.1 流量切换方案

采用DNS权重分流+业务标识路由的双层控制:

  1. 初期5%流量导向新系统
  2. 特定测试客户强制路由到新环境
  3. 关键指标对比看板:
指标旧系统新系统允许偏差
成功率99.95%99.93%±0.1%
P99延迟1200ms800ms+20%
并发处理能力500TPS2000TPS-

4.2 全链路追踪改造

为定位跨系统问题,我们整合了老系统的日志表和新系统的SkyWalking:

-- 改造老系统日志表加入trace_id ALTER TABLE system_log ADD trace_id VARCHAR(32); UPDATE system_log SET trace_id = SYS_GUID() WHERE trace_id IS NULL;

对应的日志关联查询:

public List<LogEntry> getRelatedLogs(String traceId) { return jdbcTemplate.query( "SELECT * FROM (SELECT * FROM legacy_logs WHERE trace_id=? UNION ALL " + "SELECT * FROM new_logs WHERE trace_id=?) ORDER BY timestamp", new LogEntryMapper(), traceId, traceId); }

5. 那些值得记录的踩坑瞬间

  • 日期处理陷阱:老系统使用Oracle的TO_DATE时默认格式是DD-MON-YY,而Java的SimpleDateFormat解析"01-JAN-00"会变成2000年而非1900年。解决方案:

    DateTimeFormatter formatter = new DateTimeFormatterBuilder() .appendPattern("dd-MMM-") .appendValueReduced(ChronoField.YEAR, 2, 2, 1900) .toFormatter(Locale.ENGLISH);
  • 魔法数字之谜:审批状态字段里,数字5代表"已撤回",但在任何文档中都找不到依据。后来在某个2008年的邮件存档里发现这是当时为某跨国客户做的特殊定制。

  • 线程池饥饿:老系统的EJB线程池默认大小是50,而新系统的Tomcat线程池是200。当突发流量到来时,老系统成为瓶颈。最终通过Hystrix做了熔断配置:

    hystrix.threadpool.legacy.coreSize=50 hystrix.command.legacyCommand.execution.isolation.thread.timeoutInMilliseconds=3000

这次改造历时9个月,最终新系统在零业务中断的情况下完全替换了老系统。技术指标上,平均响应时间从1.2秒降到300毫秒,服务器成本降低60%。但最大的收获是那些无法量化的经验——如何在不完美的条件下做出最优权衡,这或许是处理遗留系统最珍贵的技能。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询