1. 概述
在构建内容管理系统时,通常会面临两个核心问题:文件本身要存哪里,以及如何通过数据库进行索引管理。
一种做法是直接把文件内容存进数据库;另一种更常见的做法是将文件内容存放在外部存储系统中,而数据库只保存其元信息和访问路径。
本文将以一个简单的图像归档应用为例,演示这两种文件存储方式的实现,并提供对应的 REST API 实现上传与下载功能。
2. 使用场景
我们的图像归档应用支持 JPEG 图像的上传和下载。
上传时,系统会为每张图片生成唯一标识符(ID),后续可通过该 ID 下载对应图片。
我们将使用关系型数据库,并结合 Spring Data JPA 和 Hibernate 来实现持久化操作。
3. 数据库存储方案
3.1. 图像实体类
首先定义 Image
实体类:
@Entity
class Image {
@Id
@GeneratedValue
Long id;
@Lob
byte[] content;
String name;
// Getters and Setters
}
@Id
+@GeneratedValue
:由数据库自动生成主键。@Lob
:告诉 JPA 这是一个可能很大的二进制字段。
3.2. 图像 Repository
接下来创建用于数据库交互的 Repository:
@Repository
interface ImageDbRepository extends JpaRepository<Image, Long> {}
现在我们已经可以保存图像了,下一步就是提供上传接口。
4. REST 控制器
我们使用 MultipartFile
处理文件上传,上传成功后返回图像 ID。
4.1. 图像上传
创建 ImageController
支持上传:
@RestController
class ImageController {
@Autowired
ImageDbRepository imageDbRepository;
@PostMapping
Long uploadImage(@RequestParam MultipartFile multipartImage) throws Exception {
Image dbImage = new Image();
dbImage.setName(multipartImage.getName());
dbImage.setContent(multipartImage.getBytes());
return imageDbRepository.save(dbImage)
.getId();
}
}
上传逻辑很简单:
- 从
MultipartFile
中提取文件名和字节流 - 构造
Image
对象并保存到数据库 - 返回生成的 ID
4.2. 图像下载
添加下载接口:
@GetMapping(value = "/image/{imageId}", produces = MediaType.IMAGE_JPEG_VALUE)
Resource downloadImage(@PathVariable Long imageId) {
byte[] image = imageRepository.findById(imageId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND))
.getContent();
return new ByteArrayResource(image);
}
- 根据
imageId
查询数据库 - 若未找到则抛出 404 异常
- 否则返回封装为
ByteArrayResource
的字节流
5. 测试数据库存储方案
打包并启动应用:
mvn package
java -jar target/image-archive-0.0.1-SNAPSHOT.jar
5.1. 上传测试
使用 curl 上传图像:
curl -H "Content-Type: multipart/form-data" \
-F "[email protected]" http://localhost:8080/image
响应结果为:
1
5.2. 下载测试
使用 curl 下载图像:
curl -v http://localhost:8080/image/1 -o image.jpeg
输出示例:
< HTTP/1.1 200
< Content-Type: image/jpeg
< Content-Length: 9291
浏览器访问地址:http://localhost:8080/image/1
✅ 成功下载说明一切正常。
6. 分离内容与位置
前面的做法是将整个文件内容都存在数据库里,其实这不是最优解。
更好的方式是将文件内容存放在外部存储(如文件系统、云存储等),数据库仅记录其访问路径。
为此我们需要在 Image
实体中新增字段:
String location;
这个字段表示文件在外部存储中的逻辑路径,比如本地文件系统的绝对路径或者 S3 的 URI。
7. 文件系统存储实现
7.1. 文件写入
创建 FileSystemRepository
类来处理文件存储:
@Repository
class FileSystemRepository {
String RESOURCES_DIR = FileSystemRepository.class.getResource("/")
.getPath();
String save(byte[] content, String imageName) throws Exception {
Path newFile = Paths.get(RESOURCES_DIR + new Date().getTime() + "-" + imageName);
Files.createDirectories(newFile.getParent());
Files.write(newFile, content);
return newFile.toAbsolutePath()
.toString();
}
}
⚠️ 注意:为了避免文件名冲突,我们在文件名前加上时间戳保证唯一性。
7.2. 文件读取
读取文件内容的方法如下:
FileSystemResource findInFileSystem(String location) {
try {
return new FileSystemResource(Paths.get(location));
} catch (Exception e) {
throw new RuntimeException();
}
}
返回的是 Spring 的 FileSystemResource
,它支持延迟加载和流式传输。
7.3. Spring Resource 机制
FileSystemResource
是 Spring 提供的 Resource
接口的一个实现。
它的优势在于:
- ✅ 延迟加载:只有在真正需要时才开始读取文件
- ✅ 流式传输:避免一次性将大文件加载进内存
如果将来要迁移到云存储(如 AWS S3、GCS),只需替换为相应的 Resource
实现即可,例如 InputStreamResource
或 ByteArrayResource
。
8. 链接数据库与文件系统
现在我们要整合数据库和文件系统两部分。
8.1. 存储流程整合
创建 FileLocationService
服务类:
@Service
class FileLocationService {
@Autowired
FileSystemRepository fileSystemRepository;
@Autowired
ImageDbRepository imageDbRepository;
Long save(byte[] bytes, String imageName) throws Exception {
String location = fileSystemRepository.save(bytes, imageName);
return imageDbRepository.save(new Image(imageName, location))
.getId();
}
}
流程:
- 将文件保存至文件系统
- 将文件路径存入数据库
8.2. 查询流程整合
查询方法:
FileSystemResource find(Long imageId) {
Image image = imageDbRepository.findById(imageId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
return fileSystemRepository.findInFileSystem(image.getLocation());
}
流程:
- 在数据库中查找图像记录
- 获取其 location 字段
- 从文件系统中读取文件并返回
9. 文件系统上传与下载接口
创建新的 Controller:
@RestController
@RequestMapping("file-system")
class FileSystemImageController {
@Autowired
FileLocationService fileLocationService;
@PostMapping("/image")
Long uploadImage(@RequestParam MultipartFile image) throws Exception {
return fileLocationService.save(image.getBytes(), image.getOriginalFilename());
}
@GetMapping(value = "/image/{imageId}", produces = MediaType.IMAGE_JPEG_VALUE)
FileSystemResource downloadImage(@PathVariable Long imageId) throws Exception {
return fileLocationService.find(imageId);
}
}
接口路径以 /file-system
开头,区别于纯数据库版本。
10. 文件系统版本测试
上传:
curl -H "Content-Type: multipart/form-data" \
-F "[email protected]" http://localhost:8080/file-system/image
1
下载:
curl -v http://localhost:8080/file-system/image/1 -o image.jpeg
✅ 同样验证无误。
11. 总结
本篇文章展示了两种文件存储策略:
- 直接将文件内容存入数据库
- 将文件内容存入外部存储(如文件系统),数据库仅保留路径信息
我们还构建并测试了基于 Spring Boot 的 REST API,实现了多部件上传与资源流式下载功能。
完整代码可参考 GitHub 示例项目: https://github.com/eugenp/tutorials/tree/master/persistence-modules/spring-boot-persistence-2