文件上传

这节的任务是做一个文件上传服务。

概况

参考链接

原文

thymeleaf

spring-mvc-flash-attributes

@ControllerAdvice

你构建的内容

分两部分,

  1. 服务端,由springboot构建。

  2. 客户端,是一个简单的html网页用来测试上传文件。

你需要的东西

  • 大约15min
  • 喜欢的编辑器或IDE(这里用IntelliJ)
  • jdk1.8+
  • Maven 3.2+ 或Gradle 4+

如何完成

跟着教程一步一步走。

通过Maven来构建

创建项目结构

mkdir -p src/main/java/hello,其实也就是在IntelliJ里面新建一个空的Java项目,然后添加一个main.java.hellopackage。

添加pom.xml文件。

<?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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion> <groupId>org.springframework</groupId>
<artifactId>gs-uploading-files</artifactId>
<version>0.1.0</version> <parent>
<groupId>org.springframework</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
</properties> <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.1.4.RELEASE</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 是java的服务端模板引擎。

Spring Boot Maven Plugin 有以下几个作用

  • 把项目打包成一个可执行的jar文件。
  • 搜索并标记public static void main()为可执行类。
  • 使用内置的依赖管理

之前设置的parent和dependency里面的version只指定了RELEASE,这里执行mvn compile的时候报了个错

[ERROR] [ERROR] Some problems were encountered while processing the POMs:
[WARNING] 'parent.version' is either LATEST or RELEASE (both of them are being deprecated) @ line 13, column 18
[WARNING] 'dependencies.dependency.version' for org.springframework.boot:spring-boot-starter-thymeleaf:jar is either LATEST or RELEASE (both of them are being deprecated) @ line 28, column 22

大意就是parent.version的LATEST 和RELEASE的值设置都是已经被废弃,所以,我们这里需要指定一下具体的版本。2.1.4.RELEASE

这个pom.xml配置在后面会报个错,Re-run Spring Boot Configuration Annotation Processor to update generated metadata,需要添加一个dependency

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

创建一个IStoreService

Controller 里面需要实现一些文件上传或者是读取的逻辑,我们可以在hello.storage包中创建一个IStorageService服务来处理这些。然后在controller中使用它。面向接口编程。

src/main/java/hello/storage/IStorageService.java

package hello.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 IStorageService {
void init(); void store(MultipartFile file);
//
Stream<Path> loadAll(); Path load(String fileName); Resource loadAsResource(String filename); void deleteAll();
}

创建一个StorageProperties

package hello.storage;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("storage")
public class StorageProperties { /**
* Folder location for storing files
*/
private String location = "upload-dir"; public String getLocation() {
return location;
} public void setLocation(String location) {
this.location = location;
} }

主要用来配置上传相关的设置,比如文件夹路径。

创建FileSystemStorageService实现这个IStoreService接口

src/main/java/hello/storage/FileSystemStorageService.java

/*
* Copyright (c) 2019.
* lou
*/ package hello.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.util.StringUtils;
import org.springframework.web.multipart.MultipartFile; 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; @Service
public class FileSystemStorageService implements IStorageService {
private final Path rootLocation; @Autowired
public FileSystemStorageService(StorageProperties storageProperties) {
this.rootLocation = Paths.get(storageProperties.getLocation());
} @Override
public void init() {
System.out.println("初始化");
try {
Files.createDirectories(rootLocation);
} catch (IOException e) {
throw new StorageException("无法初始化", e);
}
} @Override
public void store(MultipartFile file) {
String fileName = StringUtils.cleanPath(file.getOriginalFilename());
try {
if (file.isEmpty()) {
throw new StorageException("不能保存空文件" + fileName);
}
if (fileName.contains("..")) {
//相对路径安全检查
throw new StorageException("不能保存文件" + fileName + "到当前文件夹外");
}
try (InputStream inputStream = file.getInputStream()) {
Files.copy(inputStream, this.rootLocation.resolve(fileName), StandardCopyOption.REPLACE_EXISTING);
}
} catch (IOException e) {
new StorageException("保存文件失败:" + fileName, e);
} } @Override
public Stream<Path> loadAll() {
System.out.println("获取所有");
try {
return Files.walk(this.rootLocation, 1)
.filter(path -> !path.equals(this.rootLocation))
// .map(path -> rootLocation.relativize(path));
//::表示一个委托
.map(this.rootLocation::relativize);
} catch (IOException e) {
throw new StorageException("读取保存的文件失败", e);
}
} @Override
public Path load(String fileName) {
System.out.println("加载单个文件" + fileName + "路径");
return rootLocation.resolve(fileName);
} @Override
public Resource loadAsResource(String filename) {
System.out.println("返回" + filename + "Resource类型的内容");
try {
Path file = load(filename);
Resource resource = new UrlResource(file.toUri());
if (resource.exists() || resource.isReadable()) {
return resource;
}
throw new StorageFileNotFoundException("文件" + filename + "不存在"); } catch (MalformedURLException e) {
throw new StorageException("无法读取文件" + filename, e);
}
} @Override
public void deleteAll() {
System.out.println("删除所有");
FileSystemUtils.deleteRecursively(rootLocation.toFile()); }
}

