文件上传听起来很无聊,直到你花了一下午时间在生产环境中调试 MultipartException: Failed to parse multipart servlet request。
在 Spring Boot 中,文件上传涉及 MultipartFile、MultipartAutoConfiguration、spring.servlet.multipart.* 属性,以及默认运行在 servlet 容器上的假设。它能工作,但包含大量隐藏机制。
Solon 采用了不同的路径。核心类是 UploadedFile,它位于 web 层,无需额外配置,你可以明确控制何时进行 multipart 解析以及何时清理临时文件。
基础:UploadedFile
当控制器方法包含 UploadedFile 参数时,Solon 会自动触发 multipart 解析 —— 无需注解:
@Controller
public class FileController {
@Post
@Mapping("/upload")
public String upload(UploadedFile file) {
try {
file.transferTo(new File("/storage/uploads/" + file.name));
return "uploaded: " + file.name + " (" + file.contentSize + " bytes)";
} finally {
file.delete(); // 始终清理
}
}
}
Enter fullscreen mode Exit fullscreen mode
你最常用的关键属性:
| 属性 | 描述 |
|---|---|
file.name |
完整文件名(含扩展名) |
file.extension |
仅扩展名(如 jpg) |
file.contentType |
MIME 类型 |
file.contentSize |
文件大小(字节) |
file.content |
原始 InputStream
|
file.contentAsBytes |
byte[] |
file.isEmpty() |
上传是否为空 |
值得注意的一点:框架不会自动清理临时文件。如果你跳过 delete(),这些临时文件会留在磁盘上。上面的 try/finally 模式不是可选的。
同名字段的多文件
当客户端在同一字段名下发送多个文件时,使用 UploadedFile[](自 v2.3.8 起支持):
@Post
@Mapping("/upload/batch")
public void uploadBatch(UploadedFile[] files) {
for (UploadedFile file : files) {
try {
file.transferTo(new File("/storage/" + file.name));
} finally {
file.delete();
}
}
}
Enter fullscreen mode Exit fullscreen mode
字段名不匹配时
如果表单字段名与参数名不同,使用 @Param:
@Post
@Mapping("/avatar")
public void setAvatar(@Param("user_avatar") UploadedFile file) {
// 处理表单字段 "user_avatar"
}
Enter fullscreen mode Exit fullscreen mode
混合表单:文件 + 常规字段
你可以在同一请求中同时接收文件和文本字段。参数注入会自动处理两者:
@Post
@Mapping("/upload/with-meta")
public void uploadWithMeta(UploadedFile file, String description, int category) {
try {
// file: 上传的文档
// description, category: 纯文本表单字段
saveFile(file, description, category);
} finally {
file.delete();
}
}
Enter fullscreen mode Exit fullscreen mode
没有 UploadedFile 参数时
有时 multipart 表单只有文本字段 —— 没有文件附件。此时 Solon 不会自动触发 multipart 解析。在映射上使用 multipart = true:
@Post
@Mapping(path = "/submit", multipart = true)
public void submit(String username, int age) {
// 仅含文本字段的 multipart 表单
}
Enter fullscreen mode Exit fullscreen mode
你也可以在需要更多控制时通过 Context 手动访问文件:
@Post
@Mapping(path = "/upload/manual", multipart = true)
public void uploadManual(String username, Context ctx) {
UploadedFile file = ctx.file("attachment");
UploadedFile[] extras = ctx.files("extras");
// 处理...
}
Enter fullscreen mode Exit fullscreen mode
安全:控制 autoMultipart
默认情况下,autoMultipart 为 true —— 任何传入的 multipart 请求都会被自动解析。在面向公众的服务上,这意味着客户端可以向任何端点发送大文件并触发解析开销。
通过路由过滤器收紧控制:
Solon.start(App.class, args, app -> {
app.router().filter(-1, (ctx, chain) -> {
// 仅对上传路径解析 multipart
ctx.autoMultipart(ctx.path().startsWith("/upload"));
chain.doFilter(ctx);
});
});
Enter fullscreen mode Exit fullscreen mode
对于集中式临时文件清理(v2.7.3+),过滤器同样有效:
@Component
public class MultipartCleanupFilter implements Filter {
@Override
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
try {
chain.doFilter(ctx);
} finally {
if (ctx.isMultipartFormData()) {
ctx.filesDelete(); // 清理所有上传的临时文件
}
}
}
}
Enter fullscreen mode Exit fullscreen mode
注意: 如果使用此模式,不要让上传处理器异步执行。过滤器在请求完成时运行 —— 异步处理器可能仍在运行时触发清理。
文件大小配置
# app.yml
server:
request:
maxBodySize: 2mb # 最大请求体(默认:2mb)
maxFileSize: 20mb # 单个文件最大大小
maxHeaderSize: 8kb # 最大头部大小
fileSizeThreshold: 512kb # 低于此值:内存;高于此值:临时文件(v3.6.0+)
Enter fullscreen mode Exit fullscreen mode
fileSizeThreshold 设置(在 v3.6.0 中引入)会自动将小文件路由到内存,大文件路由到磁盘。在 v3.6.0 之前,你可以使用 useTempfile: true 强制临时文件模式 —— 该标志现已弃用。
文件下载
返回文件同样简洁。返回 DownloadedFile 用于字节数组或流,或直接返回 java.io.File:
@Get
@Mapping("/download/report")
public DownloadedFile downloadReport() {
byte[] pdf = reportService.generatePdf();
return new DownloadedFile("application/pdf", pdf, "report.pdf");
}
@Get
@Mapping("/download/avatar/{userId}")
public File downloadAvatar(@Path String userId) {
return new File("/storage/avatars/" + userId + ".jpg");
}
Enter fullscreen mode Exit fullscreen mode
如需更多控制(缓存、内联显示、ETag):
@Get
@Mapping("/preview/logo")
public DownloadedFile previewLogo() {
DownloadedFile file = new DownloadedFile(new File("/assets/logo.png"));
file.asAttachment(false); // 内联显示,不触发下载
file.cacheControl(3600); // 缓存 1 小时(304)
file.eTag("logo-v3");
return file;
}
Enter fullscreen mode Exit fullscreen mode
DownloadedFile 还支持 HTTP Range,因此无需额外配置即可用于视频流和大文件可恢复下载。
无需额外依赖
以上所有内容均在 solon-web 中 —— 无需单独的 multipart 启动器,无需查找自动配置类:
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-web</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode
选择任意服务器适配器(JDK HTTP、Jetty、Undertow、Grizzly、smarthttp)—— 它们都通过 fileSizeThreshold 支持临时文件模式。
心智模型
与 Spring 的对比主要在于显式性:
-
Spring Boot:
MultipartFile由 servlet 层注入;MultipartAutoConfiguration注册一切;spring.servlet.multipart.*配置限制;CommonsMultipartResolver可选。 -
Solon:
UploadedFile参数 → 触发解析;临时文件保留直到你调用delete();autoMultipart提供路径级控制;配置位于server.request.*。
对于大多数项目,差异很小。关键在于当你希望清楚地理解何时发生解析、什么存在于内存与磁盘,以及谁负责清理时。Solon 使这些问题显式化,而不是隐藏在自动配置中。
参考:
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.