图片上传与多图展示——Spring Boot 文件上传实战
部分效果截图

一、引言:图片在二手交易中的重要性
在二手交易平台中,图片是商品的"脸面"。一张清晰、真实的商品图片能极大地提高商品的曝光率和成交率。用户在浏览商品时,首先看到的就是图片,图片质量直接影响用户的购买决策。
作为商品模块的负责人,我需要实现以下图片相关功能:
- 商品图片上传(支持本地图片上传);
- 图片存储与访问;
- 图片格式校验;
- 图片展示与预览;
- 头像上传功能(与用户模块联动)。
本文将详细讲解如何在 Spring Boot 项目中实现完整的文件上传功能,并解决开发过程中遇到的各种问题。
二、文件上传的基本原理
2.1 HTTP 文件上传机制
文件上传使用 HTTP 的 multipart/form-data 格式,这是一种专门用于传输二进制数据的编码方式。
上传流程:
- 前端通过表单或 AJAX 将文件数据发送到后端;
- 后端接收
MultipartFile对象; - 后端将文件保存到磁盘或云存储;
- 后端返回文件的访问 URL。
2.2 Spring Boot 文件上传配置
在 application.yml 中配置文件上传参数:
spring:
servlet:
multipart:
enabled: true
max-file-size: 10MB
max-request-size: 10MB
配置说明:
enabled: true:启用文件上传功能;max-file-size: 10MB:单个文件最大大小;max-request-size: 10MB:整个请求最大大小。
三、UploadController:文件上传接口实现
3.1 UploadController 完整代码
package com.hlw.controller;
import com.hlw.common.Result;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
@RestController
@RequestMapping("/upload")
public class UploadController {
@PostMapping("/image")
public Result uploadImage(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return Result.fail("请选择要上传的文件");
}
String originalFilename = file.getOriginalFilename();
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
String allowedTypes = ".jpg,.jpeg,.png,.gif,.webp";
if (!allowedTypes.contains(extension.toLowerCase())) {
return Result.fail("只支持jpg、jpeg、png、gif、webp格式的图片");
}
String filename = UUID.randomUUID().toString() + extension;
String uploadDir = System.getProperty("user.dir") + "/uploads/";
try {
Path path = Paths.get(uploadDir);
if (!Files.exists(path)) {
Files.createDirectories(path);
}
File dest = new File(uploadDir + filename);
file.transferTo(dest);
String url = "/uploads/" + filename;
return Result.success(url);
} catch (IOException e) {
return Result.fail("文件上传失败:" + e.getMessage());
}
}
@PostMapping("/avatar")
public Result uploadAvatar(@RequestParam("file") MultipartFile file) {
return uploadImage(file);
}
}
3.2 接口详解
3.2.1 图片上传接口(POST /upload/image)
@PostMapping("/image")
public Result uploadImage(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return Result.fail("请选择要上传的文件");
}
String originalFilename = file.getOriginalFilename();
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
String allowedTypes = ".jpg,.jpeg,.png,.gif,.webp";
if (!allowedTypes.contains(extension.toLowerCase())) {
return Result.fail("只支持jpg、jpeg、png、gif、webp格式的图片");
}
String filename = UUID.randomUUID().toString() + extension;
String uploadDir = System.getProperty("user.dir") + "/uploads/";
try {
Path path = Paths.get(uploadDir);
if (!Files.exists(path)) {
Files.createDirectories(path);
}
File dest = new File(uploadDir + filename);
file.transferTo(dest);
String url = "/uploads/" + filename;
return Result.success(url);
} catch (IOException e) {
return Result.fail("文件上传失败:" + e.getMessage());
}
}
处理流程:
- 文件判空:检查上传文件是否为空;
- 获取文件扩展名:从原始文件名中提取扩展名;
- 格式校验:只允许 jpg、jpeg、png、gif、webp 格式;
- UUID 重命名:使用 UUID 避免文件名冲突;
- 创建目录:检查并创建 uploads 目录;
- 保存文件:将文件写入磁盘;
- 返回 URL:返回文件的访问路径。
参数说明:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
file | MultipartFile | 是 | 要上传的图片文件 |
返回结果:
{
"code": 200,
"msg": "操作成功",
"data": "/uploads/xxx.jpg"
}
3.2.2 头像上传接口(POST /upload/avatar)
@PostMapping("/avatar")
public Result uploadAvatar(@RequestParam("file") MultipartFile file) {
return uploadImage(file);
}
设计意图:
- 头像上传和商品图片上传共用同一个逻辑;
- 通过不同的 URL 区分用途;
- 便于后续扩展不同的处理逻辑。
3.3 关键代码解析
解析1:UUID 重命名
String filename = UUID.randomUUID().toString() + extension;
为什么要用 UUID?
- 用户上传的文件名可能相同;
- 直接使用原文件名会导致文件覆盖;
- UUID 保证了文件名的唯一性。
解析2:绝对路径存储
String uploadDir = System.getProperty("user.dir") + "/uploads/";
为什么用绝对路径?
user.dir是项目的工作目录;- 使用绝对路径避免相对路径导致的文件找不到问题;
- 文件保存在项目根目录下的
uploads/文件夹。
解析3:目录自动创建
Path path = Paths.get(uploadDir);
if (!Files.exists(path)) {
Files.createDirectories(path);
}
设计意图:
- 如果
uploads/目录不存在,自动创建; - 避免因目录不存在导致的
FileNotFoundException。
四、静态资源映射:让图片可访问
4.1 为什么需要静态资源映射
文件上传到磁盘后,无法直接通过 HTTP 访问。需要配置 Spring Boot 的静态资源映射,将 /uploads/** 路径映射到磁盘上的 uploads/ 目录。
4.2 FileUploadConfig 配置
package com.hlw.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class FileUploadConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/uploads/**").addResourceLocations("file:uploads/");
}
}
配置说明:
addResourceHandler("/uploads/**"):匹配所有以/uploads/开头的 URL;addResourceLocations("file:uploads/"):将 URL 映射到磁盘上的uploads/目录;file:前缀表示文件系统路径。
4.3 图片访问示例
上传图片后,返回的 URL 是 /uploads/xxx.jpg,可以通过以下地址访问:
http://localhost:6789/uploads/xxx.jpg
五、前端图片上传实现
5.1 商品图片上传
// 商品图片上传
$('#goodsImage').change(function(e) {
var file = e.target.files[0];
if (!file) return;
var formData = new FormData();
formData.append('file', file);
$.ajax({
url: '/upload/image',
type: 'POST',
data: formData,
contentType: false,
processData: false,
success: function(res) {
if (res.code === 200) {
var imageUrl = res.data;
$('#imagePreview').attr('src', imageUrl);
$('#image').val(imageUrl);
App.showToast('图片上传成功');
} else {
App.showToast(res.msg);
}
},
error: function() {
App.showToast('上传失败');
}
});
});
关键配置:
contentType: false:不设置 Content-Type,让浏览器自动设置;processData: false:不处理数据,直接发送 FormData;FormData:用于封装文件数据。
5.2 头像上传
// 头像上传
$('#avatarInput').change(function(e) {
var file = e.target.files[0];
if (!file) return;
var formData = new FormData();
formData.append('file', file);
$.ajax({
url: '/upload/avatar',
type: 'POST',
data: formData,
contentType: false,
processData: false,
success: function(res) {
if (res.code === 200) {
var avatarUrl = res.data;
$('#avatarPreview').attr('src', avatarUrl);
$('#avatar').val(avatarUrl);
App.showToast('头像上传成功');
} else {
App.showToast(res.msg);
}
}
});
});
5.3 图片预览
<div class="form-group">
<label>商品图片</label>
<div class="input-group">
<input type="file" id="goodsImage" class="form-control-file">
</div>
<img id="imagePreview" src="" alt="图片预览" style="width: 200px; height: 200px; object-fit: cover; margin-top: 10px; display: none;">
<input type="hidden" id="image" name="image">
</div>
// 预览图片
function previewImage(input, previewId) {
var file = input.files[0];
if (file) {
var reader = new FileReader();
reader.onload = function(e) {
$('#' + previewId).attr('src', e.target.result);
$('#' + previewId).show();
};
reader.readAsDataURL(file);
}
}
六、与商品模块的联动
6.1 发布商品时上传图片
// 发布商品
$('#submitBtn').click(function() {
var title = $('#title').val();
var description = $('#description').val();
var category = $('#category').val();
var image = $('#image').val();
var originalPrice = $('#originalPrice').val();
var price = $('#price').val();
if (!title || !price) {
App.showToast('请填写必填项');
return;
}
$.ajax({
url: '/goods',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
userId: App.user.id,
title: title,
description: description,
category: category,
image: image,
originalPrice: parseFloat(originalPrice),
price: parseFloat(price)
}),
success: function(res) {
if (res.code === 200) {
App.showToast('发布成功');
window.location.href = 'goods-list.html';
} else {
App.showToast(res.msg);
}
}
});
});
6.2 商品详情展示图片
// 加载商品详情
function loadGoodsDetail(id) {
$.ajax({
url: '/goods/' + id,
type: 'GET',
success: function(res) {
if (res.code === 200) {
var goods = res.data;
$('#goodsTitle').text(goods.title);
$('#goodsDescription').text(goods.description);
$('#goodsPrice').text('¥' + goods.price);
// 展示商品图片
if (goods.image) {
$('#goodsImage').attr('src', goods.image);
} else {
$('#goodsImage').attr('src', '/images/default-goods.jpg');
}
}
}
});
}
6.3 商品列表展示缩略图
// 渲染商品列表
function renderGoods(goodsList) {
var html = '';
goodsList.forEach(function(goods) {
var image = goods.image || '/images/default-goods.jpg';
html += '<div class="col-md-3 mb-4">';
html += '<div class="card">';
html += '<img src="' + image + '" class="card-img-top" alt="' + goods.title + '" style="height: 200px; object-fit: cover;">';
html += '<div class="card-body">';
html += '<h5 class="card-title">' + goods.title + '</h5>';
html += '<p class="card-text">¥' + goods.price + '</p>';
html += '<a href="goods-detail.html?id=' + goods.id + '" class="btn btn-primary">查看详情</a>';
html += '</div></div></div>';
});
$('#goodsContainer').html(html);
}
七、开发过程中的踩坑记录
7.1 坑1:文件路径问题
问题:上传文件后,前端无法访问图片,报 404 错误。
原因:
- 文件保存路径不正确;
- 静态资源映射配置错误;
- 路径分隔符问题(Windows 用
\,Linux 用/)。
解决方案:
// 使用绝对路径
String uploadDir = System.getProperty("user.dir") + "/uploads/";
// 使用 Paths API 处理路径
Path path = Paths.get(uploadDir);
// 静态资源映射
registry.addResourceHandler("/uploads/**").addResourceLocations("file:uploads/");
7.2 坑2:文件覆盖问题
问题:多个用户上传同名文件时,后面的文件会覆盖前面的文件。
原因:直接使用原文件名保存。
解决方案:
// 使用 UUID 重命名
String filename = UUID.randomUUID().toString() + extension;
7.3 坑3:文件大小限制
问题:上传大文件时,报 MaxUploadSizeExceededException 错误。
原因:Spring Boot 默认的文件大小限制为 1MB。
解决方案:
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
7.4 坑4:文件格式校验
问题:用户可以上传非图片文件(如 .exe、.zip)。
原因:没有进行文件格式校验。
解决方案:
String allowedTypes = ".jpg,.jpeg,.png,.gif,.webp";
if (!allowedTypes.contains(extension.toLowerCase())) {
return Result.fail("只支持jpg、jpeg、png、gif、webp格式的图片");
}
7.5 坑5:前端直接显示文件路径
问题:用户头像字段是 /uploads/xxx.jpg,但前端直接显示字符串而不是图片。
原因:没有判断头像字段的类型。
解决方案:
var avatar = this.user.avatar || '👤';
var avatarHtml = avatar;
if (avatar && avatar.startsWith('/uploads/')) {
avatarHtml = '<img src="' + avatar + '" style="width:100%;height:100%;border-radius:50%;object-fit:cover;">';
}
八、代码优化建议
8.1 增加文件大小校验
@PostMapping("/image")
public Result uploadImage(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return Result.fail("请选择要上传的文件");
}
// 文件大小校验(5MB)
long maxSize = 5 * 1024 * 1024;
if (file.getSize() > maxSize) {
return Result.fail("文件大小不能超过5MB");
}
// ... 其他逻辑
}
8.2 抽取文件上传工具类
@Component
public class FileUploadUtil {
private static final String UPLOAD_DIR = System.getProperty("user.dir") + "/uploads/";
private static final String ALLOWED_TYPES = ".jpg,.jpeg,.png,.gif,.webp";
private static final long MAX_SIZE = 5 * 1024 * 1024;
public String upload(MultipartFile file) throws IOException {
if (file.isEmpty()) {
throw new IllegalArgumentException("请选择要上传的文件");
}
if (file.getSize() > MAX_SIZE) {
throw new IllegalArgumentException("文件大小不能超过5MB");
}
String originalFilename = file.getOriginalFilename();
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
if (!ALLOWED_TYPES.contains(extension.toLowerCase())) {
throw new IllegalArgumentException("只支持jpg、jpeg、png、gif、webp格式的图片");
}
String filename = UUID.randomUUID().toString() + extension;
Path path = Paths.get(UPLOAD_DIR);
if (!Files.exists(path)) {
Files.createDirectories(path);
}
File dest = new File(UPLOAD_DIR + filename);
file.transferTo(dest);
return "/uploads/" + filename;
}
}
使用:
@RestController
@RequestMapping("/upload")
public class UploadController {
@Autowired
private FileUploadUtil fileUploadUtil;
@PostMapping("/image")
public Result uploadImage(@RequestParam("file") MultipartFile file) {
try {
String url = fileUploadUtil.upload(file);
return Result.success(url);
} catch (IllegalArgumentException e) {
return Result.fail(e.getMessage());
} catch (IOException e) {
return Result.fail("文件上传失败");
}
}
}
8.3 支持多图上传
@PostMapping("/images")
public Result uploadImages(@RequestParam("files") MultipartFile[] files) {
List<String> urls = new ArrayList<>();
for (MultipartFile file : files) {
try {
String url = fileUploadUtil.upload(file);
urls.add(url);
} catch (Exception e) {
// 单个文件失败不影响其他文件
}
}
return Result.success(urls);
}
8.4 添加图片删除接口
@DeleteMapping("/image/{filename}")
public Result deleteImage(@PathVariable String filename) {
String filePath = UPLOAD_DIR + filename;
File file = new File(filePath);
if (file.exists()) {
file.delete();
return Result.success("删除成功");
}
return Result.fail("文件不存在");
}
8.5 使用云存储(如阿里云 OSS)
对于生产环境,建议使用云存储:
@Service
public class OssUploadService {
@Value("${oss.endpoint}")
private String endpoint;
@Value("${oss.accessKeyId}")
private String accessKeyId;
@Value("${oss.accessKeySecret}")
private String accessKeySecret;
@Value("${oss.bucketName}")
private String bucketName;
public String upload(MultipartFile file) throws IOException {
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
String filename = UUID.randomUUID().toString() + getExtension(file.getOriginalFilename());
InputStream inputStream = file.getInputStream();
ossClient.putObject(bucketName, filename, inputStream);
ossClient.shutdown();
return "https://" + bucketName + "." + endpoint + "/" + filename;
}
}
九、安全考虑
9.1 文件类型校验
当前实现:通过文件扩展名判断。
更好的方案:通过文件头部的 Magic Number 判断真实文件类型。
public boolean isImage(MultipartFile file) throws IOException {
byte[] bytes = file.getBytes();
if (bytes.length < 4) {
return false;
}
// JPEG
if (bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xD8 && bytes[2] == (byte) 0xFF) {
return true;
}
// PNG
if (bytes[0] == (byte) 0x89 && bytes[1] == (byte) 0x50 && bytes[2] == (byte) 0x4E && bytes[3] == (byte) 0x47) {
return true;
}
// GIF
if ((bytes[0] == (byte) 0x47 && bytes[1] == (byte) 0x49 && bytes[2] == (byte) 0x46 && bytes[3] == (byte) 0x38)) {
return true;
}
return false;
}
9.2 文件大小限制
spring:
servlet:
multipart:
max-file-size: 5MB
max-request-size: 10MB
9.3 防止路径遍历攻击
问题:用户可能上传名为 ../../../etc/passwd 的文件。
解决方案:
String filename = UUID.randomUUID().toString() + extension;
// 直接使用 UUID,不使用用户提供的文件名
9.4 限制访问权限
方案:
- 对上传目录设置适当的权限;
- 使用 Nginx 作为静态资源服务器,限制访问;
- 对敏感图片(如头像)进行访问控制。
十、性能优化
10.1 图片压缩
上传时对图片进行压缩,减少存储和传输成本:
public String uploadAndCompress(MultipartFile file) throws IOException {
BufferedImage image = ImageIO.read(file.getInputStream());
// 压缩到最大宽度 800px
int maxWidth = 800;
int maxHeight = 800;
int width = image.getWidth();
int height = image.getHeight();
if (width > maxWidth || height > maxHeight) {
double ratio = Math.min((double) maxWidth / width, (double) maxHeight / height);
int newWidth = (int) (width * ratio);
int newHeight = (int) (height * ratio);
BufferedImage compressedImage = new BufferedImage(newWidth, newHeight, image.getType());
Graphics2D g2d = compressedImage.createGraphics();
g2d.drawImage(image, 0, 0, newWidth, newHeight, null);
g2d.dispose();
String filename = UUID.randomUUID().toString() + ".jpg";
ImageIO.write(compressedImage, "jpg", new File(UPLOAD_DIR + filename));
return "/uploads/" + filename;
}
// 不需要压缩,直接保存
return upload(file);
}
10.2 使用 CDN
对于生产环境,使用 CDN 加速图片访问:
var imageUrl = 'https://cdn.example.com' + res.data;
10.3 添加缓存
设置静态资源的缓存时间:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:uploads/")
.setCachePeriod(3600 * 24 * 7); // 缓存7天
}
十一、总结
图片上传是二手交易平台的核心功能之一,本文详细讲解了如何在 Spring Boot 项目中实现完整的文件上传功能。
核心收获:
- 文件上传的完整流程:前端选择文件 → 后端接收并保存 → 返回访问 URL;
- 静态资源映射:通过
WebMvcConfigurer配置文件访问路径; - 文件安全:格式校验、大小限制、UUID 重命名;
- 踩坑经验:路径问题、文件覆盖、404 错误等常见问题的解决方案。
作为组员,我深刻体会到:文件上传看似简单,但涉及路径、安全、性能等多个方面,需要认真对待每一个细节。
更多推荐




所有评论(0)