一、引言
在日常 web 应用开发中,文件上传是一个极为常见的功能。无论是用户头像的上传、文档资料的提交,还是图片、视频等多媒体文件的传输,文件上传功能都扮演着关键角色。然而,当涉及到大文件上传时,传统的单次上传方式往往会暴露出诸多问题。
大文件上传时,若遭遇网络不稳定的情况,如网络突然中断或波动,整个文件就需要重新上传,这不仅浪费用户的时间和流量,还会导致用户体验急剧下降。服务器也会对上传文件的大小有所限制,一旦文件超出限制,上传便会失败。同时,一次性接收整个大文件可能会使服务端内存不足,出现内存溢出的情况。而且,在长时间的上传过程中,若无法展示上传进度条,用户只能在无尽的等待中猜测上传状态,这很容易让用户产生焦虑情绪,甚至可能导致用户误操作中断上传。
为了解决这些问题,分片上传、断点续传与进度条展示等技术应运而生。而 spring boot 作为当下流行的 java 开发框架,提供了丰富的功能和便捷的工具,能够帮助我们高效地实现这些功能。接下来,就让我们深入探讨如何使用 spring boot 实现分片上传、断点续传与进度条展示。
二、技术原理介绍
分片上传
分片上传,即将一个大文件按照固定的大小分割成多个较小的分片(也叫块),然后分别对这些分片进行上传 。每个分片都有一个唯一的标识,通常可以通过文件的唯一标识(如文件的 md5 值)与分片的序号来确定。在前端,通过 javascript 的 file api 可以方便地实现文件的分片操作。例如,使用file\.slice\(\)方法按照设定的分片大小(如 5mb)对文件进行切割。
在后端,spring boot 接收到每个分片后,会根据文件唯一标识与分片序号,将分片临时存储在服务器的指定目录中。当所有分片都上传完成后,后端会按照分片序号的顺序,将这些分片依次读取并合并成最终的完整文件。这样做的好处是,即使在上传过程中某个分片上传失败,也只需重新上传该分片,而无需重新上传整个文件,大大提高了上传的稳定性和效率。
断点续传
断点续传是建立在分片上传基础之上的一项技术。当文件上传过程中由于网络中断、服务器故障等原因导致上传中断时,客户端会记录下已经上传成功的分片信息,比如已经上传的分片序号。当用户再次尝试上传该文件时,客户端会根据之前记录的信息,从上次中断的位置开始,继续上传剩余的分片。
在服务端,需要保存已上传分片的状态信息。可以通过在数据库中记录每个文件的上传进度,或者使用缓存(如 redis)来存储已上传的分片列表等方式实现。当客户端发起续传请求时,服务端会根据这些记录,判断哪些分片已经上传成功,哪些还需要上传,从而实现断点续传的功能。这样就避免了用户因为上传中断而需要重新上传整个文件的烦恼,极大地提升了用户体验。
进度条
进度条的实现原理是通过监听文件上传过程中的数据传输量,实时计算并展示上传进度。在前端,当使用 xmlhttprequest 或 fetch api 进行文件上传时,可以监听其progress事件。这个事件会在上传过程中不断触发,通过事件对象可以获取到已经上传的字节数和文件的总字节数。根据这两个数据,就可以计算出上传的进度百分比,即已上传字节数 / 文件总字节数 \* 100%。
然后,将这个进度百分比更新到前端页面的进度条元素上,比如通过修改\<progress\>标签的value属性,或者使用 css 动画来模拟进度条的增长,从而让用户直观地了解文件上传的进度情况。在后端,spring boot 可以配合前端,通过一些机制(如 websocket、sse 等)将上传进度实时推送给前端,确保前端展示的进度与后端实际的上传进度保持一致 。
三、spring boot 实现分片上传
项目搭建
首先,我们使用 spring initializr(start.spring.io/ )来创建一个 spring boot 项目。在这个网页中,我们可以对项目进行多项配置,比如选择项目的构建工具(maven 或 gradle),指定 spring boot 的版本,以及添加所需的依赖。这里,我们主要添加spring\-boot\-starter\-web依赖,它提供了构建 web 应用所需的核心组件,让我们能够方便地创建 http 接口 。同时,为了处理文件上传,spring boot 默认已经集成了相关的依赖,无需额外引入。
如果使用 maven 构建项目,在pom\.xml文件中,添加如下依赖:
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-web</artifactid>
</dependency>如果是 gradle 项目,在build\.gradle文件中添加:
implementation 'org.springframework.boot:spring-boot-starter-web'
创建好项目后,将其导入到我们喜欢的集成开发环境(ide)中,比如 intellij idea 或 eclipse,这样就可以开始后续的开发工作了。
前端实现
前端我们可以使用原生 javascript 结合fetch api 来实现文件分片。下面是一个简单的示例代码:
<!doctype html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<title>文件分片上传</title>
</head>
<body>
<input type="file" id="largefile">
<button onclick="startupload()">开始上传</button>
<div id="progressbar"></div>
<script>
async function startupload() {
const file = document.getelementbyid('largefile').files[0];
if (!file) return;
// 配置参数
const chunk_size = 5 * 1024 * 1024; // 5mb分片
const total_chunks = math.ceil(file.size / chunk_size);
const file_id = `${file.name}-${file.size}-${date.now()}`;
// 创建进度跟踪器
const uploadedchunks = new set();
// 并行上传控制(最大5并发)
const parallellimit = 5;
let currentuploads = 0;
let activechunks = 0;
for (let chunkindex = 0; chunkindex < total_chunks; ) {
if (currentuploads >= parallellimit) {
await new promise(resolve => settimeout(resolve, 500));
continue;
}
if (uploadedchunks.has(chunkindex)) {
chunkindex++;
continue;
}
currentuploads++;
activechunks++;
const start = chunkindex * chunk_size;
const end = math.min(start + chunk_size, file.size);
const chunk = file.slice(start, end);
uploadchunk(chunk, chunkindex, file_id, total_chunks, file.name).then(() => {
uploadedchunks.add(chunkindex);
updateprogress(uploadedchunks.size, total_chunks);
}).catch(err => console.error(`分片${chunkindex}失败:`, err)).finally(() => {
currentuploads--;
activechunks--;
});
chunkindex++;
}
// 检查所有分片完成
const checkcompletion = setinterval(() => {
if (activechunks === 0 && uploadedchunks.size === total_chunks) {
clearinterval(checkcompletion);
completeupload(file_id, file.name);
}
}, 1000);
}
async function uploadchunk(chunk, index, fileid, total, filename) {
const formdata = new formdata();
formdata.append('file', chunk, filename);
formdata.append('chunkindex', index);
formdata.append('totalchunks', total);
formdata.append('fileid', fileid);
return fetch('/api/upload/chunk', {
method: 'post',
body: formdata
}).then(res => {
if (!res.ok) throw new error('上传失败');
});
}
function updateprogress(uploaded, total) {
const progressbar = document.getelementbyid('progressbar');
const progress = (uploaded / total) * 100;
progressbar.style.width = `${progress}%`;
progressbar.textcontent = `${progress.tofixed(2)}%`;
}
async function completeupload(fileid, filename) {
const formdata = new formdata();
formdata.append('fileid', fileid);
formdata.append('filename', filename);
return fetch('/api/upload/merge', {
method: 'post',
body: formdata
}).then(res => {
if (res.ok) {
alert('上传成功!');
} else {
throw new error('合并失败');
}
});
}
</script>
</body>
</html>在这段代码中,startupload函数负责处理文件分片和上传的逻辑。首先获取用户选择的文件,然后根据设定的chunk\_size(这里是 5mb)计算总分片数total\_chunks,并生成一个唯一的file\_id。通过一个for循环,对文件进行分片,每个分片通过uploadchunk函数上传到后端。uploadchunk函数将分片数据、分片序号、总分片数和文件唯一标识等信息通过formdata对象发送到/api/upload/chunk接口。updateprogress函数用于更新上传进度条,根据已上传的分片数和总分片数计算进度百分比,并更新页面上进度条的宽度和显示文本 。当所有分片上传完成后,通过completeupload函数向/api/upload/merge接口发送合并请求。
后端实现
后端在 spring boot 中创建接收分片的接口,并将分片保存到临时目录。首先创建一个控制器类fileuploadcontroller:
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.multipartfile;
import java.io.file;
import java.io.ioexception;
import java.nio.file.files;
import java.nio.file.path;
import java.nio.file.paths;
import java.util.uuid;
@restcontroller
@requestmapping("/api/upload")
public class fileuploadcontroller {
private static final string temp_dir = system.getproperty("java.io.tmpdir");
@postmapping("/chunk")
public string handlechunk(@requestparam("file") multipartfile file,
@requestparam("chunkindex") int chunkindex,
@requestparam("totalchunks") int totalchunks,
@requestparam("fileid") string fileid) {
try {
// 创建临时文件存储目录
path tempdirpath = paths.get(temp_dir, fileid);
files.createdirectories(tempdirpath);
// 构建分片文件路径
path chunkfilepath = tempdirpath.resolve(chunkindex + ".part");
// 保存分片文件
file.transferto(chunkfilepath.tofile());
return "分片上传成功";
} catch (ioexception e) {
e.printstacktrace();
return "分片上传失败";
}
}
@postmapping("/merge")
public string mergechunks(@requestparam("fileid") string fileid,
@requestparam("filename") string filename) {
try {
// 构建临时文件存储目录
path tempdirpath = paths.get(temp_dir, fileid);
// 构建最终合并文件路径
path mergedfilepath = paths.get(temp_dir, filename);
// 使用nio进行文件合并
files.walk(tempdirpath)
.filter(path ->!path.equals(tempdirpath))
.sorted((p1, p2) -> {
string name1 = p1.getfilename().tostring();
string name2 = p2.getfilename().tostring();
int index1 = integer.parseint(name1.substring(0, name1.lastindexof(".")));
int index2 = integer.parseint(name2.substring(0, name2.lastindexof(".")));
return integer.compare(index1, index2);
})
.foreach(chunkpath -> {
try {
files.copy(chunkpath, mergedfilepath, java.nio.file.standardcopyoption.append);
files.delete(chunkpath);
} catch (ioexception e) {
e.printstacktrace();
}
});
// 删除临时目录
files.delete(tempdirpath);
return "文件合并成功";
} catch (ioexception e) {
e.printstacktrace();
return "文件合并失败";
}
}
}
在fileuploadcontroller中,handlechunk方法用于处理前端上传的分片。它接收前端传来的分片文件、分片序号、总分片数和文件唯一标识。首先创建以fileid命名的临时目录,然后在该目录下根据分片序号构建分片文件路径,最后将接收到的分片文件保存到该路径。mergechunks方法用于处理文件合并。它接收文件唯一标识和文件名,先构建临时目录和最终合并文件的路径。通过files\.walk方法遍历临时目录下的所有分片文件,按照分片序号进行排序,然后依次将分片文件内容追加到最终合并文件中,并在合并完成后删除临时分片文件和临时目录 。如果在保存分片或合并文件过程中出现ioexception异常,则返回相应的失败信息。
四、spring boot 实现断点续传
前端实现
在前端,我们需要记录上传进度,以便在上传中断后能够准确地知道已经上传了哪些分片 。可以通过在本地存储(如localstorage)中记录已上传的分片序号来实现这一功能。当上传中断后,再次点击上传按钮时,前端代码会读取本地存储中的已上传分片信息。
async function startupload() {
const file = document.getelementbyid('largefile').files[0];
if (!file) return;
const chunk_size = 5 * 1024 * 1024; // 5mb分片
const total_chunks = math.ceil(file.size / chunk_size);
const file_id = `${file.name}-${file.size}-${date.now()}`;
// 从本地存储获取已上传分片
const uploadedchunks = json.parse(localstorage.getitem(file_id)) || new set();
for (let chunkindex = 0; chunkindex < total_chunks; chunkindex++) {
if (uploadedchunks.has(chunkindex)) {
continue;
}
const start = chunkindex * chunk_size;
const end = math.min(start + chunk_size, file.size);
const chunk = file.slice(start, end);
try {
await uploadchunk(chunk, chunkindex, file_id, total_chunks, file.name);
uploadedchunks.add(chunkindex);
localstorage.setitem(file_id, json.stringify(array.from(uploadedchunks)));
updateprogress(uploadedchunks.size, total_chunks);
} catch (err) {
console.error(`分片${chunkindex}上传失败:`, err);
// 上传失败时可以在这里添加重试逻辑
}
}
if (uploadedchunks.size === total_chunks) {
completeupload(file_id, file.name);
}
}
在上述代码中,startupload函数在开始上传前,先从localstorage中读取已上传的分片信息。如果某个分片已经上传过(通过uploadedchunks\.has\(chunkindex\)判断),则跳过该分片的上传。当一个分片上传成功后,将其序号添加到uploadedchunks集合中,并更新localstorage中的记录 。这样,即使上传过程中出现中断,再次上传时也能从上次中断的位置继续。
后端实现
后端需要提供一个接口,用于查询某个文件已经上传的分片。在fileuploadcontroller中添加如下方法:
import org.springframework.web.bind.annotation.*;
import java.io.file;
import java.nio.file.files;
import java.nio.file.path;
import java.nio.file.paths;
import java.util.arraylist;
import java.util.list;
@restcontroller
@requestmapping("/api/upload")
public class fileuploadcontroller {
private static final string temp_dir = system.getproperty("java.io.tmpdir");
@getmapping("/uploadedchunks")
public list<integer> getuploadedchunks(@requestparam("fileid") string fileid) {
path tempdirpath = paths.get(temp_dir, fileid);
if (!files.exists(tempdirpath)) {
return new arraylist<>();
}
list<integer> uploadedchunks = new arraylist<>();
try {
files.list(tempdirpath).foreach(path -> {
string filename = path.getfilename().tostring();
int index = integer.parseint(filename.substring(0, filename.lastindexof(".")));
uploadedchunks.add(index);
});
} catch (exception e) {
e.printstacktrace();
}
return uploadedchunks;
}
// 其他方法...
}
getuploadedchunks方法接收fileid作为参数,根据fileid构建临时目录路径。如果该目录不存在,则返回一个空列表,表示没有上传任何分片。如果目录存在,则遍历目录下的所有文件,通过文件名解析出分片序号,并将其添加到uploadedchunks列表中,最后返回该列表 。前端在上传前,可以先调用这个接口,获取已上传的分片信息,从而确定需要上传哪些分片,实现断点续传的功能。
五、spring boot 实现进度条
前端实现
在前端展示进度条,我们可以使用 html5 提供的\<progress\>标签。这个标签提供了一种简单且语义化的方式来显示任务的完成进度。它有两个主要属性:value表示当前已完成的值,max表示任务总值 。浏览器会根据这两个值自动计算并显示进度百分比。
<progress id="uploadprogress" value="0" max="100"></progress>
然后,通过 javascript 监听文件上传的progress事件来动态更新value属性,从而实现进度条的实时更新。结合前面分片上传的前端代码,在uploadchunk函数中,我们可以进一步完善对进度条的更新逻辑:
async function uploadchunk(chunk, index, fileid, total, filename) {
const formdata = new formdata();
formdata.append('file', chunk, filename);
formdata.append('chunkindex', index);
formdata.append('totalchunks', total);
formdata.append('fileid', fileid);
const controller = new abortcontroller();
const { signal } = controller;
const xhr = new xmlhttprequest();
xhr.open('post', '/api/upload/chunk', true);
xhr.upload.onprogress = function (event) {
if (event.lengthcomputable) {
const percentcomplete = (event.loaded / event.total) * 100;
document.getelementbyid('uploadprogress').value = percentcomplete;
}
};
xhr.onload = function () {
if (xhr.status === 200) {
// 上传成功后的处理
} else {
// 上传失败后的处理
}
};
xhr.send(formdata, signal);
// 这里可以添加取消上传的逻辑,比如在某个条件下调用 controller.abort()
return new promise((resolve, reject) => {
xhr.onreadystatechange = function () {
if (xhr.readystate === xmlhttprequest.done) {
if (xhr.status === 200) {
resolve();
} else {
reject(new error('上传失败'));
}
}
};
});
}
在上述代码中,xhr\.upload\.onprogress事件处理函数会在上传过程中不断被触发。event\.lengthcomputable用于判断是否可以计算上传进度,如果可以,则通过event\.loaded(已上传的字节数)和event\.total(文件总字节数)计算出上传的百分比,并更新uploadprogress进度条的value属性 。这样,用户就能在前端直观地看到每个分片的上传进度。
除了使用原生的\<progress\>标签,我们也可以使用一些第三方 ui 组件库来实现更美观、功能更丰富的进度条效果,比如 element - ui(适用于 vue 项目)、ant design(适用于 react 项目)等 。以 element - ui 为例,在 vue 项目中使用其进度条组件的代码如下:
<template>
<el-progress :percentage="progress" status="active"></el-progress>
</template>
<script>
export default {
data() {
return {
progress: 0
};
},
methods: {
async uploadchunk(chunk, index, fileid, total, filename) {
// 上传逻辑...
const xhr = new xmlhttprequest();
xhr.open('post', '/api/upload/chunk', true);
xhr.upload.onprogress = function (event) {
if (event.lengthcomputable) {
const percentcomplete = (event.loaded / event.total) * 100;
this.progress = percentcomplete;
}
}.bind(this);
// 其他代码...
}
}
};
</script>在这段 vue 代码中,el \- progress是 element - ui 提供的进度条组件,percentage属性绑定了progress数据,通过在上传过程中更新progress的值,实现进度条的动态展示 。status=\&\#34;active\&\#34;可以让进度条显示为动态加载的效果,增强用户体验。
后端实现
后端将上传进度数据传递给前端,有两种常见的方式:websocket 实时推送和接口轮询获取。
websocket 实时推送:使用 websocket 可以实现服务器与客户端之间的全双工通信,服务器能够主动将上传进度推送给客户端。首先,在 spring boot 项目中添加 websocket 依赖,如果使用 maven,在pom\.xml中添加:
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-websocket</artifactid>
</dependency>然后创建一个 websocket 配置类websocketconfig:
import org.springframework.context.annotation.bean;
import org.springframework.context.annotation.configuration;
import org.springframework.web.socket.server.standard.serverendpointexporter;
@configuration
public class websocketconfig {
@bean
public serverendpointexporter serverendpointexporter() {
return new serverendpointexporter();
}
}
接下来创建一个 websocket 处理器类fileuploadwebsockethandler:
import org.springframework.stereotype.component;
import org.springframework.web.socket.closestatus;
import org.springframework.web.socket.textmessage;
import org.springframework.web.socket.websockethandler;
import org.springframework.web.socket.websocketmessage;
import org.springframework.web.socket.websocketsession;
import java.util.concurrent.concurrenthashmap;
import java.util.concurrent.concurrentmap;
@component
public class fileuploadwebsockethandler implements websockethandler {
private static final concurrentmap<string, websocketsession> sessionmap = new concurrenthashmap<>();
@override
public void afterconnectionestablished(websocketsession session) throws exception {
sessionmap.put(session.getid(), session);
}
@override
public void handlemessage(websocketsession session, websocketmessage<?> message) throws exception {
// 处理接收到的消息,这里暂不处理前端发来的消息
}
@override
public void handletransporterror(websocketsession session, throwable exception) throws exception {
session.close(closestatus.server_error);
sessionmap.remove(session.getid());
}
@override
public void afterconnectionclosed(websocketsession session, closestatus status) throws exception {
sessionmap.remove(session.getid());
}
@override
public boolean supportspartialmessages() {
return false;
}
public void sendprogress(string sessionid, int progress) {
websocketsession session = sessionmap.get(sessionid);
if (session != null && session.isopen()) {
try {
session.sendmessage(new textmessage(string.valueof(progress)));
} catch (exception e) {
e.printstacktrace();
sessionmap.remove(sessionid);
}
}
}
}
在fileuploadcontroller中,当处理分片上传时,计算上传进度并通过 websocket 推送:
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.multipartfile;
import java.io.file;
import java.io.ioexception;
import java.nio.file.files;
import java.nio.file.path;
import java.nio.file.paths;
import java.util.uuid;
@restcontroller
@requestmapping("/api/upload")
public class fileuploadcontroller {
private static final string temp_dir = system.getproperty("java.io.tmpdir");
@autowired
private fileuploadwebsockethandler fileuploadwebsockethandler;
@postmapping("/chunk")
public string handlechunk(@requestparam("file") multipartfile file,
@requestparam("chunkindex") int chunkindex,
@requestparam("totalchunks") int totalchunks,
@requestparam("fileid") string fileid,
@requestparam("sessionid") string sessionid) {
try {
// 创建临时文件存储目录
path tempdirpath = paths.get(temp_dir, fileid);
files.createdirectories(tempdirpath);
// 构建分片文件路径
path chunkfilepath = tempdirpath.resolve(chunkindex + ".part");
// 保存分片文件
file.transferto(chunkfilepath.tofile());
// 计算上传进度
int progress = (int) ((chunkindex + 1) * 100 / totalchunks);
fileuploadwebsockethandler.sendprogress(sessionid, progress);
return "分片上传成功";
} catch (ioexception e) {
e.printstacktrace();
return "分片上传失败";
}
}
// 其他方法...
}
在前端,建立 websocket 连接并监听进度消息:
const socket = new websocket('ws://localhost:8080/websocket');
socket.onmessage = function (event) {
const progress = parseint(event.data);
document.getelementbyid('uploadprogress').value = progress;
};
在上述代码中,后端通过fileuploadwebsockethandler的sendprogress方法将上传进度发送给对应的 websocket 会话。前端建立 websocket 连接后,通过onmessage事件接收并更新进度条 。
接口轮询获取:接口轮询的方式是前端定时向后端发送请求,获取最新的上传进度。在后端fileuploadcontroller中添加一个获取进度的接口:
import org.springframework.web.bind.annotation.*;
import java.util.hashmap;
import java.util.map;
@restcontroller
@requestmapping("/api/upload")
public class fileuploadcontroller {
private static final string temp_dir = system.getproperty("java.io.tmpdir");
private final map<string, integer> progressmap = new hashmap<>();
@postmapping("/chunk")
public string handlechunk(@requestparam("file") multipartfile file,
@requestparam("chunkindex") int chunkindex,
@requestparam("totalchunks") int totalchunks,
@requestparam("fileid") string fileid) {
try {
// 保存分片逻辑...
// 计算上传进度
int progress = (int) ((chunkindex + 1) * 100 / totalchunks);
progressmap.put(fileid, progress);
return "分片上传成功";
} catch (ioexception e) {
e.printstacktrace();
return "分片上传失败";
}
}
@getmapping("/progress")
public int getprogress(@requestparam("fileid") string fileid) {
return progressmap.getordefault(fileid, 0);
}
// 其他方法...
}
在前端,使用setinterval定时调用接口获取进度并更新进度条:
const fileid = "your - unique - file - id";
const progressinterval = setinterval(() => {
fetch(`/api/upload/progress?fileid=${fileid}`)
.then(response => response.json())
.then(data => {
const progress = data;
document.getelementbyid('uploadprogress').value = progress;
});
}, 1000);
在上述代码中,前端每隔 1 秒(1000 毫秒)向后端的/api/upload/progress接口发送请求,获取文件的上传进度,并更新进度条。这种方式实现相对简单,但相比 websocket 实时推送,会增加一定的网络开销,并且进度更新可能不够及时 。
六、整合与优化
整合步骤
将分片上传、断点续传和进度条功能整合在一起时,首先要确保前端和后端的交互逻辑清晰且一致 。在前端,需要统一文件选择、上传操作的触发逻辑。例如,在一个文件上传组件中,通过一个按钮来触发文件选择对话框,当用户选择文件后,自动触发分片上传流程。在这个流程中,既要包含分片上传的逻辑,如按固定大小对文件进行分片、生成唯一的文件标识等,也要融入断点续传的逻辑,即从本地存储或后端获取已上传的分片信息,跳过已上传的分片 。同时,实时更新进度条的显示,将每个分片的上传进度及时反馈给用户。
在后端,各个功能对应的接口要协同工作。接收分片上传的接口不仅要保存分片文件,还要记录上传进度,以便为进度条功能提供数据支持 。查询已上传分片的接口要与断点续传功能紧密配合,准确地返回已上传的分片列表,让前端能够根据这个列表确定需要上传的剩余分片。文件合并的接口要在所有分片上传完成后,正确地将分片合并成完整文件,并处理好合并过程中的异常情况 。在整合过程中,要注意数据的一致性和准确性,比如文件唯一标识在前端和后端的传递和使用要保持一致,避免出现因标识不一致而导致的上传错误。
性能优化
从网络优化方面来看,可以采用 cdn(内容分发网络)加速技术 。cdn 通过在全球各地部署节点服务器,将文件缓存到离用户更近的节点。当用户上传文件时,请求会被路由到距离最近的 cdn 节点,这样可以大大减少网络传输的延迟,提高上传速度。例如,对于跨国用户上传文件的场景,cdn 可以有效地解决因网络距离远而导致的上传缓慢问题 。同时,合理调整上传的并发数也能提升性能。在前端,可以根据用户设备的性能和网络状况,动态调整并发上传的分片数量。如果用户设备性能较强且网络稳定,可以适当增加并发数,如将并发上传的分片数从默认的 5 个增加到 10 个,从而加快上传速度;反之,则减少并发数,避免因过多的并发请求导致网络拥塞。
在服务器资源利用方面,合理配置服务器参数至关重要。对于 java 应用服务器(如 tomcat),可以调整 jvm 堆大小。如果上传的文件较大且并发上传请求较多,适当增加 jvm 堆大小,比如将堆大小从默认的 512mb 增加到 1gb,能够避免因内存不足而导致的上传失败 。同时,优化线程池配置也能提高服务器的并发处理能力。根据服务器的硬件配置和预计的上传并发量,调整线程池的核心线程数、最大线程数和队列容量。例如,将核心线程数设置为 10,最大线程数设置为 50,队列容量设置为 100,这样可以使服务器在高并发上传场景下,更有效地处理上传请求,避免线程资源的浪费和请求的积压。此外,采用异步处理机制,将文件上传任务放入任务队列中,由后台线程异步处理,也能提高服务器的响应速度,避免因同步处理上传任务而导致服务器长时间阻塞,影响其他请求的处理。
异常处理
上传过程中可能出现多种异常情况。网络超时是比较常见的异常,当网络不稳定或上传时间过长时,就可能发生网络超时。为了处理这种异常,在前端可以设置上传请求的超时时间,比如设置为 30 秒。当请求超时后,自动触发重试机制,重新上传该分片。在后端,也可以配置网络连接的超时时间,并且当接收到超时的上传请求时,记录相关日志,以便后续分析问题 。如果多次重试后仍然失败,可以提示用户检查网络连接,并提供一些常见的网络问题排查建议。
文件损坏也是可能出现的异常情况。在前端,可以在上传前对文件进行完整性校验,比如计算文件的 md5 值或 sha - 1 值。在后端,接收到分片文件后,再次计算分片的校验值,并与前端传来的校验值进行比对。如果校验值不一致,说明文件在传输过程中可能损坏,此时可以返回错误信息给前端,告知用户文件损坏,需要重新上传 。同时,后端可以记录损坏文件的相关信息,如文件名、文件大小、上传时间等,以便进一步分析文件损坏的原因,是网络传输问题还是其他因素导致的。此外,对于服务器内部错误,如磁盘空间不足、文件系统故障等,后端要捕获相应的异常,返回友好的错误提示给前端,并且在服务器端详细记录异常日志,包括异常类型、异常堆栈信息等,方便运维人员快速定位和解决问题 。
以上就是springboot中大文件分片上传、断点续传与进度条的实现全解析的详细内容,更多关于springboot大文件上传的资料请关注代码网其它相关文章!
发表评论