前言
FileUpload文件上傳
是開發中經常遇到的事,通常都是網上copy一段代碼來上傳,可是你的代碼足夠完善嗎,可以應對日益增長的文件需求嗎,可以同時當上傳和下載
服務器嗎,今天讓我們來跟着Spring官方
的Uploading Files教程進行優化和改造文件上傳服務器
(適應於少量
文件上傳,量大請使用DFS
)。
項目結構
核心代碼如下:
application.yml
- Storage
Controller
- Storage
Service
&StorageServiceImpl
- Storage
Excetpion
&StorageFileNotFoundExcetpion
HTML
Application.yml
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/
Exception
- StorageException
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);
}
}
- StorageFileNotFoundException
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);
}
}
Service
- Interface 接口
public interface StorageService {
void init();
void store(MultipartFile file);
Stream<Path> loadAll();
Path load(String filename);
Resource loadAsResource(String filename);
void deleteAll();
Path getPath();
}
- Implement 實現
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);
}
}
}
StorageController
文件存儲控制器包含以下REST API方法:
GET /
文件上傳頁面,基於Bootstrap+Freemarker,通過storageService.loadAll()
瀏覽存儲目錄文件功能,通過MvcUriComponentsBuilder.fromMethodName
映射文件提供下載功能(關於該功能更多詳情請見附錄部分)。GET /files/{filename}
文件下載URL,如果文件存在則下載,然後返回"Content-Disposition:attachment; filename=/.../"
的響應頭ResponseHeader
,在瀏覽器中觸發文件下載。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);
//attachment附件下載模式,直接下載文件
return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + file.getFilename() + "\"").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
<html>
<head>
<meta charset="utf-8">
<!-- Bootstrap 4 -->
<link href="//cdn.staticfile.org/twitter-bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet">
<!-- jQuery -->
<script src="//cdn.staticfile.org/jquery/3.3.1/jquery.min.js"></script>
<!-- Bootstrap -->
<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>
效果展示
UriComponentsBuilder.fromMethodName
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。