SpringBoot文件上传存储解决方案
在实际的应用开发中,文件上传是一个非常常见的需求,无论是头像上传、文件分享,还是图片上传,都需要一个易读、可靠且高效的文件上传功能来支持。
本文将展示如何使用Spring Boot框架来实现一个较为完善的文件上传以及文件存储的处理功能,让你能够快速在自己的项目中应用文件上传功能,提升开发效率
一、搭建storage存储服务
新建一个storage存储包(与文件存储打交道)
1. application.yaml 配置文件
spring:
servlet:
# 文件上传配置(如果上传的文件过大、需要在这里配置)
multipart:
enabled: true
file-size-threshold: 0
max-file-size: 250MB
max-request-size: 1024MB
file-storage:
localStorage:
root-path: D:\360downloads\IDEA-workplace\SurveyKing-master\survery-server\files # 这里写本机任意文件保存路径
pathStrategy: byDate # byDate| byId | byNo
nameStrategy: seqAndOriginalName # seqAndOriginalName | originalNameAndSeq | seq | uuid
dateFormat: yyMM/dd
upload:
requestPre: '/images' # 前端访问加上前缀映射
2. StorageProperties 自定义配置属性类
@Data
@ConfigurationProperties("file-storage")
public class StorageProperties {
@NestedConfigurationProperty
private final LocalStorageStrategy storageStrategy = new LocalStorageStrategy();
@Data
public static class LocalStorageStrategy {
private String rootPath;
private String pathStrategy = LocalStoragePathStrategyEnum.BY_DATE.getStrategy();
private String nameStrategy = LocalStorageNameStrategyEnum.SEQ_AND_ORIGINAL_NAME.getStrategy();
private String dateFormat = "YYYY-MM-dd hh:mm:ss";
}
}
3. StorageService 统一对外上传存储接口
public interface StorageService {
/**
* 图片上传
* @param inputStream 文件流
* @param filePath 文件保存的完整路径
*/
void upload(InputStream inputStream, String filePath);
/**
* 下载目录文件
* @param filePath 文件路径
* @return 文件字节数据
*/
byte[] download(String filePath);
}
4. AbstractStorageService 抽象类
该类是抽取所有存储服务实现类共有的方法实现,易于解耦复用。如生成缩略图等默认实现都可以写在这里。
public abstract class AbstractStorageService implements StorageService {
// 配置属性类
protected StorageProperties storageConfig;
public AbstractStorageService(StorageProperties storageProperties){
this.storageConfig = storageProperties;
this.init(); // 初始化
}
/**
* 子实现类初始化方法
*/
abstract void init();
}
5. LocalStorageServiceImpl本地存储服务实现类
public class LocalStorageServiceImpl extends AbstractStorageService{
// 配置中存储路径的Path对象
private Path rootLocation;
public LocalStorageServiceImpl(StorageProperties storageProperties) {
super(storageProperties);
}
/**
* 父类构造函数执行中自动初始化的启动代码
*/
@Override
public void init(){
try{
// 父类中获取properties文件保存根路径,提前创建目录
String path = this.storageConfig.getStorageStrategy().getRootPath();
this.rootLocation = Paths.get(path);
Files.createDirectories(rootLocation);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void upload(InputStream inputStream,String savePath) {
try {
// 得到要保存文件的父目录 = files + 路径策略
// 1. 创建父目录(多线程) 2.创建/替换文件
Path filePath = this.rootLocation.resolve(savePath);
Path dirPath = filePath.getParent();
if(!Files.exists(dirPath) || !Files.isDirectory(dirPath)){
// 目录还不存在需要创建 ,两个线程有可能同时执行到这里,获取锁后还应二次判断
synchronized (dirPath.toAbsolutePath().toString().intern()){
if(!Files.exists(dirPath) || !Files.isDirectory(dirPath)) {
try {
Files.createDirectories(dirPath);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
Files.copy(inputStream,filePath, StandardCopyOption.REPLACE_EXISTING);
}catch (IOException e){
e.printStackTrace();
}
}
@Override
public byte[] download(String filePath){
// TODO
}
}
6. StorageAutoConfiguration 自动配置类
按照spring常规注入方式,使用AutoConfiguration的配置类方式注入本地存储实现类,service层便可以@Autowire注入本地存储服务实现类对象使用
@Configuration
@RequiredArgsConstructor
@ConfigurationPropertiesScan("com.group6.product_source.storage") // 自己项目的storage文件夹路径
public class StorageAutoConfiguration {
@Bean
// 如果存在这个配置file-storage.storageStrategy.rootPath
@ConditionalOnProperty(prefix = "file-storage.storageStrategy",name = "rootPath")
// 确保单例
@ConditionalOnMissingBean
public StorageService storageService(StorageProperties storageProperties){
// storageProperties是Spring中自动参数注入 ↑
return new LocalStorageServiceImpl(storageProperties);
}
}
7. LocalStoragePathStrategyEnum 存储路径策略
自定义文件存储路径策略,本地存储策略 => 存储本地的路径
public enum LocalStoragePathStrategyEnum {
/** 所有文件存储在 rootPath 下 */
BY_NO("byNo"),
/** 按照项目的short-id分文件夹存储,例如 rootPath/RyP2rR */
BY_ID("byId"),
/** 按照上传日期存储,例如 rootPath/2022/06/01 */
BY_DATE("byDate");
private final String strategy;
LocalStoragePathStrategyEnum(String strategy){
this.strategy = strategy;
}
public String getStrategy() {
return strategy;
}
}
8. LocalStorageNameStrategyEnum 文件命名策略
自定义文件存储命名策略类,上传的源文件名 => 存储本地的文件名
public enum LocalStorageNameStrategyEnum {
/**
* 文件名策略
* 1. seqAndOriginalName: 序列号加原文件名
* 2. originalNameAndSeq: 原文件名+序列号
* 3. Seq: 序列号(项目启动时间戳的自增)
* 4. UUID: 去除短杠'-'的UUID
*/
UUID("uuid"),
SEQ("seq"),
SEQ_AND_ORIGINAL_NAME("seqAndOriginalName"),
ORIGINAL_NAME_AND_SEQ("originalNameAndSeq");
private final String strategy;
LocalStorageNameStrategyEnum(String strategy){
this.strategy = strategy;
}
public String getStrategy() {
return strategy;
}
}
二、开始在业务层中使用
1. FileServiceImpl业务服务层
@Service
// 构造函数的方式注入对象
@RequiredArgsConstructor
public class FileServiceImpl implements FileService {
// 存储服务实现类
private final StorageService storageService;
// 存储服务工具类
private final StorageStrategyUtils storageStrategyUtils;
@Override
public String uploadImage(MultipartFile imageFile) {
String fileName = imageFile.getOriginalFilename();
// TODO 在这里保存前进行文件校验如支持扩展名验证等、具体实现略
// 1. 根据系统的当前存储策略获取文件存储命名
String fileSaveName = storageStrategyUtils.getNameStrategy(fileName);
// 2. 根据系统当前存储策略获取文件存储路径
String fileSavePath = storageStrategyUtils.getPathStrategy(fileSaveName,null);
try(InputStream inputStream = imageFile.getInputStream()){
storageService.upload(inputStream,fileSavePath);
// 保存上传的文件,前端图片请求前缀,对应mvc
return storageStrategyUtils.getRequestPath(fileSavePath);
} catch (IOException e) {
throw new RuntimeException("图片上传失败");
}
}
}
2. StorageStrategyUtils 工具类
工具类Storage策略工具类,静态方法功能如下
- 根据源文件名+命名策略获取存储文件名、
- 根据路径策略获取存储文件路径
- 根据文件保存的完整路径获取前端访问文件的路径
@Component
public class StorageStrategyUtils {
private final static AtomicLong atomicLong = new AtomicLong(System.currentTimeMillis());
@Resource
private StorageProperties storageConfig;
@Value("${server.port}")
private String port;
private String ip = "127.0.0.1";
@Value("${upload.requestPre}")
private String requestPre;
public String getNameStrategy(String originalName){
String fileName = originalName.substring(0,originalName.lastIndexOf("."));
String suffix = originalName.substring(originalName.lastIndexOf("."));
String strategy = storageConfig.getStorageStrategy().getNameStrategy();
if(strategy.equals(LocalStorageNameStrategyEnum.UUID.getStrategy())){
return UUID.randomUUID().toString().replaceAll("-","") + suffix;
}else if(strategy.equals(LocalStorageNameStrategyEnum.SEQ.getStrategy())){
return atomicLong.incrementAndGet() + suffix;
}else if(strategy.equals(LocalStorageNameStrategyEnum.ORIGINAL_NAME_AND_SEQ.getStrategy())){
return fileName + "_" + atomicLong.incrementAndGet() + suffix;
}else if(strategy.equals(LocalStorageNameStrategyEnum.SEQ_AND_ORIGINAL_NAME.getStrategy())){
return atomicLong.incrementAndGet() + "_" + fileName + suffix;
}
// 默认
return fileName;
}
public String getPathStrategy(String fileName, String dirId){
String pathStrategy = storageConfig.getStorageStrategy().getPathStrategy();
if(pathStrategy.equals(LocalStoragePathStrategyEnum.BY_DATE.getStrategy())){
// 根据日期创建文件夹存放 yyMM/dd + fileName -> yyMM\\dd\\fileName
String storageDateFormat = storageConfig.getStorageStrategy().getDateFormat();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(storageDateFormat);
String dateFormatStr = LocalDate.now().format(formatter).replace("/", File.separator).replace("\\", File.separator);
return dateFormatStr + File.separator + (StringUtils.hasText(dirId)?dirId + File.separator:"") +fileName;
}else if(pathStrategy.equals(LocalStoragePathStrategyEnum.BY_ID.getStrategy())){
// 放在id文件夹中
return dirId + File.separator + fileName;
}else if(pathStrategy.equals(LocalStoragePathStrategyEnum.BY_NO.getStrategy())){
// 全放到rootPath下
return fileName;
}
// 默认文件夹名
return "default" + File.separator + fileName;
}
/**
* 根据保存路径返回前端访问它的url
* @param fileSavePath 文件完整保存路径
* @return
*/
public String getRequestPath(String fileSavePath) {
// 保存上传的文件,前端图片请求前缀,mvc资源的请求路径映射也是requestPre
return "http://" + ip + ":" + port + requestPre + "/" + fileSavePath.replaceAll("\\\\","/");
}
}
3. WebConfig资源路径映射配置
public class WebConfig implements WebMvcConfigurer {
@Value("${upload.requestPre}")
private String requestPre;
@Autowired
private StorageProperties storageProperties;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry){
String filePath = storageProperties.getStorageStrategy().getRootPath();
registry.setOrder(1) // 设置静态资源的映射优先级高于前端控制器mapping ,前缀映射:前端访问requestPre的url下映射为本机的静态资源目录
.addResourceHandler(String.format("%s/**",requestPre))
.addResourceLocations("file:" + filePath + "\\");
}
}
三、开始测试
路径存储策略:yyMM/dd
命名存储策略:序列号(项目启动时间戳的自增)+ 源文件名
运行结果:
四、最后小结
以上的解决方案为学习总结仅代表个人观点,其细节和功能有待完善和扩展,如解耦复用并发,oos上传、分片、缩略图等……