[File] Use SpringBoot to upload and download files

File uploading and downloading are used a lot in actual projects, so I’ll summarize it here today.

This blog is not only about completing a file upload/download function, but more importantly, standardizing the coding: logging, input/return parameter verification, single method responsibility, etc.

Development environment:

1.IDEA 2020.2
2.Maven 3.6.0
3.SpringBoot 2.0.0.RELEASE

1. Upload of single file

Create a new SpringBoot project. The project structure diagram is as follows:

Insert image description here
POM dependencies:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-configuration-processor</artifactId>
	<optional>true</optional>
</dependency>
<dependency>
	<groupId>joda-time</groupId>
	<artifactId>joda-time</artifactId>
	<version>2.9.7</version>
</dependency>

applicaiton.yml

spring:
  servlet:
    multipart:
      enabled: true
      max-file-size: 10MB       # 单个文件上传的最大上限
      max-request-size: 10MB    # 一次请求总大小上限
file:                           # 文件上传路径
  uploadPath: E:/upload

Next, it was officially implemented in code~~

File upload interfaceFileController

@RestController
@RequestMapping("/file")
public class FileController {
    
    

    @Autowired
    private FileService fileService;

    // 上传单个文件
    @PostMapping("/upload")
    public ResultVo<String> uploadFile(@RequestParam("file") MultipartFile file) {
    
    
        return fileService.uploadFile(file);
    }

}

FileService

public interface FileService {
    
    
    // 上传文件
    ResultVo<String> uploadFile(MultipartFile file);
    
}

FileServiceImpl

@Service
@Slf4j
public class FileServiceImpl implements FileService {
    
    

    // 上传文件
    @Override
    public ResultVo<String> uploadFile(MultipartFile file) {
    
    
        log.info("【文件上传】进入到文件上传方法");
        // 1.参数校验
        if (null == file || file.isEmpty()) {
    
    
            log.error("【文件上传】文件为空!");
            throw new ParamErrorException();
        }
        // 2.上传文件
        ResultVo<String> resultVo = FileUtil.uploadFile(file);
        return resultVo;
    }

}

illustrate:

  1. do logging
  2. Verify parameters
  3. Custom exception ParamErrorExceptionhandling
  4. Unified encapsulation of return resultsResultVo
  5. Single responsibility, use tool class to perform file upload operationsFileUtil#uploadFile()

1》、Custom exceptionParamErrorException

@Data
public class ParamErrorException extends RuntimeException {
    
    

    // 错误码
    private Integer code;

    // 错误消息
    private String msg;

    public ParamErrorException() {
    
    
        this(ResultCodeEnum.PARAM_ERROR.getCode(), ResultCodeEnum.PARAM_ERROR.getMessage());
    }
    public ParamErrorException(String msg) {
    
    
        this(ResultCodeEnum.PARAM_ERROR.getCode(), msg);
    }
    public ParamErrorException(Integer code, String msg) {
    
    
        super(msg);
        this.code = code;
        this.msg = msg;
    }

}

ResultCodeEnum

Custom enumeration classes are also used in custom exceptions.

@Getter
public enum ResultCodeEnum {
    
    

    SUCCESS(200, "成功")
    ,
    ERROR(301, "错误")
    ,
    PARAM_ERROR(303, "参数错误")
    ,
    FILE_NOT_EXIST(304, "文件不存在")
    ,
    CLOSE_FAILD(305, "关闭流失败")
    ;

    private Integer code;
    private String message;

    ResultCodeEnum(Integer code, String message) {
    
    
        this.code = code;
        this.message = message;
    }

}

FileExceptionControllerAdvice

In SpringBoot, instead of handling each thrown exception try {} catch(Exception e) {}, annotations are used @RestControllerAdviceto handle exceptions uniformly:

@RestControllerAdvice
public class FileExceptionControllerAdvice {
    
    

