Solon File Upload: UploadedFile Over MultipartFile

Solon File Upload: UploadedFile Over MultipartFile

File upload sounds boring until you’ve spent an afternoon debugging MultipartException: Failed to parse multipart servlet request in production. In Spring Boot, file upload involves MultipartFile, MultipartAutoConfiguration, spring.servlet.multipart.* properties, and a silent assumption that you’re running on a servlet container. It works, but it carries a lot of hidden machinery.

文件上传听起来很无聊,直到你花了一个下午在生产环境中调试 MultipartException: Failed to parse multipart servlet request。在 Spring Boot 中,文件上传涉及 MultipartFileMultipartAutoConfigurationspring.servlet.multipart.* 属性,以及一个默认你运行在 Servlet 容器中的隐性假设。它确实能用,但背后隐藏了大量的复杂机制。

Solon takes a different path. The core class is UploadedFile, it lives in the web layer with no extra configuration needed, and you have explicit control over when multipart parsing happens and when temporary files get cleaned up.

Solon 走了一条不同的路径。其核心类是 UploadedFile,它存在于 Web 层,无需额外配置,并且你可以显式控制何时进行 multipart 解析以及何时清理临时文件。

The Basics: UploadedFile

基础:UploadedFile

When a controller method includes an UploadedFile parameter, Solon automatically triggers multipart parsing — no annotation required:

当控制器方法包含 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(); // always clean up
        }
    }
}

Key properties you’ll use most: 你最常使用的关键属性:

PropertyDescription描述
file.nameFull filename including extension包含扩展名的完整文件名
file.extensionExtension only (e.g. jpg)仅扩展名(例如 jpg)
file.contentTypeMIME typeMIME 类型
file.contentSizeFile size in bytes文件大小(字节)
file.contentRaw InputStream原始输入流
file.contentAsBytesbyte[]字节数组
file.isEmpty()Whether the upload is empty上传是否为空

One thing worth noting: the framework does not auto-clean temporary files. If you skip delete(), those temp files stay on disk. The try/finally pattern above isn’t optional.

值得注意的一点是:框架不会自动清理临时文件。如果你跳过 delete(),这些临时文件会留在磁盘上。上面的 try/finally 模式是必须的。

Multiple Files, Same Field Name

多文件,相同字段名

Use UploadedFile[] when the client sends multiple files under the same field name (supported since v2.3.8):

当客户端在同一个字段名下发送多个文件时,请使用 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();
        }
    }
}

When the Field Name Doesn’t Match

当字段名不匹配时

If the form field name differs from your parameter name, use @Param:

如果表单字段名与你的参数名不同,请使用 @Param

@Post
@Mapping("/avatar")
public void setAvatar(@Param("user_avatar") UploadedFile file) {
    // handles form field "user_avatar"
    // 处理表单字段 "user_avatar"
}

Mixed Forms: Files + Regular Fields

混合表单:文件 + 普通字段

You can receive both file and text fields in the same request. Parameter injection handles both automatically:

你可以在同一个请求中同时接收文件和文本字段。参数注入会自动处理两者:

@Post
@Mapping("/upload/with-meta")
public void uploadWithMeta(UploadedFile file, String description, int category) {
    try {
        // file: the uploaded document
        // description, category: plain text form fields
        // file: 上传的文档
        // description, category: 普通文本表单字段
        saveFile(file, description, category);
    } finally {
        file.delete();
    }
}

When There’s No UploadedFile Parameter

当没有 UploadedFile 参数时

Sometimes a multipart form has only text fields — no file attachment. In that case Solon won’t trigger multipart parsing automatically. Use multipart = true on the mapping:

有时 multipart 表单只有文本字段,没有文件附件。在这种情况下,Solon 不会自动触发 multipart 解析。请在映射中使用 multipart = true

@Post
@Mapping(path = "/submit", multipart = true)
public void submit(String username, int age) {
    // multipart form with text fields only
    // 仅包含文本字段的 multipart 表单
}