这里需要把实现类加上@Service注解,。

定义一个StorageFileNotFound Exception

src/main/java/hello/storage/StorageFileNotFoundException.java

/*
* Copyright (c) 2019.
* lou
*/ package hello.storage; public class StorageFileNotFoundException extends RuntimeException {
public StorageFileNotFoundException(String message) {
super(message);
} public StorageFileNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}

创建一个文件上传controller

有了上面的IStorageService,下面就可以开始创建FileUploadController了。

src/main/java/hello/FileUploadController.java

/*
* Copyright (c) 2019.
* lou
*/ package hello; import hello.storage.IStorageService;
import hello.storage.StorageFileNotFoundException;
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 IStorageService storageService; @Autowired
public FileUploadController(IStorageService storageService) {
this.storageService = storageService;
} @GetMapping("/")
public String listUploadedFiles(Model model) {
model.addAttribute("files", storageService.loadAll().map(
path -> MvcUriComponentsBuilder.fromMethodName(
FileUploadController.class,
"serveFile",
path.getFileName().toString())
.build().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(MultipartFile file, RedirectAttributes redirectAttributes) {
storageService.store(file);
redirectAttributes.addFlashAttribute("message", "you successfuly uploaded " + file.getOriginalFilename() + "!");
return "redirect:/";
} @ExceptionHandler(StorageFileNotFoundException.class)
public ResponseEntity<?> handleStorageFileNotFound(StorageFileNotFoundException exc) {
return ResponseEntity.notFound().build();
} }

@Controller注解要加上。 构造函数上加上@AutoWired注解,Spring就会自动装配,因为构造函数上有IStorageService,Spring会去找实现这个类的@Service bean。然后定义几个方法,以及对应的路由。handleStorageFileNotFound方法用来处理当前controller出现的StorageFileNotFound异常。

  • GET /路由通过StorageService获取所有上传的文件列表,然后装载到Thymeleaf模板引擎中。通过MvcUriComponentsBuilder来计算得到实际的链接。
  • GET /files/{filename}加载资源,如果存在的话通过Content-Disposition头返回给浏览器用于下载。
  • POST /用于接收file,然后传递给storageService处理。

创建一个简单的HTML模板

src/main/resources/templates/uploadForm.html

<html xmlns:th="http://www.thymeleaf.org">
<body> <div th:if="${message}">
<h2 th:text="${message}"/>
</div> <div>
<form method="POST" enctype="multipart/form-data" action="/">
<table>
<tr><td>File to upload:</td><td><input type="file" name="file" /></td></tr>
<tr><td></td><td><input type="submit" value="Upload" /></td></tr>
</table>
</form>
</div> <div>
<ul>
<li th:each="file : ${files}">
<a th:href="${file}" th:text="${file}" />
</li>
</ul>
</div> </body>
</html>

有3点:

  • 第一个div中是可选的message参数,用来展示spring mvc设置的flash-scoped message
  • 第二个div用来给用户添加上传文件。
  • 第三个div显示所有的文件。

调节上传文件的相关限制

一般来说,我们会设置上传的文件大小。设想一下如果让spring去处理一个5G的文件上传。可以通过如下方法设置。

添加application.properties文件。

src/main/resources/application.properties

# Copyright (c) 2019.
# lou
# spring.servlet.multipart.max-file-size=128KB
spring.servlet.multipart.max-request-size=128KB

设置了最大文件大小和最大的请求大小,这样如果上传的文件太大,会获取到异常。

Whitelabel Error Page

This application has no explicit mapping for /error, so you are seeing this as a fallback.

Mon May 06 17:46:51 CST 2019

There was an unexpected error (type=Internal Server Error, status=500).

Maximum upload size exceeded; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (727520) exceeds the configured maximum (131072)

max-request-size里面包括所有input的内容,也就是说request-size≥file-size。

定义一个FileUploadExceptionAdvice来处理MaxUploadSizeExceededException

这个org.springframework.web.multipart.MaxUploadSizeExceededException在是无法在控制器里面获取到的,所以可以通过@ControllerAdvice来处理。

src/main/java/hello/storage/FileUploadExceptionAdvice.java

/*
* Copyright (c) 2019.
* lou
*/ package hello.storage; import org.apache.tomcat.util.http.fileupload.FileUploadBase;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.multipart.MaxUploadSizeExceededException; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; @ControllerAdvice
public class FileUploadExceptionAdvice {
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<ResponseResult> handleMaxSizeException(
MaxUploadSizeExceededException ex,
HttpServletRequest request,
HttpServletResponse response) {
long actual = request.getContentLengthLong();
long permitted = -1L;
if (ex.getCause() != null && ex.getCause().getCause() instanceof FileUploadBase.SizeLimitExceededException) {
FileUploadBase.SizeLimitExceededException causeEx = (FileUploadBase.SizeLimitExceededException) ex.getCause().getCause();
permitted = causeEx.getPermittedSize();
}
return ResponseEntity.ok(new ResponseResult("上传文件大小:"+actual + ",超过了最大:" + permitted, false));
}
}

通过MaxUploadSizeExceededException.getCause()获取内部的SizeLimitExceededException异常详细信息,再通过ResponseEntity.ok()返回json数据。

构建可执行程序

下面就到了写Application.java 的时候了。

src/main/java/hello/Application.java

/*
* Copyright (c) 2019.
* lou
*/ package hello; import hello.storage.IStorageService;
import hello.storage.StorageProperties;
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 Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
} @Bean
CommandLineRunner run(IStorageService storageService) {
System.out.println("进入run方法");
return args -> {
storageService.deleteAll();
storageService.init();
};
}
}