    // 处理文件为空的异常
    @ExceptionHandler(ParamErrorException.class)
    public ResultVo<String> fileExceptionHandler(ParamErrorException exception) {
    
    
        return ResultVoUtil.error(exception.getCode(), exception.getMsg());
    }

}

2》. Unified return of encapsulated resultsResultVo

@Data
public class ResultVo<T> {
    
    

    // 错误码
    private Integer code;

    // 提示信息
    private String msg;

    // 返回的数据
    private T data;
    
    // 判断是否成功
	public boolean checkSuccess() {
    
    
        return ResultCodeEnum.SUCCESS.getCode().equals(this.code);
    }

}

ResultVoUtil

Encapsulate a tool class to return success/failure

public class ResultVoUtil {
    
    

    public static ResultVo success() {
    
    
        return success(null);
    }
    public static ResultVo success(Object object) {
    
    
        ResultVo result = new ResultVo();
        result.setCode(ResultCodeEnum.SUCCESS.getCode());
        result.setMsg("成功");
        result.setData(object);
        return result;
    }
    public static ResultVo success(Integer code, Object object) {
    
    
        return success(code, null, object);
    }
    public static ResultVo success(Integer code, String msg, Object object) {
    
    
        ResultVo result = new ResultVo();

        result.setCode(code);
        result.setMsg(msg);
        result.setData(object);
        return result;
    }

    public static ResultVo error(String msg) {
    
    
        ResultVo result = new ResultVo();
        result.setCode(ResultCodeEnum.ERROR.getCode());
        result.setMsg(msg);
        return result;
    }
    public static ResultVo error(Integer code, String msg) {
    
    
        ResultVo result = new ResultVo();
        result.setCode(code);
        result.setMsg(msg);
        return result;
    }

}

3》、File upload

@Slf4j
public class FileUtil {
    
    

    private static FileConfig fileConfig = ApplicationContextHolder.getContext().getBean(FileConfig.class);

    // 下划线
    public static final String UNDER_LINE = "_";

    // 上传文件
    public static ResultVo<String> uploadFile(MultipartFile file) {
    
    
        // 1.获取一个新的文件名
        String newFileName = getNewFileName(file);
        if (StringUtil.isBlank(newFileName)) {
    
    
            log.error("【上传文件】转换文件名称失败");
            return ResultVoUtil.error("【上传文件】转换文件名称失败");
        }
        // 2.获取文件上传路径
        String uploadPath = fileConfig.getUploadPath();
        if (StringUtil.isBlank(uploadPath)) {
    
    
            log.error("【上传文件】获取文件上传路径失败");
            return ResultVoUtil.error("【上传文件】获取文件上传路径失败");
        }
        uploadPath = uploadPath + File.separator +  DateUtil.getCurrentDate();
        // 3.生成上传目录
        File uploadDir = mkdirs(uploadPath);
        if (!uploadDir.exists()) {
    
    
            log.error("【上传文件】生成上传目录失败");
            return ResultVoUtil.error("【上传文件】生成上传目录失败");
        }
        // 4.文件全路径
        String fileFullPath = uploadPath + File.separator + newFileName;
        log.info("上传的文件:" + file.getName() + "," + file.getContentType() + ",保存的路径为:" + fileFullPath);
        try {
    
    
            // 5.上传文件
            doUploadFile(file, fileFullPath);
        } catch (IOException e) {
    
    
            log.error("【上传文件】上传文件报IO异常,异常信息为{}", e.getMessage());
            return ResultVoUtil.error(e.getMessage());
        }
        return ResultVoUtil.success(fileFullPath);
    }
    
}

illustrate:

  1. I encapsulate a string tool class StringUtilto determine whether the string is empty;
  2. Get the file upload path: String uploadPath = fileConfig.getUploadPath();This code is very interesting;
  3. DateUtil.getCurrentDate()Method to get the current date.

StringUtil: String tool class

@Component
public class StringUtil {
    
    

