1、先上一个简单的工具类FileUtils

import org.springframework.util.StringUtils;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.text.MessageFormat;

/**
 * FileUtils
 *
 * @desc: TODO 类的设计目的、功能及注意事项
 * @version:
 * @createTime: 2020/5/3 17:10
 * @author: 
 */
public class FileUtils {

    /**
     * 文件下载的接口地址,与后面要写的文件下载接口相呼应
     */
    private final static String DOWNLOAD_API = "/api/file/download";

    /**
     * filePath 与后面要写的文件下载接口相呼应
     */
    private final static String FILE_PATH_KEY = "filePath";

    /**
     * rename 与后面要写的文件下载接口相呼应
     */
    private final static String RENAME_KEY = "rename";

    /**
     *
     */
    private final static String EMPTY_STRING = "";
    
    /**
     * forceMkdir
     * @desc: TODO 描述这个方法的功能、适用条件及注意事项
     * @author: 
     * @createTime: 2020/5/3 19:53
     * @param directory
     * @return: void
     */
    public static void forceMkdir(File directory){
        String message;
        if (directory.exists()) {
            if (!directory.isDirectory()) {
                message = "File " + directory + " exists and is " + "not a directory. Unable to create directory.";
                throw new BusinessException(message);
            }
        } else if (!directory.mkdirs() && !directory.isDirectory()) {
            message = "Unable to create directory " + directory;
            throw new BusinessException(message);
        }
    }
    /**
     * getFileSuffix
     * @desc: TODO 描述这个方法的功能、适用条件及注意事项
     * @author: 
     * @createTime: 2020/5/8 11:19
     * @param fileName
     * @return: java.lang.String
     */
    public static String getFileSuffix(String fileName){
        int index = fileName.lastIndexOf(".");
        return index>-1 ? fileName.substring(index) : EMPTY_STRING;
    }
    
    /**
     * getDownloadUrl
     * @desc: TODO 描述这个方法的功能、适用条件及注意事项
     * @author: 
     * @createTime: 2020/5/7 18:19
     * @param filePath 
     * @param rename 
     * @return: java.lang.String
     */
    public static String getDownloadUrl(String filePath, String rename){
        try {
            if(StringUtils.isEmpty(rename)){
                return MessageFormat.format("{0}?{1}={2}", DOWNLOAD_API, FILE_PATH_KEY, URLEncoder.encode(filePath, "UTF-8"));
            }
            return MessageFormat.format("{0}?{1}={2}&{3}={4}", DOWNLOAD_API, FILE_PATH_KEY, URLEncoder.encode(filePath, "UTF-8"), FileUtils.RENAME_KEY, URLEncoder.encode(rename, "UTF-8"));
        } catch (UnsupportedEncodingException e) {
            throw new BusinessException("build downloadPath error", e);
        }
    }

    /**
     * getDownloadUrl
     * @desc: TODO 描述这个方法的功能、适用条件及注意事项
     * @author: 
     * @createTime: 2020/5/7 18:19
     * @param filePath 
     * @return: java.lang.String
     */
    public static String getDownloadUrl(String filePath){
        return getDownloadUrl(filePath, null);
    }

}


2、自定义MultipartPropertiesMultipartAutoConfiguration,而不是使用其自带的MultipartPropertiesMultipartAutoConfiguration

import org.springframework.boot.autoconfigure.web.servlet.MultipartProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.util.Assert;
import org.springframework.util.unit.DataSize;

import javax.servlet.MultipartConfigElement;
import java.io.File;

/**
 * CustomMultipartProperties
 *
 * @desc: TODO 类的设计目的、功能及注意事项
 * @version:
 * @createTime: 2020/5/3 16:20
 * @author: 
 */
@ConfigurationProperties(
        prefix = "spring.servlet.multipart",
        ignoreUnknownFields = false
)
public class CustomMultipartProperties extends MultipartProperties {

