一、实现效果展示:
(1)三个目录,一个存放图片(png、jpg、gif..),一个存放文件(txt、word,pbf...),另一个存压缩文件(zip)
(2)spring-boot配置文件里面配置存放文件的目录路径,并指定一次性和总过上传文件的大小限制
(3)多文件上传前端简单UI展示
(4)分别选择要上传的文件
(5)调用后台接口,先分别为这三个文件进行信息注册
(6)格式化Json串
{
"status": 200,
"message": "成功",
"data": [
{
"fileUUID": "98ea0037-6d7b-4449-b00c-44d554cdc028",
"fileName": "zz",
"path": "F:/appleyk/files/b/5/",
"fileUrl": "F:/appleyk/files/b/5/98ea0037-6d7b-4449-b00c-44d554cdc028_zz.osm",
"ext": "osm",
"totalCount": 10463441,
"currentCount": 0,
"status": "stop",
"createDate": "2018-05-18 07:23:23",
"lastModifyDate": "2018-05-18 07:23:23",
"size": "9.98M",
"expire": false
}, {
"fileUUID": "fac533f7-7c00-4369-be86-020df87e33d0",
"fileName": "4",
"path": "F:/appleyk/images/f/6/",
"fileUrl": "F:/appleyk/images/f/6/fac533f7-7c00-4369-be86-020df87e33d0_4.png",
"ext": "png",
"totalCount": 54832,
"currentCount": 0,
"status": "stop",
"createDate": "2018-05-18 07:23:23",
"lastModifyDate": "2018-05-18 07:23:23",
"size": "53.55K",
"expire": false
}, {
"fileUUID": "0e10a14e-a4c3-48b1-b28c-6a137a127fc6",
"fileName": "china-latest",
"path": "F:/appleyk/files/e/1/",
"fileUrl": "F:/appleyk/files/e/1/7619dada-aebf-4ef5-abe5-62a39449740d_china-latest.pbf",
"ext": "pbf",
"totalCount": 425831210,
"currentCount": 0,
"status": "stop",
"createDate": "2018-05-18 07:23:24",
"lastModifyDate": "2018-05-18 07:23:24",
"size": "406.10M",
"expire": false
}
],
"timestamp": "2018-05-18 15:23:24"
}
(7)取出其中一个文件注册的fileUUID进行异步请求,后台拿到这个fileUUID后,先从Ehcache文件缓存中匹配,如果有,就将之前注册的文件信息里面的二进制数据data拿出来进行异步写入文件,效果如下
A、第一次请求 【fileUUID = 0e10a14e-a4c3-48b1-b28c-6a137a127fc6】
由于文件上传写入操作在后台是异步完成的,因此请求会很快返回,而不是等待文件全部上传完毕才返回
由于上一步文件已经写入完毕了,其状态status:“done”和 currentCount的值已经说明了这一切,因此,这里再请求一次,会将已经上传的文件的fileUUID从Ehcache文件缓存中移除掉,效果就是
(8)文件上传写入后的效果
思路说明:如果上传一个大小超过500M的文件,直接上传写入文件内容byte[]的方式,使得后台接收到前端的请求再到响应结果给前端会很耗时,比如,整个写入操作需要10s【其实也没有那么夸张】,这10s内,用户是不是还要等待,而且用户也不知道文件究竟上传了多少,也就是我们常说的文件上传进度。假设文件很大,那么用户等待的时间将会更长,因此,为了增强用户文件上传的体验效果以及实时返回大文件的上传进度,我们得采用异步请求的方式去后台拿到实时的文件上传的状态,前端有ajax异步请求,那么后端有没有呢? 答案是有的,在Spring-Boot中,我们只需要在Application入口处加上注解@EnableAsync即可开启异步调用,随后,只需在要异步执行的方法上加上注解@Async即可实现完整的后端业务方法的异步执行了。
补充说明:由于文件不是一下子就上传写入的,因此,异步上传文件的第一步需要注册文件信息,注册文件信息包括读文件的名称、文件的大小、文件的二进制byte[]数据、文件的后缀名、以及根据文件名取得字符串uuid【唯一】,其中一个uuid对应一个文件,我们拿到uuid后,将其放入Ehcache文件缓存中,以备第二步文件真正上传写入时用;第二步就是,前端拿到后台返回的文件注册信息后,依次根据文件对应的uuid进行异步请求,请求来到后端后,交由后端进行异步文件写入操作,而这个异步调用,会实时的给前端返回当前文件的上传进度以及文件上传状态信息。
二、项目目录结构图
三、pom依赖
<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.appleyk</groupId>
<artifactId>Spring-Boot-MultiFile-UpLoad</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<description>异步调用+ehcache缓存+单文件、多文件的上传</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.12.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<!-- optional=true,依赖不会传递 -->
<!-- 本项目依赖devtools;若依赖本项目的其他项目想要使用devtools,需要重新引入 -->
<optional>true</optional>
</dependency>
<!-- 缓存 -->
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
<!-- 添加thymeleaf 支持页面跳转 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
<include>**/*.html</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>
四、属性文件properties
server.port=8080
server.session.timeout=10
server.tomcat.uri-encoding=utf8
#在application.properties文件中引入日志配置文件
#===================================== log =============================
logging.config=classpath:logback-boot.xml
#开启多文件上传
spring.http.multipart.enabled=true
#单个文件上传不超过 2G
spring.http.multipart.max-file-size=2048Mb
#多文件上传,总文件大小不超过10G
spring.http.multipart.max-request-size=10240Mb
#图片存放路径
file.imageDir=F:/appleyk/images/
#文件存放路径
file.fileDir=F:/appleyk/files/
#压缩包存放路径
file.zipFileDir=F:/appleyk/zipfiles/
五、Spring-Boot开启异步调用
package com.appleyk;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.support.SpringBootServletInitializer;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class Application extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
}
六、Ehcache文件缓存配置
package com.appleyk.config;
import org.ehcache.CacheManager;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.appleyk.file.FileUploadState;
@Configuration
public class CacheConfig {
/*
* 文件上传缓存
*/
@Bean(name = "cacheFile")
public CacheManager cacheFile() {
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().
withCache("fileState",
CacheConfigurationBuilder.newCacheConfigurationBuilder(String.class, FileUploadState.class,
ResourcePoolsBuilder.heap(100)).build()).
build(true);
return cacheManager;
}
}
七、文件注册/上传状态类
package com.appleyk.file;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Date;
import com.appleyk.utils.FileUtils;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* 文件上传状态类 == 便于前后端交互
* @blob http://blog.csdn.net/appleyk
* @date 2018年5月18日-下午2:48:47
*/
public class FileUploadState {
private String fileUUID;
private String fileName;
private String path;
/**
* 文件的绝对url
*/
private String fileUrl;
private String ext;
@JsonIgnore
private byte[] data;
/**
* 文件的总字节数
*/
private Integer totalCount;
/**
* 当前文件上传的字节数 和 totalCount组成 进度条
*/
private Integer currentCount;
/**
* 文件上传的状态值
* 1.stop == 初始状态
* 2.start == 进行状态
* 3.done == 完成状态
*/
private String status;
/**
* 创建时间
*/
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")
private Date createDate;
/**
* 最后修改时间
*/
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")
private Date lastModifyDate;
/**
* 文件的大小【容量】
*/
private String size;
public static FileUploadState createFileUploadState(String fileName,String ext,String fileDir,byte[] data) {
FileUploadState result = new FileUploadState();
result.fileName = getFileName(fileName);
result.fileUUID = java.util.UUID.randomUUID().toString();
result.createDate = new Date();
result.lastModifyDate = new Date();
result.path = getDirPath(fileName, fileDir);
result.ext = ext;
result.data = data;
result.status = "stop";
result.size = FileUtils.getFileSize(data.length);
result.totalCount = data.length;
result.currentCount = 0;
return result;
}
public FileUploadState() {
this.lastModifyDate = new Date();
}
/**
* 1. 获取文件名称的hashCode:int hCode = name.hashCode();;
2. 获取hCode的低4位,然后转换成16进制字符;
3. 获取hCode的5~8位,然后转换成16进制字符;
4. 使用这两个16进制的字符生成目录链。例如低4位字符为“5”
采用hash算法来打散目录,防止一个目录上传的文件过多!
* @return
*/
public static String getDirPath(String fileName,String fileDir){
// 1.获取文件名的hashCode
int hCode = fileName.hashCode();
// 2.获取hCode的低4位,并转换成16进制字符串
String dir1 = Integer.toHexString(hCode & 0xF);
// 3.获取hCode的低5~8位,并转换成16进制字符串
String dir2 = Integer.toHexString(hCode >>> 4 & 0xF);
// 4.与文件保存目录连接成完整路径
String path = fileDir + dir1 + "/" + dir2+"/";
// 5.防止目录不存在,创建
new File(path).mkdirs();
return path;
}
public String getFileUUID() {
return fileUUID;
}
public void setFileUUID(String fileUUID) {
this.fileUUID = fileUUID;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getFileUrl() {
fileUrl = LocalPathAndFileName();
return fileUrl;
}
public void setFileUrl(String fileUrl) {
this.fileUrl = fileUrl;
}
public String getExt() {
return ext;
}
public void setExt(String ext) {
this.ext = ext;
}
public byte[] getData() {
return data;
}
public void setData(byte[] data) {
this.data = data;
}
public Integer getTotalCount() {
return totalCount;
}
public void setTotalCount(Integer totalCount) {
this.totalCount = totalCount;
}
public Integer getCurrentCount() {
return currentCount;
}
public void setCurrentCount(Integer currentCount) {
this.currentCount = currentCount;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Date getLastModifyDate() {
return lastModifyDate;
}
public void setLastModifyDate(Date lastModifyDate) {
this.lastModifyDate = lastModifyDate;
}
public String getSize() {
return size;
}
public void setSize(String size) {
this.size = size;
}
public Date getCreateDate() {
return createDate;
}
public void setCreateDate(Date createDate) {
this.createDate = createDate;
}
public String UUIDFileName() {
return fileUUID+ '_' +fileName;
}
/**
* 不要后缀
* @param fileName
* @return
*/
public static String getFileName(String fileName){
return fileName.substring(0, fileName.indexOf("."));
}
/**
* 为防止同名文件上传出现覆盖的问题,上传的文件名采用 UUID+_FileName
* @return
*/
public String LocalPathAndFileName() {
return path + fileUUID+ '_' +fileName+"."+ext;
}
// 获取解压缩文件路径
public String LocalUnCompressPath() {
File file = new File(path + UUIDFileName());
if (!file.exists()) {
file.mkdir();
}
return file.getPath();
}
/**
* 创建文件 == 先占个位置
*
* @return
*/
public boolean createFile() {
File file = new File(LocalPathAndFileName());
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(file);
} catch (IOException e) {
return false;
} finally {
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException e) {
}
}
}
return true;
}
/**
* 删除文件
*
* @return
*/
public boolean deleteFile() {
File file = new File(LocalPathAndFileName());
return file.delete();
}
/**
* 文件是否过期
*
* @return
*/
public boolean isExpire() {
Date currentDate = new Date();
long diff = currentDate.getTime() - lastModifyDate.getTime();
long seconds = diff / 1000;
//过期秒
return seconds > 60;
}
}
八、文件数据异步写入工具类
package com.appleyk.utils;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.Date;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import com.appleyk.file.FileUploadState;
@Component
public class FileWriteUtils {
/**
* 异步写入文件数据
*
* @param fileUploadState
* @throws Exception
*/
@Async
public void writeData(FileUploadState fileUploadState) throws Exception {
File file = new File(fileUploadState.LocalPathAndFileName());
FileOutputStream fileOutputStream = null;
fileOutputStream = new FileOutputStream(file, true);
byte[] data = fileUploadState.getData();
/**
* 分批写 一次性写入1024个字节
*/
InputStream is = new ByteArrayInputStream(data);
byte[] buffer = new byte[1024];
int len = 0;
/**
* 当前写入的进度
*/
int currentCount = 0;
/**
* 每次读将buffer填满,如果读完,返回-1
*/
while ((len = is.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, len);
currentCount += len;
fileUploadState.setCurrentCount(currentCount);
}
is.close();
fileOutputStream.close();
// 记录最后修改时间
fileUploadState.setLastModifyDate(new Date());
fileUploadState.setStatus("done");
}
}
九、多文件上传html页面
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>多文件上传</title>
</head>
<body>
<form method="POST" enctype="multipart/form-data" action="/appleyk/file/createmultifile">
<p>文件1:<input type="file" name="file" /></p>
<p>文件2:<input type="file" name="file" /></p>
<p>文件3:<input type="file" name="file" /></p>
<p><input type="submit" value="上传" /></p>
</form>
</body>
</html>
十、项目完整demo地址
我的GitHub:Spring-Boot+异步调用+ehcache缓存+单文件、多文件的上传