CommandLineRunner+@Bean确保程序启动的时候会运行。

@SpringBootApplication提供一下几点:

  • 表名这个configuration 类里面声明了一些@Bean方法。
  • 触发 auto-configuration
  • 开启 component 扫描.
  • 等于同时定义了 @Configuration, @EnableAutoConfiguration and @ComponentScan.

@EnableConfigurationProperties使得StorageProperties可以作为配置类。

运行输入 mvn spring-boot:run

打包输入mvn package。然后生成jar就可以用java -jar xxx.jar运行了。

最新文章

  1. MyEclipse10的一些问题(git插件,jdk7)
  2. 基于DDD的.NET开发框架 - ABP仓储实现
  3. 运维技能大全 | Devops Tools 周期表
  4. Codeforces Beta Round #91 (Div. 1 Only) E. Lucky Array 分块
  5. C++ 并发消息队列
  6. C# type - IsPrimitive
  7. HDU 5776 sum (BestCoder Round #85 A) 简单前缀判断+水题
  8. Linux 源码的安装 3个步骤
  9. iOS崩溃日志分析-b
  10. pandas 处理dataframe(一)
  11. cuda网格的限制
  12. BZOJ_2134_单选错位——期望DP
  13. phpmock测试
  14. Linux下Apache配置HTTPS功能
  15. logstash采集与清洗数据到elasticsearch案例实战
  16. 【多线程补充】SimpleDateFormat非线程安全与线程中、线程组中异常的处理
  17. LiteIDE 在 Windows 下为 Go 语言添加智能提示代码补全
  18. canvas 写一个刮刮乐抽奖
  19. spring mvc 数据转换
  20. POJ 2431 (优先队列)

热门文章

  1. Python extend函数解读
  2. jsp页面科学计数法显示问题的解决办法
  3. bootstrap table使用colResizable后表格不能自适应
  4. 【LeetCode】2. 两数相加
  5. Z从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存
  6. Python中的函数参数有冒号 声明后有-&gt; 箭头
  7. webservice调用天气服务
  8. Django 全局log process_exception中间件
  9. 使用AVFoundation完成照片拍摄存储相册, 开启关闭闪光灯, 切换摄像头
  10. [b0005] Linux 系统常用操作命令