第 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}/cover | ch12 MVP 验收项 |
| 环境隔离 | application-dev.yml / prod | ch11 生产部署 |
| 配置绑定 | UploadProperties | ch13 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
使用 @ConfigurationProperties 将 upload.* 前缀配置绑定到 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 + "/");
}
}
生产环境通常由 Nginx 或 CDN 提供 /media/,应用只写数据库 URL 字段。
开发:Spring ResourceHandler → ./uploads/
生产:Nginx alias /var/www/shop-spring-demo/media/