    // 判断字符串是否为空
    public static boolean isBlank(String content) {
    
    
        if (null == content || "".equals(content)) {
    
    
            return true;
        }
        return false;
    }

}

Why should we write this way to get the file upload path? String uploadPath = fileConfig.getUploadPath();

Think about it carefully, we configure the file upload path in application.ymlthe file:

file:
  uploadPath: E:/upload

So, how do we get this value?

There are two ways of thinking: @Valueannotation; @ConfigurationPropertiesannotation

Here, I chose the 2nd option:

FileConfigkind:

@Data
@Component
@ConfigurationProperties(prefix = "file")
public class FileConfig {
    
    

    // 上传路径
    private String uploadPath;

}

In this way, I can assign values ​​to the FileConfigproperties in the class . uploadPathThen, we can fileConfig.getUploadPath()get it via .

However, this method does not work in static methods! ! Right now:

public class FileUtil {
    
    
    @Autowired
    private FileConfig fileConfig;
	
	public static ResultVo<String> uploadFile(MultipartFile file) {
    
    
		...
		String uploadPath = fileConfig.getUploadPath();
		...
	}
}

This kind of coding can't even pass the compilation period! ! Why?

Because non-static properties/methods cannot be called in static methods.

So, the solution is:

private static FileConfig fileConfig = ApplicationContextHolder.getContext().getBean(FileConfig.class);

ApplicationContextHolder: Static variable saves Spring ApplicationContext, which can be taken out from any code, anywhere at any time ApplicaitonContext:

@Component
public class ApplicationContextHolder implements ApplicationContextAware {
    
    

    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    
    
        context = applicationContext;
    }

    public static ApplicationContext getContext() {
    
    
        return context;
    }

    public static Object getBean(String name) {
    
    
        return context != null ? context.getBean(name) : null;
    }

    public static <T> T getBean(Class<T> clz) {
    
    
        return context != null ? context.getBean(clz) : null;
    }

    public static <T> T getBean(String name, Class<T> clz) {
    
    
        return context != null ? context.getBean(name, clz) : null;
    }

    public static void addApplicationListenerBean(String listenerBeanName) {
    
    
        if (context != null) {
    
    
            ApplicationEventMulticaster applicationEventMulticaster = (ApplicationEventMulticaster)context.getBean(ApplicationEventMulticaster.class);
            applicationEventMulticaster.addApplicationListenerBean(listenerBeanName);
        }
    }

}

DateUtil:Date tool class:

@Component
public class DateUtil {
    
    

    // 获取当前时间
    public static String getCurrentTime() {
    
    
        DateTime now = new DateTime();
        return now.toString(DateConstant.DEFAULT_FORMAT_PATTERN);
    }

    // 获取当前日期
    public static String getCurrentDate() {
    
    
        LocalDate localDate = new LocalDate();
        return localDate.toString();
    }
    
}

DateConstant:Date constant class

public interface DateConstant {
    
    

    // 默认的日期格式化格式
    String DEFAULT_FORMAT_PATTERN = "yyyy-MM-dd HH:mm:ss";

}

For the date here, I chose joda-timethis component. This component was recommended to me by a big shot in the department, so I will briefly recommend it here! !

getNewFileName(MultipartFile)Method: Convert uploaded file to a new file name

// 将上传的文件转换为一个新的文件名
public static String getNewFileName(MultipartFile file) {
    
    
    // 1.获取上传的文件名称(包含后缀。如:test.jpg)
    String originalFilename = file.getOriginalFilename();
    log.info("【上传文件】上传的文件名为{}", originalFilename);
    // 2.以小数点进行分割
    String[] split = originalFilename.split("\\.");
    String newFileName = null;
    if (null == split || split.length == 0) {
    
    
        return null;
    }
    StringBuilder builder = new StringBuilder();
    if (1 == split.length) {
    
    
        // 3.此文件无后缀
        newFileName = builder.append(originalFilename).append(UNDER_LINE).append(System.nanoTime()).toString();
        return newFileName;
    }
    // 4.获取文件的后缀
    String fileSuffix = split[split.length - 1];
    for (int i = 0; i < split.length - 1; i++) {
    
    
        builder.append(split[i]);
        if (null != split[i + 1] && "" != split[i + 1]) {
    
    
            builder.append(UNDER_LINE);
        }
    }
    newFileName = builder.append(System.nanoTime()).append(".").append(fileSuffix).toString();
    return newFileName;
}

