Spring-Boot+异步(Aysn)调用+Ehcache本地缓存+实现多(大)文件的上传

一、实现效果展示:



(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】


由于文件上传写入操作在后台是异步完成的,因此请求会很快返回,而不是等待文件全部上传完毕才返回





B、第二次请求 【fileUUID = 0e10a14e-a4c3-48b1-b28c-6a137a127fc6】,依然是这个fileUUID




C、第三次请求 【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;
    }   
}


七、文件注册/上传状态类



FileUploadState.java


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;
    }

   
}



八、文件数据异步写入工具类


FileWriteUtils.java


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缓存+单文件、多文件的上传



猜你喜欢

转载自blog.csdn.net/appleyk/article/details/80364843