文件上传

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

概况

参考链接

原文

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运行了。

[SpingBoot guides系列翻译]文件上传的更多相关文章

  1. JS组件系列——Bootstrap文件上传组件:bootstrap fileinput

    前言:之前的三篇介绍了下bootstrap table的一些常见用法,发现博主对这种扁平化的风格有点着迷了.前两天做一个excel导入的功能,前端使用原始的input type='file'这种标签, ...

  2. HTML5 进阶系列:文件上传下载

    前言 HTML5 中提供的文件API在前端中有着丰富的应用,上传.下载.读取内容等在日常的交互中很常见.而且在各个浏览器的兼容也比较好,包括移动端,除了 IE 只支持 IE10 以上的版本.想要更好地 ...

  3. Oracle Apex 有用笔记系列 2 - 文件上传管理

    1. 页面设计 页面A有若干region, 当中一个region用于文件列表管理(包含显示,下载.删除).如图A. 在页面A有一button,点击它会调用页面B,页面B负责文件上传.如图B. 图A 图 ...

  4. form表单系列中文件上传及预览

    文件上传及预览 Form提交 Ajax 上传文件 时机: 如果发送的[文件]:->iframe, jQurey(),伪Ajax 预览 import os img_path = os.path.j ...

  5. [SpingBoot guides系列翻译]Redis的消息订阅发布

    Redis的消息 部分参考链接 原文 CountDownLatch 概述 目的 这节讲的是用Redis来实现消息的发布和订阅,这里会使用Spring Data Redis来完成. 这里会用到两个东西, ...

  6. ASP.NET MVC5+EF6+EasyUI 后台管理系统(32)-swfupload多文件上传[附源码]

    系列目录 文件上传这东西说到底有时候很痛,原来的asp.net服务器控件提供了很简单的上传,但是有回传,还没有进度条提示.这次我们演示利用swfupload多文件上传,项目上文件上传是比不可少的,大家 ...

  7. SpringBoot文件上传异常之提示The temporary upload location xxx is not valid

    原文: 一灰灰Blog之Spring系列教程文件上传异常原理分析 SpringBoot搭建的应用,一直工作得好好的,突然发现上传文件失败,提示org.springframework.web.multi ...

  8. JSP 文件上传下载系列之二[Commons fileUpload]

    前言 关于JSP 文件上传的基础和原理在系列一中有介绍到. 这里介绍一个很流行的组件commons fileupload,用来加速文件上传的开发. 官方的介绍是:  让添加强壮,高性能的文件到你的se ...

  9. 补习系列(11)-springboot 文件上传原理

    目录 一.文件上传原理 二.springboot 文件机制 临时文件 定制配置 三.示例代码 A. 单文件上传 B. 多文件上传 C. 文件上传异常 D. Bean 配置 四.文件下载 小结 一.文件 ...

随机推荐

  1. GPG 密码修改

    一.前言 相信大家在使用gpp的时候都会遇到这样子都情况: 忘记密码 想要定时更换密码,保证安全 此时不用担心,gpg  的密码更新特别简单. 二.步骤说明 1> 执行命令获 gpg2 --li ...

  2. Python连载46-XML文件修改创建

    一.XML文件写入 1.更改 (1)ele.set:修改属性 (2)ele.remove:删除元素. (3)ele.append:添加子元素. 我们举个例子并且使用新建的XML和新学的方法 impor ...

  3. Python爬虫教程-使用chardet

    Spider-03-使用chardet继续学习python爬虫,我们经常出现解码问题,因为所有的页面编码都不统一,我们使用chardet检测页面的编码,尽可能的减少编码问题的出现 网页编码问题解决使用 ...

  4. Jmeter 压测使用以及参数介绍

    . 下载地址 https://jmeter.apache.org/download_jmeter.cgi Binaries¶ 下的apache-jmeter-5.2.1.zipsha512pgp . ...

  5. php捕获Fatal error错误与异常处理

    php中的错误和异常是两个不同的概念. 错误:是因为脚本的问题,比如少写了分号,调用未定义的函数,除0,等一些编译语法错误. 异常:是因为业务逻辑和流程,不符合预期情况,比如验证请求参数,不通过就用 ...

  6. PHP判断设备访问来源

    /** * 判断用户请求设备是否是移动设备 * @return bool */ function isMobile() { //如果有HTTP_X_WAP_PROFILE则一定是移动设备 if (is ...

  7. PlayJava Day007

    今日所学: /* 2019.08.19开始学习,此为补档. */ 1.String类 实例化:①String name1 = "张三" ; ②String name2 = new ...

  8. HTTP面试常见题

    1.HTTP2.0.1.1.1.0.0.9的区别? 答:HTTP0.9:是HTTP协议的第一个版本,只允许发送get请求,并且不支持请求头.一次请求对应一次响应.是短连接. HTTP1.0:相比于0. ...

  9. JavaScript初探 一(认识JavaScript)

    JavaScript 初探 JavaScript插入HTML中 内嵌的Js代码 <!DOCTYPE html> <html> <head> <meta cha ...

  10. Dynamics 365中计算字段与Now进行计算实体导入报错:You can't use Now(), which is of type DateTime, with the current function.

    微软动态CRM专家罗勇 ,回复338或者20190521可方便获取本文,同时可以在第一间得到我发布的最新博文信息,follow me. 计算字段是从Dynamics CRM 2015 SP1版本开始推 ...