FileUpload文件上传
是开发中经常遇到的事,通常都是网上copy一段代码来上传,可是你的代码足够完善吗,可以应对日益增长的文件需求吗,可以同时当上传和下载
服务器吗,今天让我们来跟着Spring官方
的Uploading Files教程进行优化和改造文件上传服务器
(适应于少量
文件上传,量大请使用DFS
)。
2020年3月9日更新:
1583765828605巴厘岛.xlsx
会变1583765828605魳_xlsx
的未知格式问题。在改方法直接返回一个URLEncoder UTF-8编码的文件名即可。 @GetMapping("/files/{filename:.+}")
@ResponseBody
public ResponseEntity<Resource> serveFile(@PathVariable String filename) throws UnsupportedEncodingException {
//加载文件
Resource file = storageService.loadAsResource(filename);
log.info("download file:"+file.getFilename());
//attachment附件下载模式,直接下载文件
return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + URLEncoder.encode(file.getFilename(), "UTF-8") + "\"").body(file);
}
核心代码如下:
application.yml
Controller
Service
&StorageServiceImpl
Excetpion
&StorageFileNotFoundExcetpion
HTML
server:
port: 9999
servlet:
context-path: /fileupload
tomcat:
remote-ip-header: x-forward-for
uri-encoding: UTF-8
max-threads: 10
background-processor-delay: 30
spring:
http:
encoding:
force: true
charset: UTF-8
application:
name: spring-cloud-study-fileupload
freemarker:
request-context-attribute: request
#prefix: /templates/
suffix: .html
content-type: text/html
enabled: true
cache: false
charset: UTF-8
allow-request-override: false
expose-request-attributes: true
expose-session-attributes: true
expose-spring-macro-helpers: true
#template-loader-path: classpath:/templates/
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
file:
devPath: C:\workspace\Temp\
prodPath: /dev/fileupload/
public class StorageException extends RuntimeException {
private static final long serialVersionUID = 1L;
public StorageException(String message) {
super(message);
}
public StorageException(String message, Throwable cause) {
super(message, cause);
}
}
public class StorageFileNotFoundException extends StorageException {
private static final long serialVersionUID = 1L;
public StorageFileNotFoundException(String message) {
super(message);
}
public StorageFileNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
public interface StorageService {
void init();
void store(MultipartFile file);
Stream<Path> loadAll();
Path load(String filename);
Resource loadAsResource(String filename);
void deleteAll();
Path getPath();
}
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.stream.Stream;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import com.softdev.system.demo.entity.StorageException;
import com.softdev.system.demo.entity.StorageFileNotFoundException;
/**
* FileUpload Service
* @author zhengkai.blog.csdn.net
* */
@Service
public class StorageServiceImpl implements StorageService {
//从application.yml中读取
@Value("${spring.file.devPath}")
private String devPath;
//从application.yml中读取
@Value("${spring.file.prodPath}")
private String prodPath;
//请勿直接使用path,应该用getPath()
private Path path;
@Override
public Path getPath() {
if(path==null) {
//如果在Window下,用dev路径,如果在其他系统,则用生产环境路径prodPath by zhengkai.blog.csdn.net
if(System.getProperty("os.name").toLowerCase().startsWith("win")) {
path = Paths.get(devPath);
}else {
path = Paths.get(prodPath);
}
}
return path;
}
@Override
public void store(MultipartFile file) {
String filename = StringUtils.cleanPath(file.getOriginalFilename());
try {
if (file.isEmpty()) {
throw new StorageException("Failed to store empty file " + filename);
}
if (filename.contains("..")) {
// This is a security check
throw new StorageException(
"Cannot store file with relative path outside current directory "
+ filename);
}
try (InputStream inputStream = file.getInputStream()) {
Files.copy(inputStream, getPath().resolve(filename),
StandardCopyOption.REPLACE_EXISTING);
}
}
catch (IOException e) {
throw new StorageException("Failed to store file " + filename, e);
}
}
@Override
public Stream<Path> loadAll() {
try {
return Files.walk(getPath(), 1)
.filter(path -> !path.equals(getPath()))
.map(getPath()::relativize);
}
catch (IOException e) {
throw new StorageException("Failed to read stored files", e);
}
}
@Override
public Path load(String filename) {
return getPath().resolve(filename);
}
@Override
public Resource loadAsResource(String filename) {
try {
Path file = load(filename);
Resource resource = new UrlResource(file.toUri());
if (resource.exists() || resource.isReadable()) {
return resource;
}
else {
throw new StorageFileNotFoundException(
"Could not read file: " + filename);
}
}
catch (MalformedURLException e) {
throw new StorageFileNotFoundException("Could not read file: " + filename, e);
}
}
@Override
public void deleteAll() {
FileSystemUtils.deleteRecursively(getPath().toFile());
}
@Override
public void init() {
try {
Files.createDirectories(getPath());
}
catch (IOException e) {
throw new StorageException("Could not initialize storage", e);
}
}
}
文件存储控制器包含以下REST API方法:
GET /
文件上传页面,基于Bootstrap+Freemarker,通过storageService.loadAll()
浏览存储目录文件功能,通过MvcUriComponentsBuilder.fromMethodName
映射文件提供下载功能(关于该功能更多详情请见附录部分)。GET /files/{filename}
文件下载URL,如果文件存在则下载,然后返回"Content-Disposition:attachment; filename=/.../"
的响应头Response Header
,在浏览器中触发文件下载。POST /
文件上传方法,通过StorageService.store(file)
提供上传功能。
import java.io.IOException;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.alibaba.fastjson.JSON;
import com.softdev.system.demo.entity.StorageFileNotFoundException;
import com.softdev.system.demo.service.StorageService;
@RestController
@RequestMapping("/storage")
/**
* SpringBoot2FileUpload文件上传
* @author zhengkai.blog.csdn.net
* */
public class StorageController {
@Autowired
private StorageService storageService;
@GetMapping("/files")
public ModelAndView listUploadedFiles(ModelAndView modelAndView) throws IOException {
//返回目录下所有文件信息
modelAndView.addObject("files", storageService.loadAll().map(
path -> MvcUriComponentsBuilder.fromMethodName(StorageController.class,
"serveFile", path.getFileName().toString()).build().toString())
.collect(Collectors.toList()));
//返回目录信息
modelAndView.addObject("path",storageService.getPath());
modelAndView.setViewName("uploadForm");
//查看ModelAndView包含的内容
System.out.println(JSON.toJSONString(modelAndView));
return modelAndView;
}
@GetMapping("/files/{filename:.+}")
@ResponseBody
public ResponseEntity<Resource> serveFile(@PathVariable String filename) {
//加载文件
Resource file = storageService.loadAsResource(filename);
log.info("download file:"+file.getFilename());
//attachment附件下载模式,直接下载文件
return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + URLEncoder.encode(file.getFilename(), "UTF-8") + "\"").body(file);
}
@PostMapping("/files")
public ModelAndView handleFileUpload(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes) {
//存储文件
storageService.store(file);
//返回成功消息
redirectAttributes.addFlashAttribute("message",
"恭喜你,文件" + file.getOriginalFilename() + "上传成功!");
return new ModelAndView("redirect:/storage/files");
}
@ExceptionHandler(StorageFileNotFoundException.class)
public ResponseEntity<?> handleStorageFileNotFound(StorageFileNotFoundException exc) {
return ResponseEntity.notFound().build();
}
}
<html>
<head>
<meta charset="utf-8">
<link href="//cdn.staticfile.org/twitter-bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet">
<script src="//cdn.staticfile.org/jquery/3.3.1/jquery.min.js">script>
<script src="//cdn.staticfile.org/twitter-bootstrap/4.1.1/js/bootstrap.min.js">script>
head>
<body>
<#if message??>
${message!!}
#if>
<div>
<form method="POST" enctype="multipart/form-data" action="${request.contextPath}/storage/files">
<table>
<tr><td><input value="选择文件" type="file" name="file" class="btn btn-primary"/>td>tr>
<tr><td><input type="submit" value="上传" class="btn btn-primary"/>td>tr>
table>
form>
div>
<div>
<p>存储目录${path!!}有以下文件,可点击下载:p>
<div class="list-group">
<#list files as file>
<a href="${file!!}" class="list-group-item list-group-item-action">${file!!}a>
#list >
div>
div>
body>
html>
http://localhost:9999/fileupload/storage/files
SpringMvc4提供的新功能,MvcUriComponentsBuilder官方DOC文档,功能如下:
方法 | 功能 |
---|---|
UriComponentsBuilder.fromMethodName(UriComponentsBuilder builder, Class> controllerType, String methodName, Object… args) | 通过Controller(控制器名或类)和Method(是方法名不是mapping名),映射该方法到URL上面 |
例如上文DEMO中
MvcUriComponentsBuilder.fromMethodName(StorageController.class,"serveFile", path.getFileName().toString())
,
代表
http://domain:post(application.yml的server.port)
+
/basePath(application.yml的server.servlet.context-path)
+
/Controller(根据控制器名或类找到对应的Mapping名)
+
/Method的Mapping(例如“serveFile”method的mapping是@GetMapping("/files/{filename:.+}"),则附加上Object... args所有的参数进去作为参数,获得最终的url)
得到最终访问该方法的url。