从传统行业杀进码农圈:我在SpringBoot里和“资源”死磕的那些日子
大家好,我是老张(真名就不说了,怕被前同事认出来)。30岁高龄、去年刚从传统制造业转行做Java开发的“新人”。坐标成都,每天骑共享单车上下班,听着City Pop写代码,生活节奏舒服得有点不像搞IT的。说真的,要不是前公司连ERP系统都还在用VB6,我可能这辈子都不会想到自己会坐在这儿敲SpringBoot。
今天想跟大家聊聊一个让我头秃又成长飞快的经历——关于资源管理在SpringBoot项目中的实战探索。别误会,我说的不是云计算那种“资源”,而是更接地气的东西:文件、数据库连接、线程池、缓存……这些一旦没管好,轻则内存泄漏,重则服务直接崩盘。
起因:产品经理的一句“很简单”
事情发生在上个月,我们团队接了个内部工具重构任务。老系统是外包写的,代码像意大利面条,而且每次上传个Excel文件,服务器内存就蹭蹭涨,重启才能解决。运维小哥已经快把我拉黑了:“老张,你们这服务比我家猫还娇气。”
产品经理拍着胸脯说:“这个需求很简单,就是让用户上传一个CSV,解析后存到库里,再导出一份校验报告。”
我心想:不就是读文件、处理数据、写文件嘛?能有多难?
结果,第一次压测就翻车了。100个并发用户同时上传10MB的CSV,服务直接OOM(OutOfMemoryError),日志里全是:
java.lang.OutOfMemoryError: Java heap space
我当时盯着屏幕,差点把键盘砸了——这才入职半年,就要背锅线上事故了?
问题在哪?“资源”没关!
冷静下来一查,发现罪魁祸首是资源泄漏。
老代码里,读取文件用的是FileInputStream,但根本没调用close();数据库连接用完也没归还;临时生成的报告文件堆积在/tmp目录,几天就占了几十GB。整个系统就像个漏水的桶,越跑越慢,最后直接干裂。
更扎心的是,我一开始居然没意识到这是“资源管理”问题。毕竟以前在工厂做PLC编程,资源?那是什么?有电就行!
但现实狠狠打了我一巴掌:在Java世界里,资源不是“用完就扔”的一次性筷子,而是需要你亲手“送回家”的孩子。
SpringBoot里的资源管理:优雅 vs 地狱
SpringBoot号称“约定优于配置”,但对资源管理这块,它只提供了工具,没替你兜底。你要是不用心,照样掉坑。
坑1:文件流没关,内存直接爆炸
最初我这么写:
public void parseCsv(String filePath) {
FileInputStream fis = new FileInputStream(filePath);
BufferedReader reader = new BufferedReader(new InputStreamReader(fis));
// ... 读取逻辑
}
看起来没问题?错!如果中间抛异常,fis和reader永远不会关闭,文件描述符一直占用,最终导致系统无法打开新文件(Too many open files)。
解决方案:try-with-resources
SpringBoot项目里,一定要用JDK7+的try-with-resources语法:
public void parseCsv(String filePath) {
try (FileInputStream fis = new FileInputStream(filePath);
BufferedReader reader = new BufferedReader(new InputStreamReader(fis))) {
String line;
while ((line = reader.readLine()) != null) {
// 处理每一行
}
} catch (IOException e) {
log.error("解析CSV失败", e);
throw new BusinessException("文件读取异常");
}
}
这样,无论是否抛异常,流都会自动关闭。血泪教训啊兄弟们!
坑2:临时文件堆积如山
我们导出的校验报告是临时文件,原代码直接扔到/tmp:
File report = new File("/tmp/report_" + System.currentTimeMillis() + ".csv");
// ... 写入内容
但没人删!结果三天后,磁盘爆满,服务起不来。
解决方案:主动清理 + 使用NIO.2
现在我改用Files.createTempFile,并配合@PreDestroy或定时任务清理:
@Service
public class ReportService {
private final List<Path> tempFiles = Collections.synchronizedList(new ArrayList<>());
public Path generateReport(List<Data> data) throws IOException {
Path tempFile = Files.createTempFile("validate_report_", ".csv");
tempFiles.add(tempFile); // 记录,方便后续清理
try (BufferedWriter writer = Files.newBufferedWriter(tempFile)) {
// 写入数据
}
return tempFile;
}
@PreDestroy
public void cleanup() {
temp_files.forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
log.warn("清理临时文件失败: {}", path, e);
}
});
}
}
虽然@PreDestroy不能100%保证执行(比如kill -9),但至少优雅停机时能清理。另外,我们还加了个每日凌晨的定时任务,扫一遍/tmp里超过24小时的文件。
坑3:数据库连接池被耗尽
有一次测试环境突然所有接口500,查日志发现:
HikariPool-1 - Connection is not available, request timed out after 30000ms.
原来是某个批量导入接口,开了事务但没控制批次大小,一次查10万条数据,连接一直占着不放。
解决方案:分页 + 显式控制事务边界
@Transactional
public void importBatch(List<Record> records) {
int batchSize = 1000;
for (int i = 0; i < records.size(); i += batchSize) {
List<Record> batch = records.subList(i, Math.min(i + batchSize, records.size()));
processBatch(batch);
// 手动flush and clear if using JPA/Hibernate
entityManager.flush();
entityManager.clear();
}
}
同时,在application.yml里合理配置Hikari连接池:
spring:
datasource:
hikari:
maximum-pool-size: 20
leak-detection-threshold: 60000 # 检测连接泄漏,单位ms
开启leak-detection-threshold后,如果连接借出去超过60秒没还,Hikari会打印堆栈,直接定位到问题代码。
资源监控:别等炸了才看
光靠代码规范不够,还得有监控兜底。我们团队后来做了三件事:
- Prometheus + Grafana 监控文件描述符和内存
- Arthas 在线诊断资源占用
- SonarQube 扫描资源未关闭的代码
比如用Arthas查线程:
thread -n 5 # 看CPU最高的5个线程
thread --state WAITING # 看等待中的线程,可能卡在IO
或者查对象:
sc *FileInputStream # 查类
getstatic java.io.FileInputStream$fd # 不太准,但能辅助判断
虽然有点黑科技,但在救火时真香。
最终效果:从“救火队员”到“架构关注者”
经过这一轮折腾,我们的服务稳定性肉眼可见地提升:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 平均内存占用 | 1.2GB | 600MB |
| 文件描述符峰值 | 8000+ | <500 |
| 月度OOM次数 | 3~5次 | 0次 |
| 临时文件堆积 | 几十GB/周 | <100MB |
最开心的是,上周五上线后,运维小哥居然在群里@我说:“老张,你们这服务最近稳得很啊!” —— 这句话比涨工资还爽。
写给和我一样的“转行新人”
说实话,刚转行那会儿,我对“资源”这种概念很模糊。总觉得SpringBoot自动配置啥都搞定了,自己只要写业务逻辑就行。结果现实教我做人。
但正是这些踩坑经历,让我开始真正关注架构设计和代码质量。现在写代码,我会下意识问自己:
- 这个流关了吗?
- 这个连接会泄漏吗?
- 这个临时文件谁来删?
- 如果并发1000,会不会崩?
这种思维转变,比学会某个框架更重要。
如果你也是半路出家的程序员,别怕犯错。每个OOM背后,都藏着一次成长的机会。就像成都的茶馆,看似悠闲,其实每一片茶叶都经历过高温炒制——代码也一样,只有被Bug锤炼过,才能写出稳健的系统。
对了,我现在边听《Plastic Love》边写这篇博客。窗外玉林路的小酒馆还没打烊,而我的SpringBoot应用,正安静地运行在阿里云上,没有一丝内存波动。
真好。
附:资源管理Checklist(自用版)
- 所有
InputStream/OutputStream用try-with-resources - 临时文件记录路径,程序退出或定时清理
- 数据库操作控制批次大小,避免长事务
- 连接池参数合理配置,开启泄漏检测
- 线程池使用
ThreadPoolTaskExecutor,拒绝策略明确 - 大文件处理用流式(streaming),别全load进内存
- 定期用
jstat、jmap、Arthas检查资源状态
共勉。

评论 0