    @Override
    public MultipartConfigElement createMultipartConfig() {
        //这里,我们将location视为文件存储路径,实际location为location下的temp文件夹
        String location = this.getLocation();
        Assert.hasLength(location, "spring.servlet.multipart.location must set");
        File folder = new File(location, "temp");
        FileUtils.forceMkdir(folder);

        MultipartConfigFactory factory = new MultipartConfigFactory();
        PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
        map.from(this.getFileSizeThreshold()).to(factory::setFileSizeThreshold);
        map.from(folder.getPath()).whenHasText().to(factory::setLocation);
        map.from((DataSize) null).to(factory::setMaxRequestSize);
        map.from((DataSize) null).to(factory::setMaxFileSize);
        return factory.createMultipartConfig();
    }

}

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import org.apache.tomcat.util.http.fileupload.servlet.ServletFileUpload;
import org.apache.tomcat.util.http.fileupload.servlet.ServletRequestContext;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.MultipartConfigElement;
import javax.servlet.Servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * CustomMultipartAutoConfiguration
 * @desc: TODO 类的设计目的、功能及注意事项
 * @version:
 * @createTime: 2020/5/3 16:31
 * @author: 
 */
@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class})
@ConditionalOnProperty(
    prefix = "spring.servlet.multipart",
    name = {"enabled"},
    matchIfMissing = true
)
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@EnableConfigurationProperties({CustomMultipartProperties.class})
@EnableAutoConfiguration(exclude = MultipartAutoConfiguration.class)
public class CustomMultipartAutoConfiguration {

    private final CustomMultipartProperties customMultipartProperties;

    public CustomMultipartAutoConfiguration(CustomMultipartProperties customMultipartProperties) {
        this.customMultipartProperties = customMultipartProperties;
    }

    @Bean
    @ConditionalOnMissingBean({MultipartConfigElement.class, CommonsMultipartResolver.class})
    public MultipartConfigElement multipartConfigElement() {
        return this.customMultipartProperties.createMultipartConfig();
    }

    @Bean(
        name = {"multipartResolver"}
    )
    @ConditionalOnMissingBean({MultipartResolver.class})
    public StandardServletMultipartResolver multipartResolver() {
        StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
        multipartResolver.setResolveLazily(this.customMultipartProperties.isResolveLazily());
        return multipartResolver;
    }

    @Bean
    public FileUploadHandler fileUploadHandler(){
        long maxFileSize = this.customMultipartProperties.getMaxFileSize().toBytes();
        return new FileUploadHandler(maxFileSize);
    }

    @AllArgsConstructor(access = AccessLevel.PRIVATE)
    static class FileUploadHandler implements HandlerInterceptor {

        private long maxFileSize;

        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            if(request!=null && ServletFileUpload.isMultipartContent(request)) {
                ServletRequestContext ctx = new ServletRequestContext(request);
                long requestSize = ctx.contentLength();
                if (requestSize > maxFileSize) {
                    throw new MaxUploadSizeExceededException(maxFileSize);
                }
            }
            return true;
        }
    }

}

3、添加FileUploadHandler拦截器,在文件超出大小时抛出异常

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * WebMvcConfig
 * @desc: TODO 类的设计目的、功能及注意事项
 * @version:
 * @createTime: 2020/4/24 11:37
 * @author: 
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private CustomMultipartAutoConfiguration.FileUploadHandler fileUploadHandler;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(fileUploadHandler).addPathPatterns("/api/file/upload");
    }

}

4、定义文件上传接口的返回值

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.util.UUID;

/**
 * UploadResult
 *
 * @desc: TODO 类的设计目的、功能及注意事项
 * @version:
 * @createTime: 2020/4/30 9:54
 * @author:
 */
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class UploadResult {

    /**
     * 文件名(原文件名称,文件存储到服务器时将对他重命名)
     */
    private final String fileName;

    /**
     * 文件后缀
     */
    private final String fileSuffix;

    /**
     * 文件大小
     */
    private final long fileSize;

    /**
     * 文件路径(相对于文件上传配置的存储目录)
     */
    private final String filePath;

    /**
     * 文件下载路径(附带rename参数,使下载的文件仍使用原文件名)
     */
    private final String downloadUrl;

    /**
     * newInstance
     * @desc: TODO 描述这个方法的功能、适用条件及注意事项
     * @author:
     * @createTime: 2020/4/30 15:02
     * @param multipartFile
     * @param folder
     * @return: UploadResult
     */
    public static UploadResult newInstance(MultipartFile multipartFile, String folder) {
        String fileName = multipartFile.getOriginalFilename();
        String fileSuffix = FileUtils.getFileSuffix(fileName);
        long fileSize = multipartFile.getSize();
        String filePath = (StringUtils.isEmpty(folder) ? "" : (folder +  File.separator)) + UUID.randomUUID().toString().replaceAll("-","") + fileSuffix;
        String downloadUrl = FileUtils.getDownloadUrl(filePath, fileName);
        return new UploadResult(fileName, fileSuffix, fileSize, filePath, downloadUrl);
    }

}


