最近同事问我有没有有关于技术的电子书,我打开电脑上的小书库,但是邮件发给他太大了,公司又禁止用文件夹共享,于是花半天时间写了个小的文件上传程序,部署在自己的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文件:
"1.0" encoding="UTF-8"?>
"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">
4.0.0
com.shuqing28
upload
0.0.1-SNAPSHOT
jar
upload
Demo project for Spring Boot
org.springframework.boot
spring-boot-starter-parent
1.5.9.RELEASE
UTF-8
UTF-8
1.8
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-configuration-processor
true
org.springframework.boot
spring-boot-starter-test
test
org.webjars
bootstrap
3.3.5
org.webjars.bower
jquery
2.2.4
org.springframework.boot
spring-boot-maven-plugin
复制代码
可以查看到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
里配置一个
,不过借助于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 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 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 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 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. 前端模板
最后定义了前端模板,这里依旧先看代码:
"http://www.thymeleaf.org">
Share Files
"col-md-8 col-md-offset-2" th:if="${message}">
"${message}"/>
"col-md-8 col-md-offset-2">
"col-md-8 col-md-offset-2">
- "linker: ${linkers}">
"${linker.fileUrl}" th:text="${linker.fileName}" />
"stylesheet" href="/webjars/bootstrap/3.3.5/css/bootstrap.min.css" />
复制代码
这里重要的地方还是标签内的内容,
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);
}
}
复制代码
说到这项目基本可以运行了,当然你也可以添加需要的内容继续完善它。