← 返回 Java 后端知识路线
阶段 04Spring 与后端

上传、拦截器、统一异常与 REST:把横向规则放在正确位置

实现一个文章封面上传边界,串起 MultipartFile、拦截器、RestControllerAdvice 和 REST 错误响应。

第 28 / 35 篇
上传拦截器异常处理RESTMultipartFile

先看这一课值不值得学

学完后,你手里多了哪些代码积木

处理文件、拦截请求,并把异常转换成一致 REST 响应。

本课正式新增

语法 / API / 命令你必须会到什么程度
MultipartFile接收上传文件及其元数据
HandlerInterceptor在 Controller 前后拦截请求
@ControllerAdvice / @ExceptionHandler集中转换异常
@ResponseStatus把业务结果映射到 HTTP 状态

本课只借用,先别硬背

  • 认证过滤器在第 31 课

学完必须能独立写

  • 实现安全上传接口
  • 返回统一错误 JSON 并避免泄露堆栈
本课目录
  1. 1. 现实问题:文件上传成功后,系统仍可能不安全
  2. 2. 最小可运行示例:接收并转译结果
  3. 3. 调用链与对象变化
  4. 4. 为什么这样设计
  5. 5. 项目落点:上传和文章状态分开提交
  6. 6. 易错排查
  7. 7. 一页复习

1. 现实问题:文件上传成功后,系统仍可能不安全

上传接口除了接收字节,还要处理文件名、内容类型、大小、存储位置、权限和失败清理。把这些逻辑塞进 Controller,会让每个接口重复。请求耗时日志、traceId 和登录检查又横跨多个请求,更适合过滤器或拦截器。异常如果由每个方法自己拼 JSON,客户端就无法稳定处理。

我们只做边界:接收 MultipartFile,生成服务端文件名,返回资源 id;不信任原始文件名,不把文件直接放进公开目录,不把异常堆栈返回给用户。

2. 最小可运行示例:接收并转译结果

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;

@RestController
class CoverController {
    @PostMapping(path = "/api/articles/42/cover", consumes = "multipart/form-data")
    UploadResult upload(@RequestPart("file") MultipartFile file) {
        if (file.isEmpty() || file.getSize() > 5 * 1024 * 1024) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "文件为空或超过大小限制");
        }
        String storedKey = "covers/" + java.util.UUID.randomUUID();
        // storage.put(storedKey, file.getInputStream(), file.getContentType());
        return new UploadResult(storedKey, file.getSize());
    }
}

record UploadResult(String key, long size) {}

真实项目应读取内容签名、限制扩展名和解压行为,使用 getOriginalFilename() 只作展示,不能直接拼路径。Controller 示例用 ResponseStatusException 说明结果,统一异常类会更适合大型项目。

3. 调用链与对象变化

Servlet multipart 解析器先把请求边界拆成文件头和临时/内存内容,Controller 参数绑定成 MultipartFile。文件名、大小和 content type 是不可信元数据;Service 校验后生成随机 key,Storage adapter 读取流并写到受控位置,最后返回不包含本地路径的 UploadResult。

拦截器的 preHandle 可读取请求上下文并提前拒绝,postHandle 在 Controller 返回后但视图渲染前执行,afterCompletion 适合清理和记录最终异常。统一异常解析器接住绑定、校验、领域和未知异常,生成稳定的 code/message/traceId JSON。

4. 为什么这样设计

文件字节属于外部资源,存储实现可能是本地磁盘、对象存储或测试内存;Controller 不应依赖具体路径。拦截器适合与 Handler 相关的横向规则,Filter 更早、能覆盖所有 Servlet 请求;认证通常由 Spring Security 处理,不要用一个自写拦截器替代完整安全链。

统一异常让前端只处理一份契约,也能防止泄露堆栈和 SQL。REST 资源响应应使用明确状态码:上传成功 201,参数错误 400,权限不足 403,资源不存在 404,冲突 409。动作接口可以返回资源表示或 operation id,不要所有结果都 200。

5. 项目落点:上传和文章状态分开提交

先上传得到 media key,再在文章 Service 中保存引用;若文章更新失败,要清理孤儿文件或用异步补偿任务。数据库事务无法自动回滚对象存储,所以必须记录状态和重试。文件下载同样做权限检查,公开 URL 和私有签名 URL 分开。

练习:增加 HandlerInterceptor 记录耗时,增加 @RestControllerAdvice 处理 ArticleNotFoundExceptionMethodArgumentNotValidException 和未知异常;写 MockMvc 测试确认上传过大、空文件、未认证和存储失败各返回稳定响应。

6. 易错排查

  • 上传 415:确认 consumes、multipart boundary、字段名和客户端请求方式。
  • 文件名路径穿越:不要直接 resolve 原始文件名,使用随机 key 并固定根目录。
  • 异常返回 HTML:检查异常处理器顺序、Accept、容器错误页和自定义响应体。
  • 拦截器没有覆盖静态或异步请求:确认注册路径和 Filter/Security 的职责边界。

