springboot大文件上传解决方案,springboot大文件上传
对于大文件的处理,无论是客户端还是服务器端,都不宜一次性读取、发送和接收,这样容易导致内存问题。所以对于大文件的上传,采用切片上传的方式。从上传效率来看,多线程并发上传可以实现效率最大化。
本文是基于springboot vue的文件上传。本文主要介绍服务器端文件上传的步骤和代码实现。关于vue的步骤和实现,请参阅我的另一篇文章。
Vue大文件片段上传-断点续传和并发上传
00-1010我的分析和上传分为:
检查文件是否已上传。如果已经上传,您可以创建一个临时文件(。_tmp)和上传的配置文件(。conf)以秒为单位。使用RandomAccessFile获取临时文件。调用RandomAccessFile的getChannel()方法,打开FileChannel FileChannel获取当前块号。计算文件的最后一个偏移量,获得当前文件块的字节数组,用于获得文件的字节长度。使用FileChannel FileChannel类的map()方法创建直接字节缓冲区MappedByteBuffer。将分段字节数组放入缓冲区的当前位置。mappedByteBuffer.put(byte[] b)释放缓冲区以检查是否所有文件都已上传。如果上传完成,临时文件名将是正式文件名
上传分步:
public class fliechunkutils {/* *并分块上传*第一步:获取RandomAccessFile,随机访问file类的对象*第二步:调用RandomAccessFile的getChannel()方法,打开FileChannel FileChannel *第三步:获取当前块并计算文件的最后一个偏移量*第四步:获取当前文件块的字节数组, 其中用于获取文件字节长度*第五步:使用file channel类的map()方法创建直接字节缓冲区MappedByteBuffer *第六步:将块字节数组放入当前位置的缓冲区MappedByteBuffer.put (byte *第七步:释放缓冲区*第八步:检查所有文件是否上传* * @ param param * @ return * @ throwsexception */public static API result uploadByMappedByteBuffer(multipart file param)抛出异常{ if(param . getidentity equals(param . getidentifier()){ param . set identifier(uuid . random uuid())。toString());}//确定是否上传if(object util . isempty(param . getfile()){ Return CheckUploadStatus(param);}//文件名字符串filename=get filename(param);//临时文件名string temp filename=param . getidentifier()filename . substring(filename . lastingdexof( . )) _ tmp ;//获取文件路径字符串file path=getuploadpath(param);//创建文件夹fileuploadutils . getabsolutefile(文件路径,文件名);//创建一个临时文件filetempfile=newfile (filepath,temp filename);//第一步获取RandomAccessFile,随机访问File类randomaccess file的对象RAF=random access file suites . getmodelrw(tempfile);//第二步调用RandomAccessFile的getChannel()方法,打开文件通道file channel file channel=RAF . get channel();//第三步,获取当前是哪个块,计算文件最后的偏移量long offset=(param . getchunknumber()-1)* param . getchunksize();//第四步:获取当前文件块的字节数组,用于获取文件字节长度byte [] FileData=param.getfile()。GetByt。
es(); //第五步 使用文件通道FileChannel类的 map()方法创建直接字节缓冲器 MappedByteBuffer MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length); //第六步 将分块的字节数组放入到当前位置的缓冲区内 mappedByteBuffer.put(byte[] b) mappedByteBuffer.put(fileData); //第七步 释放缓冲区 freeMappedByteBuffer(mappedByteBuffer); fileChannel.close(); raf.close(); //第八步 检查文件是否全部完成上传 ApiResult result = ApiResult.success(); boolean isComplete = checkUploadStatus(param, fileName, filePath); if (isComplete) { // 完成后,临时文件名为正式文件名 renameFile(tempFile, fileName); result.put("endUpload", true); } result.put("filePath", FileUploadUtils.getPathFileName(filePath, fileName)); result.put("fileName", param.getFile().getOriginalFilename()); return result; } /** * 检查文件是否上传 * * @param param * @return * @throws Exception */ public static ApiResult checkUploadStatus(MultipartFileParam param) throws Exception { String fileName = getFileName(param); // 校验conf文件 File confFile = checkConfFile(fileName, getUploadPath(param)); // 获取完成列表 byte[] completeStatusList = FileUtils.readFileToByteArray(confFile); List<String> uploadeds = new ArrayList<>(); for (int i = 0; i < completeStatusList.length; i++) { if (completeStatusList[i] == Byte.MAX_VALUE) { uploadeds.add(i + 1 + ""); } } ApiResult<Void> success = ApiResult.success(); success.put("uploaded", uploadeds); success.put("skipUpload", completeStatusList.length > 0 && completeStatusList.length == uploadeds.size()); // 新文件 if (ObjectUtil.isEmpty(completeStatusList)) { success.put("chunk", false); return success; } if (completeStatusList.length < param.getChunkNumber()) { success.put("chunk", false); return success; } byte b = completeStatusList[param.getChunkNumber() - 1]; if (b != Byte.MAX_VALUE) { success.put("chunk", false); return success; } success.put("filePath", FileUploadUtils.getPathFileName(getUploadPath(param), fileName)); success.put("chunk", true); return success; } /** * 文件下载 * * @param filePath 文件地址 * @param request * @param response * @throws IOException */ public static void download(String filePath, HttpServletRequest request, HttpServletResponse response) throws IOException { // 初始化 response response.reset(); // 获取文件 File file = new File(getDownloadPath(filePath)); long fileLength = file.length(); //获取从那个字节开始读取文件 String rangeString = request.getHeader("Range"); long range = 0; if (StrUtil.isNotBlank(rangeString)) { range = Long.valueOf(rangeString.substring(rangeString.indexOf("=") + 1, rangeString.indexOf("-"))); } if (range >= fileLength) { throw new CustomException("文件读取长度过长"); } long byteLength = 1024 * 1024; if (range + byteLength > fileLength) { byteLength = fileLength; } // 随机读文件RandomAccessFile RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r"); try { // 移动访问指针到指定位置 randomAccessFile.seek(range); // 每次请求只返回1MB的视频流 byte[] bytes = new byte[(int) byteLength]; int len = randomAccessFile.read(bytes); //获取响应的输出流 OutputStream outputStream = response.getOutputStream(); //返回码需要为206,代表只处理了部分请求,响应了部分数据 response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); //设置此次相应返回的数据长度 response.setContentLength(len); //设置此次相应返回的数据范围 response.setHeader("Content-Range", "bytes " + range + "-" + len + "/" + fileLength); // 将这1MB的视频流响应给客户端 outputStream.write(bytes, 0, len); outputStream.close(); //randomAccessFile.close(); System.out.println("返回数据区间:【" + range + "-" + (range + len) + "】"); } finally { randomAccessFile.close(); } } /** * 文件重命名 * * @param toBeRenamed 将要修改名字的文件 * @param toFileNewName 新的名字 * @return */ private static boolean renameFile(File toBeRenamed, String toFileNewName) { //检查要重命名的文件是否存在,是否是文件 if (!toBeRenamed.exists() toBeRenamed.isDirectory()) { return false; } String p = toBeRenamed.getParent(); File newFile = new File(p + File.separatorChar + toFileNewName); //修改文件名 return toBeRenamed.renameTo(newFile); } /** * 检查文件上传进度 * * @return */ private static boolean checkUploadStatus(MultipartFileParam param, String fileName, String filePath) throws Exception { // 校验conf文件 File confFile = checkConfFile(fileName, filePath); // 读取conf RandomAccessFile confAccessFile = new RandomAccessFile(confFile, "rw"); //设置文件长度 if (confAccessFile.length() != param.getTotalChunks()) { confAccessFile.setLength(param.getTotalChunks()); } //设置起始偏移量 confAccessFile.seek(param.getChunkNumber() - 1); //将指定的一个字节写入文件中 127, confAccessFile.write(Byte.MAX_VALUE); byte[] completeStatusList = FileUtils.readFileToByteArray(confFile); byte isComplete = Byte.MAX_VALUE; //这一段逻辑有点复杂,看的时候思考了好久,创建conf文件文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认的0,已上传的就是Byte.MAX_VALUE 127 for (int i = 0; i < completeStatusList.length && isComplete == Byte.MAX_VALUE; i++) { // 按位与运算,将&两边的数转为二进制进行比较,有一个为0结果为0,全为1结果为1 eg.3&5 即 0000 0011 & 0000 0101 = 0000 0001 因此,3&5的值得1。 isComplete = (byte) (isComplete & completeStatusList[i]); } if (isComplete == Byte.MAX_VALUE) { //如果全部文件上传完成,删除conf文件 // FileUtils.deleteFile(confFile.getPath()); return true; } return false; } /** * 在MappedByteBuffer释放后再对它进行读操作的话就会引发jvm crash,在并发情况下很容易发生 * 正在释放时另一个线程正开始读取,于是crash就发生了。所以为了系统稳定性释放前一般需要检 查是否还有线程在读或写 * * @param mappedByteBuffer */ private static void freeMappedByteBuffer(final MappedByteBuffer mappedByteBuffer) { try { if (mappedByteBuffer == null) { return; } mappedByteBuffer.force(); AccessController.doPrivileged(new PrivilegedAction<Object>() { @Override public Object run() { try { Method getCleanerMethod = mappedByteBuffer.getClass().getMethod("cleaner", new Class[0]); //可以访问private的权限 getCleanerMethod.setAccessible(true); //在具有指定参数的 方法对象上调用此 方法对象表示的底层方法 sun.misc.Cleaner cleaner = (sun.misc.Cleaner) getCleanerMethod.invoke(mappedByteBuffer, new Object[0]); cleaner.clean(); } catch (Exception e) { log.error("clean MappedByteBuffer error!!!", e); } return null; } }); } catch (Exception e) { e.printStackTrace(); } } private static String getFileName(MultipartFileParam param) { String extension; if (ObjectUtil.isNotEmpty(param.getFile())) { // return param.getFile().getOriginalFilename(); String filename = param.getFile().getOriginalFilename(); extension = filename.substring(filename.lastIndexOf(".")); //return FileUploadUtils.extractFilename(param.getFile()); } else { extension = param.getFilename().substring(param.getFilename().lastIndexOf(".")); //return DateUtils.datePath() + "/" + IdUtil.fastUUID() + extension; } return param.getIdentifier() + extension; } private static String getUploadPath(MultipartFileParam param) { return FileUploadUtils.getDefaultBaseDir() + "/" + param.getObjectType(); } private static String getDownloadPath(String filePath) { // 本地资源路径 String localPath = WhspConfig.getProfile(); // 数据库资源地址 String loadPath = localPath + StrUtil.subAfter(filePath, Constants.RESOURCE_PREFIX, false); return loadPath; } private static File checkConfFile(String fileName, String filePath) throws Exception { File confFile = FileUploadUtils.getAbsoluteFile(filePath, fileName + ".conf"); if (!confFile.exists()) { confFile.createNewFile(); } return confFile; }}到此这篇关于springboot大文件上传、分片上传、断点续传、秒传的实现的文章就介绍到这了,更多相关springboot大文件上传内容请搜索盛行IT以前的文章或继续浏览下面的相关文章希望大家以后多多支持盛行IT!
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。