下载工作台
Spring Boot Web 开发

文件上传与配置分环境

试读上半部分 · 解锁后可读全文

第 8 章 · 文件上传与配置分环境

本章目标:在 shop-spring-demo 中实现 MultipartFile 商品封面上传;掌握 Spring Profile 分环境配置(dev / prod);使用 @ConfigurationProperties 绑定类型安全的配置类;理解上传大小限制、存储路径与生产静态资源策略;为 ch09 日志与 ch12 毕业项目打好配置基础。

学时建议:4~5 小时(含 1.5 小时上传跟练)

前置:完成 spring-boot-web ch07(REST 统一响应);ch05 JPA 实体;ch04 Thymeleaf 表单基础。


8.1 本章在 shop-spring-demo 中的位置

shop-spring-demo 横切能力
  ch07 统一响应 Result<T>  →  ch08 上传 + Profile + Properties
  ch09 AOP/拦截器/日志      →  访问日志、耗时统计
能力本章交付后续章节
商品封面POST /api/v1/products/{id}/coverch12 MVP 验收项
环境隔离application-dev.yml / prodch11 生产部署
配置绑定UploadPropertiesch13 DTO 校验
教学项目统一使用 shop-spring-demo、域名示例 https://api.example.com;严禁写入真实数据库密码、内网地址或公司密钥。

8.2 MultipartFile 上传流程

浏览器表单 enctype="multipart/form-data" 或前端 FormData 将文件以 multipart 体提交;Spring MVC 解析为 MultipartFile 接口。

客户端 FormData → DispatcherServlet → MultipartResolver
    → Controller 参数 MultipartFile → FileStorageService 落盘

依赖与启用

Spring Boot Web Starter 已包含 multipart 支持。在 application.yml 中可限制大小:

spring:
  servlet:
    multipart:
      enabled: true
      max-file-size: 5MB
      max-request-size: 10MB
      file-size-threshold: 0
属性说明
max-file-size单文件上限
max-request-size整次请求上限(多文件合计)
file-size-threshold超过阈值写入磁盘临时目录

8.3 配置属性类 UploadProperties

使用 @ConfigurationPropertiesupload.* 前缀配置绑定到 Java 类,避免在业务代码中散落 @Value

@Validated
@ConfigurationProperties(prefix = "upload")
public class UploadProperties {

    @NotBlank
    private String baseDir = "./uploads";

    @Positive
    private long maxBytes = 5 * 1024 * 1024;

    private String allowedExtensions = "jpg,jpeg,png,webp";

    public String getBaseDir() { return baseDir; }
    public void setBaseDir(String baseDir) { this.baseDir = baseDir; }
    public long getMaxBytes() { return maxBytes; }
    public void setMaxBytes(long maxBytes) { this.maxBytes = maxBytes; }
    public String getAllowedExtensions() { return allowedExtensions; }
    public void setAllowedExtensions(String allowedExtensions) {
        this.allowedExtensions = allowedExtensions;
    }
}

在启动类或 @Configuration 类上启用:

@EnableConfigurationProperties(UploadProperties.class)
@SpringBootApplication
public class ShopSpringDemoApplication { ... }

application.yml

upload:
  base-dir: ./uploads
  max-bytes: 5242880
  allowed-extensions: jpg,jpeg,png,webp

Spring Boot 自动将 base-dir 映射到 baseDir(松散绑定)。


8.4 FileStorageService 实现

@Service
public class FileStorageService {

    private final Path root;
    private final UploadProperties props;

    public FileStorageService(UploadProperties props) throws IOException {
        this.props = props;
        this.root = Paths.get(props.getBaseDir()).toAbsolutePath().normalize();
        Files.createDirectories(root);
    }

    public String store(MultipartFile file, String subDir) throws IOException {
        if (file.isEmpty()) {
            throw new IllegalArgumentException("上传文件为空");
        }
        if (file.getSize() > props.getMaxBytes()) {
            throw new IllegalArgumentException("文件超过大小限制");
        }
        String ext = StringUtils.getFilenameExtension(file.getOriginalFilename());
        if (ext == null || !allowed().contains(ext.toLowerCase())) {
            throw new IllegalArgumentException("不允许的文件类型");
        }
        String filename = UUID.randomUUID() + "." + ext;
        Path targetDir = root.resolve(subDir).normalize();
        Files.createDirectories(targetDir);
        Path target = targetDir.resolve(filename);
        Files.copy(file.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);
        return subDir + "/" + filename;
    }

    private Set<String> allowed() {
        return Set.of(props.getAllowedExtensions().split(","));
    }
}
步骤说明
校验空文件isEmpty()
校验大小UploadProperties 一致
校验扩展名白名单,防 .exe 伪装
随机文件名避免覆盖与路径遍历
normalize()防止 ../ 路径穿越

8.5 REST 上传接口

@RestController
@RequestMapping("/api/v1/products")
public class ProductCoverController {

    private final FileStorageService storage;
    private final ProductCatalogService catalogService;

    @PostMapping("/{id}/cover")
    @PreAuthorize("hasRole('STAFF')")
    public Result<String> uploadCover(@PathVariable Long id,
                                      @RequestParam("file") MultipartFile file) throws Exception {
        String relative = storage.store(file, "products");
        String url = "/media/" + relative;
        catalogService.updateCoverUrl(id, url);
        return Result.ok(url);
    }
}

curl 测试

curl -X POST http://127.0.0.1:8080/api/v1/products/1/cover \
  -H "Cookie: JSESSIONID=<session>" \
  -F "file=@./sample-cover.jpg"

期望:{"code":0,"message":"success","data":"/media/products/uuid.jpg"}


8.6 静态资源映射 media

开发环境将本地上传目录映射为 URL 前缀:

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Value("${upload.base-dir}")
    private String uploadBaseDir;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        String location = Paths.get(uploadBaseDir).toAbsolutePath().toUri().toString();
        registry.addResourceHandler("/media/**")
                .addResourceLocations(location.endsWith("/") ? location : location + "/");
    }
}

生产环境通常由 NginxCDN 提供 /media/,应用只写数据库 URL 字段。

开发:Spring ResourceHandler → ./uploads/
生产:Nginx alias /var/www/shop-spring-demo/media/

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。