7. 一页复习

上传链是 multipart 解析 → 元数据校验 → 生成存储 key → 写入适配器 → 保存引用;横向请求规则选择 Filter、Interceptor 或 Security;异常统一翻译成稳定 REST 响应。跨文件系统和数据库的成功需要补偿策略,不要假设一个事务能回滚所有外部副作用。

上传对象的状态应包含原始文件名、content type、大小、临时位置、最终 key 和扫描状态,但只有经过校验的 key 才能进入数据库。请求成功返回的是资源标识和大小,不应返回服务器绝对路径;存储失败时临时流和文件要清理,文章引用不能先写成可公开状态。数据库提交和对象存储完成不是一个天然事务。

项目可以把 CoverController 放 web,UploadService 放 application,Storage 端口放 domain/application,LocalStorage 或 S3 实现在 infrastructure;Interceptor 只记录请求上下文,Security 负责身份,Advice 负责错误响应。这样文件大小规则、权限规则、存储路径和响应格式各有拥有者,换存储供应商不需要改 Controller。

排错时 415 查 multipart boundary/字段名/consumes,空文件查解析器和客户端,路径穿越查是否使用原始文件名和最终 normalize 路径,公开了未扫描文件查状态迁移和下载授权;异常变成 HTML 时查 Advice 顺序与 Accept。上传慢要分别记录接收、扫描、写存储和数据库耗时,不要只把接口 timeout 调大。

练习是增加上传文件 hash、大小上限和允许类型白名单,使用临时目录测试成功和失败清理;再模拟数据库写入失败,验证文件进入待清理队列。用 MockMvc 验证错误 code、traceId 和状态码,使用一次真实下载请求验证只有授权用户能读私有对象。

上传的输入状态至少包括 multipart boundary、字段名、原始文件名、媒体类型、长度和字节流;经过校验后才得到内部 storage key;存储成功后得到对象地址和 hash;数据库引用保存后才可以被文章读取。任意一步失败,都要决定临时资源是否删除、文章状态是否回滚以及客户端是否可以安全重试。

拦截器和过滤器的调用位置不同:Filter 可以在 Servlet 参数解析前添加 traceId,multipart 解析后 Controller 才能拿到 MultipartFile,Interceptor 可以围绕 HandlerMethod 记录耗时,Security 过滤器负责身份。把上传权限写在一个普通 Interceptor 里可能绕过其他请求入口,项目应先确定哪条规则属于全局安全链。

文件落点建议是 web/media/CoverControllerapplication/media/UploadServiceapplication/port/Storageinfrastructure/storage/LocalStorage。Service 接收受限的输入和 actor,Storage 只处理字节和 key,数据库 Repository 保存 media record。公开下载再经过一个授权查询,不允许拿到 key 就直接拼本地路径。

排错时 415 查 Content-Type 和 boundary,上传空查客户端字段与解析器,超过大小查网关/Boot/业务三层限制,路径穿越查原始文件名和 normalize 后路径,存储成功但文章无图查数据库引用事务,错误变 HTML 查 Advice/Accept。上传慢要把接收、hash、扫描、写对象和写库分段计时。

练习是加上 hash 去重和 pending 状态:同一个文件重复上传返回已有 media id,扫描失败保持不可公开,数据库保存失败进入清理队列。用 MockMvc 测输入,用临时目录测存储,用真实下载测权限,分别保存响应、文件状态和数据库行,证明外部对象不能靠数据库事务自动回滚。

验证清单:准备合法 multipart、空文件、超限文件、错误类型、路径穿越文件名和未授权下载六组输入,记录 boundary/字段名、校验结果、storage key、文件状态、数据库引用和响应 code。正常上传必须先得到受限 key,再写入对象并保存引用;任意中途失败都要检查临时文件是否删除、文章是否仍为 pending、客户端是否可以安全重试。故意让数据库 insert 失败,再让对象存储写失败,比较补偿队列和清理结果;如果响应返回了服务器绝对路径或原始文件名进入 key,说明边界仍不安全。

复盘调用链时把 Filter、multipart resolver、Interceptor、Controller、Service、Storage 和 Advice 排成顺序:Filter 先建立 trace,解析器把字节变成 MultipartFile,Service 验证元数据并生成内部命令,Storage 返回对象结果,Repository 保存引用,Advice 只翻译错误。下载路径再反向验证 actor、资源状态和读取流,不能因为上传时已经鉴权就跳过私有对象检查。

进阶附录:断点续传与病毒扫描

大文件可用分片和临时 upload session,完成后再合并并校验 hash。生产上传常接入异步病毒扫描和内容审核,文章状态先保持 pending,扫描通过后才公开。所有临时文件都要有过期清理任务和容量指标。

本课按「Spring Boot 4.1 MVC 上传、拦截器与 REST 异常处理」的学习范围组织,正文与示例均为本站原创整理。