5、创建文件下载的帮助类DownloadHelp

import org.apache.catalina.connector.ClientAbortException;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.text.MessageFormat;

/**
 * DownloadOption
 * @desc: TODO 类的设计目的、功能及注意事项
 * @version:
 * @createTime: 2020/4/30 14:11
 * @author: 
 */
@Slf4j
public class DownloadHelp {

    private File file;

    private long startByte;

    private long endByte;

    private int httpServletResponseStatus;

    private long contentLength;

    private DownloadHelp(File file, long startByte, long endByte, int httpServletResponseStatus){
        this.file = file;
        this.startByte = startByte;
        this.endByte = endByte;
        this.httpServletResponseStatus = httpServletResponseStatus;
        this.contentLength = endByte - startByte + 1;
    }

    private DownloadHelp setHeaders(HttpServletResponse response, String rename) {
        if(httpServletResponseStatus>0){
            response.setStatus(httpServletResponseStatus);
        }
        String contentType = "application/octet-stream";
        response.setContentType(contentType);
        response.setHeader("Accept-Ranges", "bytes");
        response.setHeader("Content-Type", contentType);
        response.setHeader("Content-Length", String.valueOf(contentLength));
        response.setHeader("Content-Range", MessageFormat.format("bytes {0}-{1}/{2}", startByte, endByte, file.length()));
        try {
            response.setHeader("Content-Disposition", MessageFormat.format("attachment;filename={0}", URLEncoder.encode(StringUtils.isEmpty(rename) ? file.getName() : rename, "UTF-8")));
        } catch (UnsupportedEncodingException e) {
            log.warn("set Content-Disposition header error", e);
        }
        return this;
    }

    private void download(HttpServletResponse response) {
        BufferedOutputStream outputStream = null;
        RandomAccessFile randomAccessFile = null;
        long transmitted = 0;
        try {
            randomAccessFile = new RandomAccessFile(file, "r");
            outputStream = new BufferedOutputStream(response.getOutputStream());
            byte[] buff = new byte[4096];
            int len = 0;
            randomAccessFile.seek(startByte);
            while ((transmitted + len) <= contentLength && (len = randomAccessFile.read(buff)) != -1) {
                outputStream.write(buff, 0, len);
                transmitted += len;
            }
            if (transmitted < contentLength) {
                len = randomAccessFile.read(buff, 0, (int) (contentLength - transmitted));
                outputStream.write(buff, 0, len);
                transmitted += len;
            }
            outputStream.flush();
            log.info(MessageFormat.format("下载完毕:{0}-{1}:{2}", startByte, endByte, transmitted));
        } catch (ClientAbortException e) {
            log.error(MessageFormat.format("用户停止下载:{0}-{1}:{2}", startByte, endByte, transmitted));
        } catch (IOException e) {
            log.error(e.getMessage());
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    log.error(e.getMessage());
                }
            }
            if (randomAccessFile != null) {
                try {
                    randomAccessFile.close();
                } catch (IOException e) {
                    log.error(e.getMessage());
                }
            }
        }
    }

    public void download(HttpServletResponse response, String rename) {
        this.setHeaders(response, rename).download(response);
    }

    public static DownloadHelp newInstance(File file, String range){
        long startByte = 0;
        long endByte = file.length() - 1;
        int httpServletResponseStatus = 0;
        if (range != null && range.contains("bytes=") && range.contains("-")) {
            range = range.substring(range.lastIndexOf("=") + 1).trim();
            String[] ranges = range.split("-");
            if (range.startsWith("-")) {
                endByte = Long.parseLong(ranges[1]);
            }else if (range.endsWith("-")) {
                startByte = Long.parseLong(ranges[0]);
            }else {
                startByte = Long.parseLong(ranges[0]);
                endByte = Long.parseLong(ranges[1]);
            }
            httpServletResponseStatus = HttpServletResponse.SC_PARTIAL_CONTENT;
        }
        return new DownloadHelp(file, startByte, endByte, httpServletResponseStatus);
    }

}

6、定义文件服务接口IFileService

import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;

/**
 * IFileService
 *
 * @desc: TODO 类的设计目的、功能及注意事项
 * @version:
 * @createTime: 2020/4/30 9:52
 * @author: 
 */
public interface IFileService {

