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);
}
}
MultipartProperties
、MultipartAutoConfiguration
,而不是使用其自带的MultipartProperties
、MultipartAutoConfiguration
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;
}
}
}
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");
}
}
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);
}
}
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);
}
}
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);
}
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);
}
}
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);
}
}
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
浏览器输入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测试文件下载成功