Spring Boot 之 编写一个可以上传和下载文件的web应用
最终效果
实现了一个可以上传和下载文件的单页面web应用
选择文件并上传:
上传成功之后顶部会有绿色的提示消息。
上传之后就可以通过自动生成的链接来访问下载,如果所示,也可以通过<img>
直接访问。
可能的用途
- 如果有服务器的话,可以把这个应用部署在服务器上,于是就有了一个私人订制的图床
- 如果有服务器的话,可以把这个应用部署在服务器上,于是就有了一个私人订制的云盘
- 如果有服务器的话,可以把这个应用部署在服务器上,于是就有了一个私人订制的XFTP
技术栈
- Spring Boot
- Maven
- Java IO
- Java Stream
- Html
- Bootstrap(美化页面所需,也可以不用)
步骤
创建项目
通过Spring Initializr创建一个Spring Boot项目:
需要的依赖项:Spring Web,ThymeLeaf。
最终的Pom文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| <?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.0.RELEASE</version> <relativePath/> </parent> <groupId>eternal.fire</groupId> <artifactId>uploading-files</artifactId> <version>0.0.1-SNAPSHOT</version> <name>uploading-files</name> <description>Demo project for Spring Boot</description>
<properties> <java.version>14</java.version> </properties>
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</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> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies>
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
</project>
|
创建Application类
为了启动Spring Boot应用,需要一个Application类。spring-boot-starter-thymeleaf
和spring-boot-starter-web
已经由Spring Boot自动配置完成,除此之外,我们还需要注册一个MultipartConfigElement类,但是多亏了有Spring Boot,一切都已经自动配置好了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| package eternal.fire.uploadingfiles;
import eternal.fire.uploadingfiles.storage.StorageProperties; import eternal.fire.uploadingfiles.storage.StorageService; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean;
@SpringBootApplication @EnableConfigurationProperties(StorageProperties.class) public class UploadingFilesApplication {
public static void main(String[] args) { SpringApplication.run(UploadingFilesApplication.class, args); }
@Bean CommandLineRunner init(StorageService storageService) { return args -> { storageService.deleteAll(); storageService.init(); }; } }
|
@SpringBootApplication
注解等价于:@Configuration
+@ComponentScan
+@EnableAutoConfiguration
。
@Configuration
:告诉Spring被标注的类里有需要注册的定制的@Bean。
@ComponentScan
:告诉Spring去寻找用@Component
、@Configuration
和@Service
标记的类并注册到Spring容器。
@EnableAutoConfiguration
:启动自动配置。
1 2 3 4 5 6 7
| @Bean CommandLineRunner init(StorageService storageService) { return args -> { storageService.deleteAll(); storageService.init(); }; }
|
在CommandLineRunner
类里覆写run方法,就可以在Spring Boot项目启动之后执行run方法内部的代码。上面使用了Lambda函数,其等价于:
1 2 3 4 5 6 7 8 9 10
| @Bean CommandLineRunner init(StorageService storageService) { return new CommandLineRunner() { @Override public void run(String... args) throws Exception { storageService.deleteAll(); storageService.init(); } }; }
|
这一步的目的是在Spring Boot项目启动的时候删除旧的文件夹,创建新的文件夹。
作为Spring Boot自动配置的一部分,MultipartConfigElement bean
会被自动创建并处于就绪状态。
创建上传文件的Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| package eternal.fire.uploadingfiles;
import eternal.fire.uploadingfiles.storage.StorageFileNotFoundException; import eternal.fire.uploadingfiles.storage.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.util.stream.Collectors;
@Controller public class FileUploadController { private final StorageService storageService;
@Autowired public FileUploadController(StorageService storageService) { this.storageService = storageService; }
@GetMapping("/") public String listUploadFiles(Model model) { model.addAttribute("files", storageService.loadAll().map(path -> MvcUriComponentsBuilder.fromMethodName(FileUploadController.class, "serveFile", path.getFileName().toString()).build().toUri().toString()).collect(Collectors.toList())); 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", String.format("You successfully uploaded %s!", file.getOriginalFilename())); return "redirect:/"; }
@ExceptionHandler public ResponseEntity<?> handleStorageFileNotFound(StorageFileNotFoundException exc) { return ResponseEntity.notFound().build(); } }
|
页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| <!DOCTYPE html> <html lang="en" xmlns:th="https://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>uploadForm</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.0/dist/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous"> <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.0/dist/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script> </head> <body>
<div class="container"> <h1 class="text-primary display-2 text-center" style="font-family: Lobster,serif">Upload Files</h1> <div th:if="${message}"> <h2 th:text="${message}" class="text-success"></h2> </div>
<div> <form method="post" enctype="multipart/form-data" action="/"> <table class="table table-bordered table-striped"> <tr> <td>File up to load:</td> <td><input type="file" name="file"/></td> </tr> <tr> <td></td> <td><input class="btn btn-primary" type="submit" value="Upload"/></td> </tr> </table> </form> </div>
<div> <ul class="list-group"> <li th:each="file:${files}" class="list-group-item"> <a th:href="${file}" th:text="${file}"></a> </li> </ul> </div> </div>
</body> </html>
|
这个页面包含三个部分:
- 可选的上传成功的提示信息
- 用来上传文件的表单
- 一个list用来显示已经上传的文件
StorageService类
需要一个StorageService类以便于Controller层和Storage层交互。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package eternal.fire.uploadingfiles.storage;
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(); }
|
StorageService的实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| package eternal.fire.uploadingfiles.storage;
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.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.util.Objects; 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 store(MultipartFile file) { try { if (file.isEmpty()) { throw new StorageException("Failed to store empty file " + file.getOriginalFilename()); } Files.copy(file.getInputStream(), this.rootLocation.resolve(Objects.requireNonNull(file.getOriginalFilename()))); } catch (IOException e) { throw new StorageException("Failed to store file " + file.getOriginalFilename(), e); } }
@Override public Stream<Path> loadAll() { try { return Files.walk(this.rootLocation, 1).filter(path -> !path.equals(this.rootLocation)).map(this.rootLocation::relativize); } 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()); }
@Override public void init() { try { Files.createDirectory(rootLocation); } catch (IOException e) { throw new StorageException("Could not initialize storage", e); } } }
|
配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package eternal.fire.uploadingfiles.storage;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("storage") public class StorageProperties {
private String location = "upload-dir";
public String getLocation() { return location; }
public void setLocation(String location) { this.location = location; }
}
|
异常类
1 2 3 4 5 6 7 8 9 10 11 12
| package eternal.fire.uploadingfiles.storage;
public class StorageException extends RuntimeException {
public StorageException(String message) { super(message); }
public StorageException(String message, Throwable cause) { super(message, cause); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package eternal.fire.uploadingfiles.storage;
import eternal.fire.uploadingfiles.storage.StorageException;
public class StorageFileNotFoundException extends StorageException {
public StorageFileNotFoundException(String message) { super(message); }
public StorageFileNotFoundException(String message, Throwable cause) { super(message, cause); } }
|
设置上传和下载的文件大小限制
修改application.yml文件为
1 2 3 4 5
| spring: servlet: multipart: max-file-size: 1024KB max-request-size: 1024KB
|
如果上传或者下载的文件超出了设置的大小,会出错(这个错误我并没有去处理)。
运行
本地测试可以选择在IDEA里直接运行,也可以用maven打包之后再运行。
服务器部署可以选择用maven打包之后用xftp上传到服务器运行。
总结
恭喜你!又写了一堆没有用的垃圾。