    /**
     * getAbsolutePath
     * @desc: TODO 描述这个方法的功能、适用条件及注意事项
     * @author: 
     * @createTime: 2020/5/7 15:26
     * @param filePath
     * @return: java.lang.String
     */
    String getAbsolutePath(String filePath);

    /**
     * upload
     * @desc: TODO 描述这个方法的功能、适用条件及注意事项
     * @author: 
     * @createTime: 2020/5/3 18:06
     * @param multipartFile
     * @param folder
     * @return: com.c503.smartecology.support.UploadResult
     */
    UploadResult upload(MultipartFile multipartFile, String folder);

    /**
     * upload
     * @desc: TODO 描述这个方法的功能、适用条件及注意事项
     * @author: 
     * @createTime: 2020/5/3 18:07
     * @param multipartFile
     * @return: com.c503.smartecology.support.UploadResult
     */
    default UploadResult upload(MultipartFile multipartFile){
        return upload(multipartFile, "");
    }

    /**
     * download
     * @desc: TODO 描述这个方法的功能、适用条件及注意事项
     * @author: 
     * @createTime: 2020/5/7 15:24
     * @param filePath
     * @param rename
     * @param range
     * @param response
     * @return: void
     */
    void download(String filePath, String rename, String range, HttpServletResponse response);
}


7、创建文件服务接口实现类FileServiceImpl

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;

/**
 * FileServiceImpl
 *
 * @desc: TODO 类的设计目的、功能及注意事项
 * @version:
 * @createTime: 2020/4/30 9:58
 * @author: 
 */
@Slf4j
@Service
public class FileServiceImpl implements IFileService {

    @Autowired
    private CustomMultipartProperties customMultipartProperties;

    @Override
    public String getAbsolutePath(String filePath) {
        return customMultipartProperties.getLocation() + File.separator + filePath;
    }

    @Override
    public UploadResult upload(MultipartFile multipartFile, String folder) {
        Assert.notNull(multipartFile, "未上传任何文件");
        UploadResult uploadResult = UploadResult.newInstance(multipartFile, folder);
        File file = new File(customMultipartProperties.getLocation(), uploadResult.getFilePath());
        FileUtils.forceMkdir(file.getParentFile());
        try {
            multipartFile.transferTo(file);
        } catch (IOException e) {
            throw new BusinessException("文件上传失败", e);
        }
        return uploadResult;
    }

    @Override
    public void download(String filePath, String rename, String range, HttpServletResponse response) {
        Assert.hasLength(filePath, "文件路径不能为空");
        DownloadHelp.newInstance(new File(customMultipartProperties.getLocation(), filePath), range).download(response, rename);
    }

}

8、创建文件上传下载的接口类FileApi

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;

/**
 * FileApi
 *
 * @desc: TODO 类的设计目的、功能及注意事项
 * @version:
 * @createTime: 2020/4/30 10:16
 * @author: 
 */
@RestController
@RequestMapping("/api/file")
public class FileApi {

    @Autowired
    private IFileService fileService;

    @PostMapping( value = "/upload")
    @ResponseBody
    public AjaxResult<UploadResult> upload(
            MultipartFile file,
            String folder
    ) {
        return AjaxResult.success(fileService.upload(file, folder));
    }

    @GetMapping("/download")
    public void download(
            @RequestParam("filePath") String filePath,
            @RequestParam(value = "rename", required = false) String rename,
            @RequestHeader(value = "Range", required = false) String range,
            HttpServletResponse response
    ){
        fileService.download(filePath, rename, range, response);
    }

}

9、在application.yml中配置文件上传的存储路径及文件大小限制

前面将location属性改为了上传文件的存储路径,真实的location设置为了这个路径下的temp文件夹,max-file-size属性也改为了在FileUploadHandler中的maxFileSize参数,若上传文件超出大小将抛出异常,异常处理在前面springboot2.2.6全局异常处理、统一返回值一文中已经提过

spring:
  servlet:
    multipart: #文件上传配置
      enabled: true
      location: E:\server-file
      max-file-size: 20MB

10、使用之前配置的swagger-ui测试接口

浏览器输入http://127.0.0.1:8080/swagger-ui.html找到我们上传接口,测试结果如下:
在这里插入图片描述

再复制downloadUrl到http://localhost:8080后面并在浏览器上打开即http://localhost:8080/api/file/download?filePath=2020%2F05%2F03%5C8298cb0abbbe41d1acbd7d5ed115e2dd.png&rename=xixian.png测试文件下载成功

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议