illustrate:

  1. If the uploaded file has no suffix (distinguished by "."), directly concatenate an underscore "_", then avoid duplication of file names, and then concatenate a timestamp;
  2. If the uploaded file has a suffix, convert the "." in the file name into an underscore "_" and then splice the timestamp.

mkdirs(String)Method: Generate the corresponding directory

// 生成相应的目录
public static File mkdirs(String path) {
    
    
    File file = new File(path);
    if(!file.exists() || !file.isDirectory()) {
    
    
        file.mkdirs();
    }
    return file;
}

doUploadFile(MultipartFile, String)Method: Upload file

// 上传文件
public static void doUploadFile(MultipartFile file, String path) throws IOException {
    
    
    Streams.copy(file.getInputStream(), new FileOutputStream(path), true);
}

Obviously, this is a conversion of flow.

There are many ways to upload files, as follows:

public static void doUploadFile(MultipartFile file, String path) throws IOException {
    
    
    // 法一:
    Streams.copy(file.getInputStream(), new FileOutputStream(path), true);

    // 法二: 通过MultipartFile#transferTo(File)
    // 使用此方法保存,必须要绝对路径且文件夹必须已存在,否则报错
    //file.transferTo(new File(path));

    // 法三:通过NIO将字节写入文件
    //Path filePath = Paths.get(path);
    //Files.write(filePath, file.getBytes());

    // 法四:
    /*try (InputStream in = file.getInputStream();
         FileOutputStream out = new FileOutputStream(path)) {
        IOUtils.copy(in, out);
    } catch (Exception e) {
        log.error("【上传文件】上传文件失败,失败信息为:{}", e.getMessage());
    }*/

    // 法五:
    /*InputStream in = file.getInputStream();
    OutputStream out = new FileOutputStream(path);
    int len = 0;
    byte[] bytes = new byte[1024];
    while ((len = in.read(bytes)) != -1) {
        out.write(bytes, 0, len);
    }
    in.close();
    out.close();*/

    // 法六:
    /*byte[] bytes = file.getBytes();
    OutputStream out = new FileOutputStream(path);
    out.write(bytes);
    out.close();*/
}

Here, the author doesn't know which method to use, I only know that these methods can realize the file upload function. But they certainly have their pros and cons.

2. Multiple file uploads

Multiple file uploads and single file uploads are essentially the same. However, the background interface for multiple file uploads uses an array to receive, and then calls the file upload method in a loop. The code is as follows:

FileController: Add an interface

@PostMapping("/uploadFiles")
public ResultVo uploadFiles(@RequestParam("files") MultipartFile[] files) {
    
    
    return fileService.uploadFiles(files);
}

FileServiceImpl:Add a method:

