作者:TRAE 技术写作团队
日期:2025-10-20
关键词:JavaWeb、文件上传、文件下载、Multipart、Spring Boot、TRAE IDE
引言:为什么文件传输是 Web 的"最后一公里"
在任何业务系统里,文件上传与下载都是"最后一公里"——它直接面向用户,却最容易翻车:
- 前端进度条卡住 99% 不动;
- 后端 Tomcat 报
java.lang.OutOfMemoryError; - 运维凌晨 2 点打电话:"服务器 /tmp 被传满,磁盘 100%!"
本文用"原理 → 代码 → 安全 → 性能 → 实战"五段式,带你一次性把 JavaWeb 文件传输打穿。文末将示范如何用 TRAE IDE 的 AI 编码助手 5 分钟生成一套可上线的上传下载脚手架。
01|HTTP 视角:上传下载到底发生了什么
1.1 上传:把"文件"塞进报文体
浏览器把 <input type="file"> 选中内容按 RFC 7578 打成 multipart/form-data 报文:
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="avatar.jpg"
Content-Type: image/jpeg
<二进制数据>
------WebKitFormBoundary7MA4YWxkTrZu0gW--后端收到后需解析 boundary,把每一段还原成 Part 对象。
1.2 下载:把"文件"塞进响应体
服务端返回 Content-Type + Content-Disposition 告知浏览器"请保存":
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="report.xlsx"
Content-Length: 2048000
<二进制数据>02|前端:三种主流上传姿势
2.1 经典 Form 表单(零 JS)
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" accept=".jpg,.png" required/>
<button type="submit">上传</button>
</form>缺点:整页刷新、无进度、无法断点。
2.2 原生 AJAX + FormData(95% 场景够用)
const file = document.querySelector('input[type=file]').files[0];
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = e => {
const percent = Math.round(e.loaded / e.total * 100);
document.querySelector('#bar').style.width = percent + '%';
};
xhr.open('POST', '/upload');
xhr.send(new FormData(document.querySelector('#form')));2.3 大文件:分片 + SparkMD5 秒传
const chunkSize = 2 * 1024 * 1024; // 2MB
const chunks = Math.ceil(file.size / chunkSize);
const spark = new SparkMD5.ArrayBuffer();
for (let i = 0; i < chunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const blob = file.slice(start, end);
const md5 = await blob.arrayBuffer().then(b => SparkMD5.ArrayBuffer.hash(b));
await fetch('/upload/chunk', {
method: 'POST',
body: JSON.stringify({ index: i, md5, chunk: blob }),
headers: { 'Content-Type': 'application/octet-stream' }
});
}
// 合并请求略03|后端:从 Servlet 到 Spring Boot
3.1 Servlet 3.0 原生写法(无框架)
@WebServlet("/upload")
@MultipartConfig(
maxFileSize = 10 * 1024 * 1024, // 单个文件 10MB
maxRequestSize = 50 * 1024 * 1024, // 整个请求 50MB
fileSizeThreshold = 1 * 1024 * 1024, // 1MB 后写磁盘
location = "/tmp")
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws IOException, ServletException {
Part part = req.getPart("file");
String submitted = part.getSubmittedFileName();
String ext = submitted.substring(submitted.lastIndexOf('.'));
String uuid = UUID.randomUUID().toString();
Path save = Paths.get("/data/file", uuid + ext);
part.write(save.toAbsolutePath().toString());
resp.getWriter().write("ok:" + uuid);
}
}3.2 Spring Boot 极简写法(90% 项目直接抄)
# application.yml
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 50MB
location: ${java.io.tmpdir}
resolve-lazily: true # 大文件异步解析@RestController
@RequestMapping("/api/file")
public class FileController {
@Value("${file.store:/data/file}")
private String storeDir;
@PostMapping("/upload")
public Map<String,String> upload(@RequestParam("file") MultipartFile file) {
String ext = FilenameUtils.getExtension(file.getOriginalFilename());
String name = UUID.randomUUID() + "." + ext;
Path path = Paths.get(storeDir, name);
try {
Files.createDirectories(path.getParent());
file.transferTo(path);
} catch (IOException e) {
throw new RuntimeException("上传失败", e);
}
return Map.of("url", "/api/file/" + name);
}
@GetMapping("/{name:.+}")
public void download(@PathVariable String name,
HttpServletResponse resp) throws IOException {
Path file = Paths.get(storeDir).resolve(name).normalize();
if (!file.startsWith(Paths.get(storeDir))) {
resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
}
resp.setContentType("application/octet-stream");
resp.setHeader("Content-Disposition",
"attachment; filename=\"" + URLEncoder.encode(name, StandardCharsets.UTF_8) + "\"");
Files.copy(file, resp.getOutputStream());
}
}3.3 存储策略:本地、NFS、OSS 怎么选?
| 场景 | 方案 | 优点 | 缺点 |
|---|---|---|---|
| 单体应用、低频 | 本地磁盘 | 零依赖 | 多实例不一致 |
| 多实例共享 | NFS/CephFS | 协议成熟 | 网络瓶颈 |
| 高并发、海量 |