SpringBoot文件上传存储解决方案

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策略工具类,静态方法功能如下

  1. 根据源文件名+命名策略获取存储文件名、
  2. 根据路径策略获取存储文件路径
  3. 根据文件保存的完整路径获取前端访问文件的路径
@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上传、分片、缩略图等……

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Spring Boot中实现文件上传进度条可以使用一些现有的库或自定义解决方案。以下是一个简单的示例,展示了如何使用Spring Boot和AJAX实现文件上传进度条功能。 首先,确保你的Spring Boot项目中已经添加了以下依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.4</version> </dependency> ``` 接下来,创建一个Controller来处理文件上传的请求: ```java import org.apache.commons.fileupload.ProgressListener;import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile;import javax.servlet.http.HttpServletRequest; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.Map; @Controller public class FileUploadController { @Value("${upload.path}") private String uploadPath; // 文件上传路径 @PostMapping("/upload") @ResponseBody public Map<String, Object> uploadFile(@RequestParam("file") MultipartFile file, HttpServletRequest request) { Map<String, Object> result = new HashMap<>(); if (file.isEmpty()) { result.put("success", false); result.put("message", "请选择文件"); return result; } try { // 创建文件上传进度监听器 ProgressListener progressListener = new CustomProgressListener(request.getSession()); // 创建文件上传处理器 ServletFileUpload upload = new ServletFileUpload(); upload.setProgressListener(progressListener); // 执行文件上传 String filename = file.getOriginalFilename(); file.transferTo(new File(uploadPath + File.separator + filename)); result.put("success", true); result.put("message", "文件上传成功"); } catch (IOException e) { result.put("success", false); result.put("message", "文件上传失败"); } return result; } } ``` 在上面的代码中,我们使用`@RequestParam`注解来接收上传的文件,并通过`MultipartFile`类型的参数接收。在文件上传过程中,我们创建了一个自定义的进度监听器`CustomProgressListener`,可以用来获取上传进度信息。 接下来,我们需要实现进度监听器: ```java import org.apache.commons.fileupload.ProgressListener; import javax.servlet.http.HttpSession; public class CustomProgressListener implements ProgressListener { private HttpSession session; public CustomProgressListener(HttpSession session) { this.session = session; } @Override public void update(long bytesRead, long contentLength, int items) { // 计算上传的百分比 double percent = (bytesRead * 100.0) / contentLength; // 将进度信息存储在session中 session.setAttribute("uploadProgress", percent); } } ``` 在进度监听器中,我们计算了上传的百分比,并将结果存储在`HttpSession`中,以便在前端页面中获取。 最后,在前端页面中使用AJAX轮询来获取上传进度: ```javascript function uploadFile() { var formData = new FormData(); var fileInput = document.getElementById("fileInput"); formData.append("file", fileInput.files[0]); var xhr = new XMLHttpRequest(); xhr.upload.addEventListener("progress", function(event) { if (event.lengthComputable) { var percentComplete = (event.loaded / event.total) * 100; console.log(percentComplete + "%"); } }, false); xhr.open("POST", "/upload"); xhr.send(formData); } ``` 以上代码创建了一个XMLHttpRequest对象,并通过监听`progress`事件来获取上传进度信息,然后将信息打印到控制台。 这样,当你执行`uploadFile()`函数时,就可以实时获取文件上传的进度了。 这只是一个简单的示例,你可以根据实际需求进行扩展和优化。希望对你有所帮助!
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

菠萝追雪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值