@Override
public ResultVo uploadFiles(MultipartFile[] files) {
    
    
    log.info("【批量上传】进入到批量上传文件");
    if (null == files || files.length == 0) {
    
    
        log.error("【批量上传】上传的文件为空,files={}", files);
        throw new ParamErrorException();
    }
    List<MultipartFile> multipartFiles = Arrays.asList(files);
    // 1.校验是否有空文件
    List<String> emptyFileNames = new ArrayList<>();
    List<MultipartFile> needUploadFiles = new ArrayList<>();
    int count = 0;
    for (MultipartFile file : multipartFiles) {
    
    
        if (null == file) {
    
    
            count++;
            continue;
        }
        if (file.isEmpty()) {
    
    
            emptyFileNames.add(file.getOriginalFilename());
            count++;
            continue;
        }
        needUploadFiles.add(file);
    }
    if (count == multipartFiles.size()) {
    
    
        log.error("【批量上传】批量上传的文件为空,无法正确上传");
        return ResultVoUtil.error("批量上传的文件为空,无法正确上传");
    }
    if (CollectionUtil.isNotEmpty(emptyFileNames)) {
    
    
        log.info("【批量上传】一共上传了{}个文件,其中,空文件数为{},空文件名分别是:{}", multipartFiles.size(), count, emptyFileNames);
    } else {
    
    
        log.info("【批量上传】一共上传了{}个文件", multipartFiles.size());
    }
    // 2.批量上传文件
    List<String> uploadFailFileNames = new ArrayList<>(needUploadFiles.size());
    needUploadFiles.forEach((file) -> {
    
    
        ResultVo<String> resultVo = FileUtil.uploadFile(file);
        // 如果没有上传成功
        if (!resultVo.checkSuccess()) {
    
    
            uploadFailFileNames.add(file.getName());
        }
    });
    if (CollectionUtil.isNotEmpty(uploadFailFileNames)) {
    
    
        log.error("一共上传了{}个文件,其中上传失败的文件数为{},文件名分别为:{}", needUploadFiles.size(), uploadFailFileNames.size(), uploadFailFileNames);
        return ResultVoUtil.success("一共上传了" + needUploadFiles.size() + "个文件,其中上传失败的文件数为" + uploadFailFileNames.size() + ",文件名分别为:" + uploadFailFileNames);
    }
    log.info("批量上传文件成功");
    return ResultVoUtil.success();
}

CollectionUtil

public class CollectionUtil {
    
    

    public static boolean isNotEmpty(Collection<?> coll) {
    
    
        return !isEmpty(coll);
    }

    public static boolean isEmpty(Collection<?> coll) {
    
    
        return coll == null || coll.isEmpty();
    }
}

When using POSTMAN to call the interface, upload multiple files:
Insert image description here
you can test it yourself!

3. Download of files

Download a file based on its path. Here, for convenience, I pass this path through the interface.

FileController

@PostMapping("/download")
public ResultVo<String> downloadFile(@RequestParam("filePath") String filePath, final HttpServletResponse response) {
    
    
    return fileService.downloadFile(filePath, response);
}

FileServiceImpl

@Override
public ResultVo<String> downloadFile(String filePath, HttpServletResponse response) {
    
    
    File file = new File(filePath);
    // 1.参数校验
    if (!file.exists()) {
    
    
        log.error("【下载文件】文件路径{}不存在", filePath);
        return ResultVoUtil.error("文件不存在");
    }
    // 2.下载文件
    log.info("【下载文件】下载文件的路径为{}", filePath);
    return FileUtil.downloadFile(file, response);
}

FileUtil:download file

public static ResultVo<String> downloadFile(File file, HttpServletResponse response) {
    
    
    try {
    
    
        // 1.设置响应头
        setResponse(file, response);
    } catch (UnsupportedEncodingException e) {
    
    
        log.error("文件名{}不支持转换为字符集{}", file.getName(), "UTF-8");
        return ResultVoUtil.error(e.getMessage());
    }
    // 2.下载文件
    return doDownLoadFile(file, response);
}

setResponse(): Set response header

public static void setResponse(File file, HttpServletResponse response) throws UnsupportedEncodingException {
    
    
    // 清空response
    response.reset();
    response.setCharacterEncoding("UTF-8");
    // 返回给客户端类型,任意类型
    response.setContentType("application/octet-stream");
    // Content-Disposition的作用:告知浏览器以何种方式显示响应返回的文件,用浏览器打开还是以附件的形式下载到本地保存
    // attachment表示以附件方式下载 inline表示在线打开 "Content-Disposition: inline; filename=文件名.mp3"
    response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(file.getName(), "UTF-8"));
    // 告知浏览器文件的大小
    response.addHeader("Content-Length", String.valueOf(file.length()));
}

