我们是如何用AOP、IoC和设计模式拯救祖传代码的
去年接手了一个"祖传"项目,代码写于2015年,经过20多位程序员的"精心呵护",已经进化成了一坨谁都不敢动的意大利面。最夸张的是,有个Service类居然有8000多行,构造函数要传37个参数!
老板说:"这系统太难维护了,但不能推倒重来,你们想办法重构一下。"看着这坨代码,我差点当场辞职。但最终,我们用了3个月时间,通过引入AOP、IoC容器和合理的设计模式,把这个项目从地狱拉回了人间。
噩梦的开始:祖传代码有多可怕
先给大家看看重构前的代码是什么样的:
public class UserService {
private DBConnection dbConnection;
private LogWriter logWriter;
private CacheManager cacheManager;
private EmailSender emailSender;
// ... 还有30多个依赖
public UserService() {
this.dbConnection = new MySQLConnection("localhost", 3306, "root", "123456");
this.logWriter = new FileLogWriter("/var/log/app.log");
this.cacheManager = new RedisCacheManager("localhost", 6379);
this.emailSender = new SMTPEmailSender("smtp.gmail.com", 587);
// ... 初始化其他30多个依赖
}
public User getUserById(String userId) {
// 记录日志
logWriter.write("Getting user: " + userId);
long startTime = System.currentTimeMillis();
try {
// 检查缓存
User cachedUser = (User) cacheManager.get("user_" + userId);
if (cachedUser != null) {
logWriter.write("Cache hit for user: " + userId);
return cachedUser;
}
// 查询数据库
User user = dbConnection.executeQuery("SELECT * FROM users WHERE id = ?", userId);
// 放入缓存
cacheManager.put("user_" + userId, user, 3600);
// 记录性能日志
long endTime = System.currentTimeMillis();
logWriter.write("Get user took: " + (endTime - startTime) + "ms");
return user;
} catch (Exception e) {
logWriter.write("Error getting user: " + e.getMessage());
emailSender.send("admin@company.com", "Error in UserService", e.toString());
throw e;
}
}
// 还有200多个这样的方法...
}
看到这代码,我统计了一下问题:
问题类型 | 具体表现 | 影响范围 | 严重程度 |
---|---|---|---|
强耦合 | 直接new依赖对象 | 所有类 | ★★★★★ |
横切关注点 | 日志/缓存/事务代码重复 | 500+方法 | ★★★★★ |
违反单一职责 | 一个类做所有事 | 核心业务类 | ★★★★☆ |
硬编码配置 | 数据库密码写死 | 全局配置 | ★★★★★ |
无法测试 | 依赖太多mock不了 | 0%测试覆盖率 | ★★★★★ |
控制反转(IoC):解开耦合的第一步
什么是控制反转?
简单说,就是把"创建对象"的控制权交出去。以前是自己new,现在让容器帮你创建和管理。
重构第一步,我们引入了Spring IoC容器:
@Service
public class UserService {
private final UserRepository userRepository;
private final CacheManager cacheManager;
@Autowired
public UserService(UserRepository userRepository, CacheManager cacheManager) {
this.userRepository = userRepository;
this.cacheManager = cacheManager;
}
public User getUserById(String userId) {
return userRepository.findById(userId);
}
}
IoC带来的改变
引入IoC后的效果对比:
指标 | 重构前 | 重构后 | 改善 |
---|---|---|---|
类的依赖数 | 平均37个 | 平均3个 | 91.9% |
代码行数 | 8000行/类 | 200行/类 | 97.5% |
启动时间 | 45秒 | 12秒 | 73.3% |
单元测试编写时间 | 无法测试 | 10分钟/测试 | ∞ |
修改一处的影响范围 | 平均15个类 | 平均2个类 | 86.7% |
依赖注入(DI):IoC的具体实现
三种注入方式对比
我们尝试了不同的依赖注入方式:
注入方式 | 优点 | 缺点 | 使用场景 | 我们的选择 |
---|---|---|---|---|
构造器注入 | 依赖明确、不可变 | 参数多时繁琐 | 必需依赖 | ✓ 主要方式 |
Setter注入 | 灵活、可选依赖 | 可能忘记注入 | 可选依赖 | ✓ 辅助方式 |
字段注入 | 简洁 | 难测试、耦合高 | 原型开发 | ✗ 禁止使用 |
循环依赖问题
使用DI时遇到的最大坑就是循环依赖:
// A依赖B,B依赖A,Spring启动失败!
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB;
}
@Service
public class ServiceB {
@Autowired
private ServiceA serviceA;
}
解决方案和效果:
解决方案 | 实现难度 | 代码侵入性 | 推荐指数 | 实际采用率 |
---|---|---|---|---|
重新设计避免循环 | ★★★★☆ | 无 | ★★★★★ | 70% |
使用@Lazy | ★☆☆☆☆ | 低 | ★★★☆☆ | 20% |
Setter注入 | ★★☆☆☆ | 中 | ★★☆☆☆ | 5% |
引入第三方类 | ★★★☆☆ | 高 | ★★★★☆ | 5% |
面向切面编程(AOP):优雅处理横切关注点
AOP解决了什么问题?
原来的代码里,每个方法都要写日志、处理事务、检查权限,代码重复得令人发指。AOP让我们能够把这些"横切关注点"抽离出来。
实施AOP前后的代码对比:
// Before: 业务逻辑淹没在各种非业务代码中
public User getUserById(String userId) {
// 权限检查
if (!hasPermission(userId)) {
throw new UnauthorizedException();
}
// 记录日志
logger.info("Getting user: " + userId);
long startTime = System.currentTimeMillis();
// 开启事务
Transaction tx = transactionManager.beginTransaction();
try {
User user = userRepository.findById(userId);
tx.commit();
// 性能日志
long endTime = System.currentTimeMillis();
logger.info("Operation took: " + (endTime - startTime) + "ms");
return user;
} catch (Exception e) {
tx.rollback();
logger.error("Error: " + e.getMessage());
throw e;
}
}
// After: 只关注核心业务
@Transactional
@PreAuthorize("hasPermission(#userId)")
@LogExecution
public User getUserById(String userId) {
return userRepository.findById(userId);
}
AOP切面统计
我们创建的主要切面:
切面类型 | 应用范围 | 代码减少量 | 性能影响 | 维护成本降低 |
---|---|---|---|---|
日志切面 | 所有Service方法 | 3000行 | <1ms | 80% |
事务切面 | 数据库操作 | 1500行 | <2ms | 90% |
缓存切面 | 查询方法 | 800行 | -50ms(提升) | 70% |
权限切面 | API接口 | 1200行 | <1ms | 85% |
性能监控切面 | 关键业务 | 500行 | <1ms | 75% |
AOP性能开销
很多人担心AOP的性能开销,我们做了详细测试:
测试场景 | 无AOP | 有AOP | 增加耗时 | 可接受度 |
---|---|---|---|---|
简单方法调用 | 0.01ms | 0.012ms | 20% | ✓ |
数据库查询 | 10ms | 10.1ms | 1% | ✓ |
复杂业务逻辑 | 100ms | 100.5ms | 0.5% | ✓ |
批量操作(1000次) | 1000ms | 1020ms | 2% | ✓ |
结论:AOP的性能开销几乎可以忽略不计。
单例模式:不是所有东西都要new
单例模式的演进
项目里有些重量级对象(如连接池、配置管理器)被到处new,既浪费资源又容易出问题。我们逐步改造成单例:
单例实现方式 | 线程安全 | 性能 | 延迟加载 | 推荐度 |
---|---|---|---|---|
饿汉式 | ✓ | ★★★★★ | ✗ | ★★★☆☆ |
懒汉式(无锁) | ✗ | ★★★★★ | ✓ | ★☆☆☆☆ |
懒汉式(同步方法) | ✓ | ★☆☆☆☆ | ✓ | ★★☆☆☆ |
双重检查锁 | ✓ | ★★★★☆ | ✓ | ★★★★☆ |
静态内部类 | ✓ | ★★★★★ | ✓ | ★★★★★ |
枚举 | ✓ | ★★★★★ | ✗ | ★★★★★ |
Spring中的单例
在Spring容器中,Bean默认就是单例的。我们统计了单例化的效果:
对象类型 | 重构前实例数 | 重构后实例数 | 内存节省 | 初始化时间节省 |
---|---|---|---|---|
数据库连接池 | 50+ | 1 | 98% | 10秒 |
配置管理器 | 100+ | 1 | 99% | 5秒 |
消息发送器 | 200+ | 1 | 99.5% | 8秒 |
缓存客户端 | 30+ | 1 | 97% | 3秒 |
单例模式的坑
但单例也不是万能的,我们踩过的坑:
问题 | 表现 | 原因 | 解决方案 |
---|---|---|---|
内存泄漏 | 内存持续增长 | 单例持有大量数据 | 定期清理 |
并发问题 | 数据错乱 | 单例非线程安全 | 加锁/无状态设计 |
测试困难 | Mock困难 | 全局状态 | 依赖注入 |
集群问题 | 数据不一致 | 每个JVM一个实例 | 分布式缓存 |
综合使用:架构的蜕变
重构后的架构
经过3个月的重构,整个项目架构焕然一新:
原架构:
UserController -> UserService(8000行) -> 直接JDBC
新架构:
UserController
↓ (AOP: 日志、权限)
UserService (业务逻辑)
↓ (AOP: 事务、缓存)
UserRepository (数据访问)
↓
数据库
所有组件通过IoC容器管理,依赖注入
重构效果数据
指标 | 重构前 | 重构后 | 改善 |
---|---|---|---|
代码总行数 | 150,000 | 45,000 | 70% |
平均类大小 | 2000行 | 150行 | 92.5% |
圈复杂度 | 平均25 | 平均5 | 80% |
测试覆盖率 | 0% | 85% | +85% |
发布时间 | 2小时 | 15分钟 | 87.5% |
线上bug数/月 | 50+ | 5 | 90% |
新功能开发时间 | 2周 | 3天 | 78.6% |
团队反馈
更重要的是团队的变化:
方面 | 之前 | 现在 |
---|---|---|
代码可读性 | “看不懂前人写的啥” | “像读文档一样清晰” |
修改信心 | “不敢动,怕改出bug” | “改哪儿影响哪儿一目了然” |
新人上手 | 需要1个月 | 1周即可开始开发 |
加班情况 | 经常通宵改bug | 基本不加班 |
经验总结
1. 渐进式重构
不要试图一次改完,我们的策略:
- 第1个月:引入IoC容器,解耦依赖
- 第2个月:应用AOP,消除重复代码
- 第3个月:优化设计模式,提升代码质量
2. 不要过度设计
设计原则 | 过度使用的后果 | 正确做法 |
---|---|---|
AOP | 到处都是切面,找不到真实代码 | 只在横切关注点使用 |
IoC | 所有对象都注入,启动巨慢 | 合理划分Bean范围 |
设计模式 | 为了模式而模式 | 解决实际问题才用 |
3. 性能不是借口
很多人说"设计模式影响性能",但实测证明:
- 良好的设计带来的维护性提升远大于微小的性能损失
- 很多时候,好的设计反而提升了性能(如缓存切面)
- 真正的性能瓶颈往往在IO,不在这些框架开销
写在最后
这次重构经历让我深刻理解了Martin Fowler的话:“任何傻瓜都能写出计算机能理解的代码,但只有优秀的程序员才能写出人类能理解的代码。”
AOP、IoC、设计模式,这些不是为了炫技,而是为了让代码更容易理解、修改和维护。当你的同事(包括三个月后的自己)能轻松理解并修改你的代码时,这些技术就值了。
最后想说,重构祖传代码确实很痛苦,但看到整个系统焕发新生,团队重拾信心,这种成就感是无法替代的。如果你也在维护祖传代码,不要绝望,制定好计划,一步步来,总能看到曙光的。
记住:优秀的代码是重构出来的,不是一次写成的。
- 点赞
- 收藏
- 关注作者
评论(0)