公司产品小伙伴画了原型后,需要上传到服务器上供开发查看;由于文件数量很多,100M左右差不多要传30分钟,这期间在替换文件导致原型无法正常查看,耽误开发小伙伴时间;随后建议他们将文件压缩为zip压缩包,基本上传也就30秒左右;但是产品小伙伴不会linux命令,也担心他们将服务器弄的“一塌糊涂”,所以就想着做个简单的zip文件上传并自动解压的简单功能。
二、知识点
SpringBoot 项目上传默认单个文件1M,一次上传请求大小10M,上传的文件超过了这个限制就会报错。
如果上传的文件不是特别大,或者在内网上传,那么就直接修改这个配置即可,web端和服务端开发都会比较简单(本文就是采用这个方式)。但是缺点就是上传时间可能比较长,如果设置了超时,会存在上传超时。
application.yml 配置:
spring:
servlet:
multipart:
# 配置 -1 为无限制
max-file-size: 100MB
max-request-size: 100MB
如果上传的文件特别大,并且希望快速上传,可以使用百度的 WebUploader JS 插件对文件进行分片上传,服务端顺序写入到文件中。这个功能可以后续写篇博文。
Spring 框架做文件上传,都是封装到 MultipartFile 对象中的。保存到文件最简单的就是使用 MultipartFile.transferTo(file) 方法。
当然也可以自己拿到文件流读取然后写入到zip文件中,注意,一定要用Zip流操作,用普通的流写的zip不可用。
使用 zip4j 工具包,解压超级简单,2行代码就搞定了。
不使用 form 表单上传,而是使用 ajax 上传,就不用做两个页面了。ajax 上传文件就需要使用内置的 FormData 对象来作为数据体对象,完成文件的上传。
三、代码实现
<!-- spring boot 父包 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath/>
</parent>
<!-- 项目依赖 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>net.lingala.zip4j</groupId>
<artifactId>zip4j</artifactId>
<version>2.9.1</version>
</dependency>
</dependencies>
application.yml 配置见上方。
2、web 代码
index.html 代码,省略不重要的标签,节约篇幅
<!-- Bootstrap 和 treeview CSS 文件 -->
<link rel="stylesheet" href="libs/bootstrap.min.css">
<link rel="stylesheet" href="libs/bootstrap-treeview.min.css">
<!-- body -->
<div class="container">
<h2>原型部署工具</h2>
<div>说明:1、选择要部署的路径;2、选择部署的zip文件;3、点击部署</div>
<div class="row">
<div class="col-md-8">
<div class="form-group">
<label for="tree">部署路径</label>
<div id="tree"></div>
<div class="form-group">
<label for="file">zip文件</label>
<input type="file" id="file" accept=".zip" required>
<p class="help-block">仅支持zip文件,且不超过100MB</p>
<button type="button" class="btn btn-default" id="bt" disabled="disabled" onclick="deploy()">提交</button>
<!-- js 依赖 -->
<script src="libs/jquery-3.6.0.min.js"></script>
<script src="libs/bootstrap.min.js"></script>
<script src="libs/bootstrap-treeview.min.js"></script>
<script src="index.js"></script>
index.js 简略版
var $tree = $('#tree');
var $file = $('#file');
// 用于校验的字段
var deployPath;
var filename;
// 加载可部署的目录树
$.ajax({
url: "deploy/path/tree",
type: "get",
success: function (data) {
$tree.treeview({
data: [data],
onNodeSelected: function (event, data) { // 选择树节点事件
deployPath = data.path;
$("button").removeAttr("disabled");
error: function (err) {
console.error(err);
alert("加载部署路径 tree 错误");
$file.on("change", function () { // 文件选择事件
filename = $file.val();
// 上传文件
function deploy() {
// 校验字段省略
// 使用 ajax 上传文件,需要用 FormData 对象
var formData = new FormData();
formData.append("path", deployPath);
formData.append("file", $file[0].files[0]); // 要上传的文件
$.ajax({
url: "deploy/upload",
type: "post",
data: formData,
contentType: false,
processData: false,
success: function (data) {
console.log("部署完成");
error: function (err) {
console.error(err);
3、 java服务端代码
Controller 代码(省略部分不重要的代码)
package com.chc.yx.deploy.web;
import com.chc.yx.deploy.bean.PathResult;
import com.chc.yx.deploy.bean.UploadFile;
import net.lingala.zip4j.ZipFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;
* 部署控制器
* @author chc
* @date 2021/12/31
* @since 1.0
@RestController
@RequestMapping("/deploy")
public class DeployController implements InitializingBean {
private Logger log = LoggerFactory.getLogger(getClass());
// 部署基础路径,只能部署在这个目录及其子目录下
@Value("${deploy.basePath}")
private String basePath;
// 解压的编码:解决linux中文乱码
@Value("${deploy.charset}")
private String charset;
* 读取指定目录下的所有子目录路径名称
@GetMapping("/path/tree")
public ResponseEntity<PathResult> pathTree() {
Path path = Paths.get(basePath);
// ...省略代码:递归扫描basePath路径下的子目录,以对应的层级封装到PathResult中,供treeview显示可以部署的路径...
return ResponseEntity.ok(pathResult);
* 上传文件
* @param UploadFile 自定义的参数类
@PostMapping("/upload")
public ResponseEntity upload(UploadFile uploadFile) {
// 部署的路径
String path = uploadFile.getPath();
MultipartFile file = uploadFile.getFile();
// zip文件名称
String originalFilename = file.getOriginalFilename();
log.info("部署信息:path = {}, name = {}", path, originalFilename);
Path zipFilePath = Paths.get(path, originalFilename);
// ... 省略代码:删除历史同名 zip 文件 ...
try {
// 使用 MultipartFile.transferTo 保存zip文件
file.transferTo(zipFilePath);
log.info("保存{}成功", originalFilename);
} catch (IOException e) {
log.error("保存" + zipFilePath + "错误", e);
return ResponseEntity.status(500).body("保存" + zipFilePath + "错误");
// 删除非压缩文件
String targetFilename=originalFilename.substring(0,originalFilename.length()-4);
Path filePath = Paths.get(path, targetFilename);
if (Files.exists(filePath)) {
AtomicBoolean result = new AtomicBoolean(true);
deleteDirectoryAll(filePath, result);
log.info("删除{}下的文件结果:{}", targetFilename, result.get());
// 使用 zip4j 解压:首先指定 zip 文件
try (ZipFile zipFile = new ZipFile(zipFilePath.toString())) {
// 指定编码集,主要是解决linux系统上中文乱码
zipFile.setCharset(Charset.forName(charset));
// 指定要解压到的目录下,并解压
zipFile.extractAll(filePath.getParent().toString());
log.info("解压{}成功", originalFilename);
} catch (IOException e) {
log.error("解压zip错误", e);
return ResponseEntity.status(500).body("解压zip文件错误");
// 删除zip文件
try {
Files.delete(zipFilePath);
} catch (IOException e) {
log.info("删除{}成功", originalFilename);
log.info("部署完成");
return ResponseEntity.ok("success");
* 递归删除目录和下面的文件————java api删除目录的话,目录必须是空的才能删除
private void deleteDirectoryAll(Path path, final AtomicBoolean result) {
try (Stream<Path> stream = Files.list(path)) {
stream.forEach(p -> {
if (Files.isDirectory(p)) {
deleteDirectoryAll(p, result);
try {
Files.delete(p);
} catch (IOException e) {
log.error("删除文件错误", e);
result.set(false);
} catch (IOException e) {
log.error("list path error", e);
result.set(false);
上面Controller中提供了两个配置参数:
1、deploy.basePath 用于指定可以部署的根目录
2、deploy.charset 用于指定解码的字符集,主要是用于解决 linux 系统上中文文件名的乱码问题的;在linux上如果使用 UTF-8 解码存在中文乱码,则可以使用 CP936 编码。
zip.file(‘00E4DAC2282E50A0_中铁二局六公司乐西合肥成都地铁13号线赣新德阳保罗昭西南昌合海南振源建材有限公司202302941.xlsx’,file);// 压缩类型选择nodebuffer,在回调函数中会返回zip压缩包的Buffer的值,再利用fs保存至本地。// ArrayBuffer转File。// 空的formData实例对象。// 给formData添加数据。// 给压缩文件里添加文件。
程序测试可用,直接解压导入到工程就可以,bat文件跟shell文件是用于在window跟linux上直接执行的脚本
我把开发的配置文档附上:
1.程序为定时任务,任务执行时间在bin目录下的配置文件mergeFilleUtil.properties中配置,在配置文件中,TASK_PERIOD表示任务执行时间间隔,单位为妙,如一天的时间间隔配置是86400,TASK_BEGIN_HOUR表示任务开始的小时时间,比如9点,TASK_BEGIN_MINUTE表任务开始的分钟,比如30分。
2. 程序用log4j记录日志,日志分正常信息跟错误信息两个级别,日志文件存放在log4j文件夹下。考虑到文件很多,日志解压、移动文件每解压、移动1000个记录一次,合并、删除文件每合并、删除50000个记录一次,
3. 启动任务前需配置文件解压合并的路径,本程序需配置的路径如下:
1). PROVINCE_DIR:原始文件存放的路径,必须配置到省的上一级路径,比如存放安徽省的文件路径为E:\test\rootfile\anhui,那么文件的路径必须配置为E:\test\rootfile,否则不能正确显示合并结果;
2). UN_ZIP_PATH:存放解压后的文件的路径;
3). OUT_PATH:存放合并后的文件路径;
4). DONE_FILE_PATH:存放已经解压处理过的文件;
5). DELETE_PATH:配置程序运行结束后欲删除文件的路径,如想删除多个文件夹下的文件,路径之间用逗号隔开,勿加空格,比如:E:\test\rootfile,E:\test\unZip;
4. 注意事项:
本解压合并程序处理文件的逻辑如下:
程序每次解压都去PROVINCE_DIR文件下去解压,将解压后的文件存放到UN_ZIP_PATH下,之后程序启动合并程序合并UN_ZIP_PATH下文件,将合并后的文件按照省份名称存放到OUT_PATH,一个省一个文件。当解压合并结束后,程序将PROVINCE_DIR路径下的文件移动到DONE_FILE_PATH下,并且删除PROVINCE_DIR跟UN_ZIP_PATH下文件,这样保证程序每次运行PROVINCE_DIR文件夹下的文件跟UN_ZIP_PATH下的文件都是最新未处理过的,避免了不断判断文件历史记录所带来的大量时间消耗。
所以为了保证文件解压跟合并的正确性,必须配置好DELETE_PATH路径下的文件,否则合并后的结果是不准确的。
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.zip.ZipEntry;...
最近遇到一个这样的需求:传一个压缩包给后台,后台保存后解压读取里面的文件,现学现做。在这里做个记录
文件上传
文件上传有很多方法,这里推荐一个自己感觉挺好用的一种,代码奉上:
@PostMapping(value = "/import", headers = "content-type=multipart/*")
public R importSqlLite(@RequestParam...
Java 导入zip,并进行解压
场景一:上传zip包,后台进行解压,提取里面的文件,进行上传。
场景二:上传zip,后台进行解压,提取zip包中“文件夹”以及该“文件夹”下的文件后,再进行压缩上传。
近日测试一个下载接口,该接口返回字节数组形式的zip文件,于是想利用该字节数组进行zip文件解压并对其中的文件做进一步解析操作
public void zipParse(byte[] content) throws IOException{
//将包含压缩包信息的字节数组转化为zipInputStream
ZipInputStream zipInputStream= new ZipInputStream(new ByteArrayInputStream(co
在我们日常使用中,zip压缩文件是非常常用的,市面上也有许多压缩软件,那么我们为什么要用java去操作zip,使用压缩软件不香吗?其实试想有这样一个需求,在这个项目中,有上传功能,如果要上传多个文件,这样频繁的操作远远不如上传一个压缩包方便,上传后如果要操作这些压缩包里的文件,用IO流不就方便多了。因此就得先了解ZipInputStream类和ZipOutputStream类。
ZipInputStream:继承自FilterInputStream类,采用了装饰器模式,可以直接读取zip包中的内容, 内部
这里写自定义目录标题前端上传一个文件,后端如何压缩?
前端上传一个文件,后端如何压缩?
在我们日常开发中,经常会遇到文件上传的功能,我们后端接收到文件后,会存入文件服务器或者数据库,那么如果文件过大的话,我们需要压缩处理,来节省空间。下面是具体如何压缩示例
//单个文件压缩
public static byte[] gZip(byte[] data) {
byte[] b;
try (ByteArrayOutputStream bos = new ByteArrayOutpu
@RestController
@RequestMapping("/upload")
public List<File> upload(MultipartFile file) throws IOException {
SimpleDateFormat sdf = new S