doDownLoadFile():download file

public static ResultVo<String> doDownLoadFile(File file, HttpServletResponse response) {
    
    
    // 法一:IOUtils
    /*try (FileInputStream in = new FileInputStream(file);
         OutputStream out = response.getOutputStream()) {
        // 2.下载文件
        IOUtils.copy(in, out);
        log.info("【文件下载】文件下载成功");
        return null;
    } catch (FileNotFoundException e) {
        log.error("【文件下载】下载文件时,没有找到相应的文件,文件路径为{}", file.getAbsolutePath());
        return ResultVoUtil.error(e.getMessage());
    } catch (IOException e) {
        log.error("【文件下载】下载文件时,出现文件IO异常");
        return ResultVoUtil.error(e.getMessage());
    }*/

    // 法二:将文件以流的形式一次性读取到内存,通过响应输出流输出到前端
    /*try (InputStream in = new BufferedInputStream(new FileInputStream(file));
         OutputStream out = new BufferedOutputStream(response.getOutputStream())) {
        byte[] buffer = new byte[in.available()];
        in.read(buffer);
        out.write(buffer);
        log.info("【文件下载】文件下载成功");
        return null;
    } catch (IOException e) {
        log.error("【文件下载】下载文件时,出现文件IO异常");
        return ResultVoUtil.error(e.getMessage());
    }*/

    // 法三:将输入流中的数据循环写入到响应输出流中,而不是一次性读取到内存,通过响应输出流输出到前端
    try (InputStream in = new FileInputStream(file);
         OutputStream out = response.getOutputStream()) {
    
    
        byte[] buffer = new byte[1024];
        int len = 0;
        while ((len = in.read(buffer)) != -1) {
    
    
            out.write(buffer, 0, len);
        }
        log.info("【文件下载】文件下载成功");
        return null;
    } catch (FileNotFoundException e){
    
    
        log.error("【文件下载】下载文件时,没有找到相应的文件,文件路径为{}", file.getAbsolutePath());
        return ResultVoUtil.error(e.getMessage());
    } catch (IOException e) {
    
    
        log.error("【文件下载】下载文件时,出现文件IO异常");
        return ResultVoUtil.error(e.getMessage());
    }
}

[Note]: After downloading the file, do not return the value again, otherwise an exception will be thrown java.lang.IllegalStateException:

java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
	at org.apache.catalina.connector.ResponseFacade.sendError(ResponseFacade.java:473) ~[tomcat-embed-core-8.5.28.jar:8.5.28]
	at org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver.handleHttpMediaTypeNotAcceptable(DefaultHandlerExceptionResolver.java:299) ~[spring-webmvc-5.0.4.RELEASE.jar:5.0.4.RELEASE]
	at org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver.doResolveException(DefaultHandlerExceptionResolver.java:180) ~[spring-webmvc-5.0.4.RELEASE.jar:5.0.4.RELEASE]
	at org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver.resolveException(AbstractHandlerExceptionResolver.java:140) [spring-webmvc-5.0.4.RELEASE.jar:5.0.4.RELEASE]...

Never write like this! !

public static ResultVo<String> doDownLoadFile(File file, HttpServletResponse response) {
    
    
    try (InputStream in = new FileInputStream(file);
         OutputStream out = response.getOutputStream()) {
    
    
        byte[] buffer = new byte[1024];
        int len = 0;
        while ((len = in.read(buffer)) != -1) {
    
    
            out.write(buffer, 0, len);
        }
        log.info("【文件下载】文件下载成功");
        
        // 千万不要这样写,否则会报错
        return ResultVoUtil.success("【下载成功】");
    } catch (FileNotFoundException e){
    
    
    	....    
    }
}

Guess you like

Origin blog.csdn.net/sco5282/article/details/121275436#comments_28358188
Recommended