You can also access files manually via Context when you need more control:

当你需要更多控制权时,也可以通过 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");
    // process...
}

Security: Controlling autoMultipart

安全性:控制 autoMultipart

By default, autoMultipart is true — any incoming multipart request will be parsed automatically. On a public-facing service, this means a client can send a large file to any endpoint and trigger parsing overhead. Tighten this up with a router filter:

默认情况下,autoMultipart 为 true,任何传入的 multipart 请求都会被自动解析。在面向公众的服务中,这意味着客户端可以向任何端点发送大文件并触发解析开销。使用路由过滤器来收紧此限制:

Solon.start(App.class, args, app -> {
    app.router().filter(-1, (ctx, chain) -> {
        // only parse multipart on upload paths
        // 仅在上传路径上解析 multipart
        ctx.autoMultipart(ctx.path().startsWith("/upload"));
        chain.doFilter(ctx);
    });
});

For centralized temp-file cleanup (v2.7.3+), a filter also works well:

对于集中式的临时文件清理(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(); // cleans all uploaded temp files
            }
        }
    }
}

Note: If you use this pattern, don’t make your upload handlers async. The filter runs on request completion — async handlers may still be working when cleanup fires.

注意:如果你使用这种模式,请不要将上传处理器设为异步。过滤器在请求完成时运行,而异步处理器在清理触发时可能仍在工作。

File Size Configuration

文件大小配置

# app.yml
server:
  request:
    maxBodySize: 2mb      # max request body (default: 2mb) / 最大请求体(默认 2mb)
    maxFileSize: 20mb     # max single file size / 最大单文件大小
    maxHeaderSize: 8kb    # max header size / 最大头部大小
    fileSizeThreshold: 512kb # below this: memory; above: temp file (v3.6.0+) / 低于此值:内存;高于此值:临时文件

The fileSizeThreshold setting (introduced in v3.6.0) automatically routes small files to memory and large files to disk. Before v3.6.0, you had useTempfile: true for forced temp-file mode — that flag is now deprecated.

fileSizeThreshold 设置(v3.6.0 引入)会自动将小文件路由到内存,将大文件路由到磁盘。在 v3.6.0 之前,你需要使用 useTempfile: true 来强制进入临时文件模式,该标志现已弃用。

Downloading Files

文件下载

Returning files is equally clean. Return DownloadedFile for byte arrays or streams, or just return a java.io.File directly:

返回文件同样简洁。对于字节数组或流,返回 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");
}

For more control — caching, inline display, ETags: 如需更多控制(缓存、内联显示、ETags):

@Get
@Mapping("/preview/logo")
public DownloadedFile previewLogo() {
    DownloadedFile file = new DownloadedFile(new File("/assets/logo.png"));
    file.asAttachment(false); // display inline, don't trigger download / 内联显示,不触发下载
    file.cacheControl(3600);  // 304 cache for 1 hour / 缓存 1 小时
    file.eTag("logo-v3");
    return file;
}

DownloadedFile also supports HTTP Range, so it works for video streaming and large file resumable downloads without any extra configuration.

DownloadedFile 还支持 HTTP Range,因此无需任何额外配置即可用于视频流和大型文件的断点续传。

No Extra Dependencies

无额外依赖

Everything above is in solon-web — no separate multipart starter, no auto-configuration class to hunt down:

以上所有内容都在 solon-web 中,无需单独的 multipart 启动器,也无需寻找自动配置类:

<dependency>
    <groupId>org.noear</groupId>
    <artifactId>solon-web</artifactId>
</dependency>

Pick any server adapter (JDK HTTP, Jetty, Undertow, Grizzly, smarthttp) — they all support temp-file mode via fileSizeThreshold.

选择任何服务器适配器(JDK HTTP, Jetty, Undertow, Grizzly, smarthttp)——它们都通过 fileSizeThreshold 支持临时文件模式。