最近同事問我有沒有有關於技術的電子書,我開啟電腦上的小書庫,但是郵件發給他太大了,公司又禁止用資料夾共享,於是花半天時間寫了個小的檔案上傳程式,部署在自己的Linux機器上。
提供功能: 1 .檔案上傳 2.檔案列表展示以及下載
原有的上傳那塊很醜,寫了點js程式碼優化了下,最後介面顯示如下圖:
先給出成果,下面就一步步演示怎麼實現。
1.新建專案
首先當然是新建一個spring-boot工程,你可以選擇在網站初始化一個專案或者使用IDE的Spring Initialier功能,都可以新建一個專案。這裡我從IDEA新建專案:
下一步,然後輸入group和artifact,繼續點選next:
這時候出現這個模組選擇介面,點選web選項,勾上Web,證明這是一個webapp,再點選Template Engines選擇前端的模板引擎,我們選擇Thymleaf,spring-boot官方也推薦使用這個模板來替代jsp。 最後一步,然後等待專案初始化成功。2.pom設定
首先檢查專案需要新增哪些依賴,直接貼出我的pom檔案:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.shuqing28</groupId>
<artifactId>upload</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>upload</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.webjars/bootstrap -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>3.3.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.webjars.bower/jquery -->
<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>jquery</artifactId>
<version>2.2.4</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
複製程式碼
可以檢視到spring-boot-starter-thymeleaf
包含了webapp,最後兩個webjars整合了bootstrap和jquery,其它的等程式碼裡用到再說。
最後一個Spring boot maven plugin是系統建立時就新增的,它有以下好處:
1 . 它能夠打包classpath下的所有jar,構建成一個可執行的“über-jar”,方便使用者轉移服務
2 . 自動搜尋public static void main()
方法並且標記為可執行類
3 . 根據spring-boot版本,提供內建的依賴解釋。
3. 上傳檔案控制器
如果你只是使用SpringMVC上傳檔案,是需要配置一個MultipartResolver
的bean的,或者在web.xml
裡配置一個<multipart-config>
,不過藉助於spring-boot的自動配置,你什麼都不必做。直接寫控制器類,我們在src/main/java
下新建controller的package,並且新建FileUploadController:
package com.shuqing28.upload.controller;
import com.shuqing28.uploadfiles.pojo.Linker;
import com.shuqing28.uploadfiles.exceptions.StorageFileNotFoundException;
import com.shuqing28.uploadfiles.service.StorageService;
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.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
@Controller
public class FileUploadController {
private final StorageService storageService;
@Autowired
public FileUploadController(StorageService storageService) {
this.storageService = storageService;
}
@GetMapping("/")
public String listUploadedFiles(Model model)throws IOException {
List<Linker> linkers = storageService.loadAll().map(
path -> new Linker(MvcUriComponentsBuilder.fromMethodName(FileUploadController.class,
"serveFile", path.getFileName().toString()).build().toString(),
path.getFileName().toString())
).collect(Collectors.toList());
model.addAttribute("linkers", linkers);
return "uploadForm";
}
@GetMapping("/files/{filename:.+}")
@ResponseBody
public ResponseEntity<Resource> serveFile(@PathVariable String filename) {
Resource file = storageService.loadAsResource(filename);
return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + file.getFilename() + "\"").body(file);
}
@PostMapping("/")
public String handleFileUpload(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes) {
storageService.store(file);
redirectAttributes.addFlashAttribute("message",
"You successfully uploaded " + file.getOriginalFilename() + "!");
return "redirect:/";
}
@ExceptionHandler(StorageFileNotFoundException.class)
public ResponseEntity<?> handleStorageFileNotFound(StorageFileNotFoundException exc) {
return ResponseEntity.notFound().build();
}
}
複製程式碼
類定義處新增了@Controller
註解,證明這是一個Controller,每個方法前新增了@GetMapping
和@PostMapping
分別相應Get和Post請求。
首先是@GetMapping("/")
,方法listUploadedFiles,顧名思義,顯示檔案列表,這裡我們藉助於storageService遍歷資料夾下的所有檔案,並且用map方法提合成了連結和檔名列表,返回了一個Linker物件的陣列,Linker物件是一個簡單pojo,只包含下面兩部分:
private String fileUrl;
private String fileName;
複製程式碼
這個方法包含了對Java8中Stream的使用,如果有不理解的可以看看這篇文章Java8 特性詳解(二) Stream API.
接下來是@GetMapping("/files/{filename:.+}")
,方法是serveFile,該方法提供檔案下載功能,還是藉助於storageservice,後面會貼出storageservice的程式碼。最後使用ResponseEntity,把檔案作為body返回給請求方。
@PostMapping("/")
的handleFileUpload使用Post請求來上傳檔案,引數@RequestParam("file")
提取網頁請求裡的檔案物件,還是使用storageService來儲存物件,最後使用重定向來重新整理網頁,並且給出成功上傳的message。
4. 檔案處理
上面Controller呼叫的很多方法由StorageService提供,我們定義一個介面,包含以下方法:
package com.shuqing28.uploadfiles.service;
import org.springframework.core.io.Resource;
import org.springframework.web.multipart.MultipartFile;
import java.nio.file.Path;
import java.util.stream.Stream;
public interface StorageService {
void init();
void store(MultipartFile file);
Stream<Path> loadAll();
Path load(String filename);
Resource loadAsResource(String filename);
void deleteAll();
}
複製程式碼
因為我這裡只是藉助於本地檔案系統處理檔案的長傳下載,所以有了以下實現類:
package com.shuqing28.uploadfiles.service;
import com.shuqing28.uploadfiles.exceptions.StorageException;
import com.shuqing28.uploadfiles.exceptions.StorageFileNotFoundException;
import com.shuqing28.uploadfiles.config.StorageProperties;
import org.springframework.beans.factory.annotation.Autowired;
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 java.io.IOException;
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;
@Service
public class FileSystemStorageService implements StorageService {
private final Path rootLocation;
@Autowired
public FileSystemStorageService(StorageProperties properties) {
this.rootLocation = Paths.get(properties.getLocation());
}
@Override
public void init() {
try {
Files.createDirectories(rootLocation);
}
catch (IOException e) {
throw new StorageException("Could not initialize storage", e);
}
}
@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);
}
Files.copy(file.getInputStream(), this.rootLocation.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(this.rootLocation, 1)
.filter(path -> !path.equals(this.rootLocation))
.map(path->this.rootLocation.relativize(path));
}
catch (IOException e) {
throw new StorageException("Failed to read stored files", e);
}
}
@Override
public Path load(String filename) {
return rootLocation.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(rootLocation.toFile());
}
}
複製程式碼
這個類也基本運用了Java的NIO,使用Path物件定義了location用於檔案的預設儲存路徑。
先看store方法,store接受一個MultipartFile物件作為引數,想比於傳統JSP中只是傳二進位制位元組陣列,MultipartFile提供了很多方便呼叫的方法讓我們可以獲取到上傳檔案的各項資訊:
public interface MultipartFile extends InputStreamSource {
String getName();
String getOriginalFilename();
String getContentType();
boolean isEmpty();
long getSize();
byte[] getBytes() throws IOException;
InputStream getInputStream() throws IOException;
void transferTo(File dest) throws IOException, IllegalStateException;
}
複製程式碼
程式碼裡使用了Files的copy方法把檔案流拷到location對應的Path裡,當然我們也可以使用transferTo方法儲存檔案,file.transferTo(this.rootLocation.resolve(filename).toFile());
loadAll方法載入該路徑下的所有檔案Path資訊,loadAsResource則是載入檔案為一個Resource物件,再看Controller的程式碼,最後是接受一個Resource物件作為body返回給請求方。
5. 前端模板
最後定義了前端模板,這裡依舊先看程式碼:
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Share Files</title>
</head>
<body>
<div class="col-md-8 col-md-offset-2" th:if="${message}">
<h2 th:text="${message}"/>
</div>
<div class="col-md-8 col-md-offset-2">
<form method="POST" action="/" enctype="multipart/form-data">
<!-- COMPONENT START -->
<input type="file" name="file" class="input-ghost" style="visibility:hidden; height:0"/>
<div class="form-group">
<div class="input-group input-file" name="Fichier1">
<input type="text" class="form-control" placeholder='Choose a file...'/>
<span class="input-group-btn">
<button class="btn btn-default btn-choose" type="button">Choose</button>
</span>
</div>
</div>
<!-- COMPONENT END -->
<div class="form-group">
<button type="submit" class="btn btn-primary pull-right">Submit</button>
<button type="reset" class="btn btn-danger">Reset</button>
</div>
</form>
</div>
<div class="col-md-8 col-md-offset-2">
<ul>
<li th:each="linker: ${linkers}">
<a th:href="${linker.fileUrl}" th:text="${linker.fileName}" />
</li>
</ul>
</div>
<script src="//ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.min.js"></script>
<script src="/webjars/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<script type="text/javascript" th:inline="javascript">
function bs_input_file() {
$(".input-file").before(
function() {
if ( ! $(this).prev().hasClass('input-ghost') ) {
var element = $(".input-ghost");
element.change(function(){
element.next(element).find('input').val((element.val()).split('\\').pop());
});
$(this).find("button.btn-choose").click(function(){
element.click();
});
$(this).find("button.btn-reset").click(function(){
element.val(null);
$(this).parents(".input-file").find('input').val('');
});
$(this).find('input').css("cursor","pointer");
$(this).find('input').mousedown(function() {
$(this).parents('.input-file').prev().click();
return false;
});
return element;
}
}
);
}
$(function() {
bs_input_file();
});
</script>
<link rel="stylesheet" href="/webjars/bootstrap/3.3.5/css/bootstrap.min.css" />
</body>
</html>
複製程式碼
這裡重要的地方還是<form>
標籤內的內容,<form method="POST" action="/" enctype="multipart/form-data">
enctype一定要寫成multipart/form-data,使用POST上傳檔案,原有的上傳控制元件很醜,所以做了一個text+input放在表面,在下面放了一個隱形的上傳檔案的input,可以自己看看程式碼,本文就不囉嗦了。
下面還放了一個list用於展示檔案列表,這裡我們獲取到服務端提供的linkers物件,不斷foreach就可以獲得裡面的兩個元素fileUrl和fileName。
這裡jquery換成了微軟的CDN,webjars的總是引入不進來,不知道什麼原因。
其它設定
在src/main/resources/application.properties
裡設定上傳檔案大小限制
spring.http.multipart.max-file-size=128MB
spring.http.multipart.max-request-size=128MB
複製程式碼
另外在``還設定了檔案預設儲存路徑:
package com.shuqing28.uploadfiles.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("storage")
public class StorageProperties {
private String location = "/home/jenkins/upload-files/";
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
}
複製程式碼
這裡注意,由於StorageProperties的設定,在Application的那個類中要新增上
@EnableConfigurationProperties註解
@SpringBootApplication
@EnableConfigurationProperties(StorageProperties.class)
public class UploadApplication {
public static void main(String[] args) {
SpringApplication.run(UploadApplication.class, args);
}
}
複製程式碼
說到這專案基本可以執行了,當然你也可以新增需要的內容繼續完善它。