springboot大文件上傳、分片上傳、斷點續(xù)傳、秒傳的實現(xiàn)
對于大文件的處理,無論是用戶端還是服務端,如果一次性進行讀取發(fā)送、接收都是不可取,很容易導致內(nèi)存問題。所以對于大文件上傳,采用切塊分段上傳,從上傳的效率來看,利用多線程并發(fā)上傳能夠達到最大效率。
本文是基于 springboot + vue 實現(xiàn)的文件上傳,本文主要介紹服務端實現(xiàn)文件上傳的步驟及代碼實現(xiàn),vue的實現(xiàn)步驟及實現(xiàn)請移步本人的另一篇文章
vue 大文件分片上傳 - 斷點續(xù)傳、并發(fā)上傳
上傳分步:
本人分析上傳總共分為:
- 檢查文件是否已上傳,如已上傳可實現(xiàn)秒傳
- 創(chuàng)建臨時文件(._tmp)和上傳的配置文件(.conf)
- 使用RandomAccessFile獲取臨時文件
- 調(diào)用RandomAccessFile的getChannel()方法,打開文件通道 FileChannel
- 獲取當前是第幾個分塊,計算文件的最后偏移量
- 獲取當前文件分塊的字節(jié)數(shù)組,用于獲取文件字節(jié)長度
- 使用文件通道FileChannel類的 map()方法創(chuàng)建直接字節(jié)緩沖器 MappedByteBuffer
- 將分塊的字節(jié)數(shù)組放入到當前位置的緩沖區(qū)內(nèi) mappedByteBuffer.put(byte[] b)
- 釋放緩沖區(qū)
- 檢查文件是否全部完成上傳,如上傳完成將臨時文件名為正式文件名
直接上代碼
public class FlieChunkUtils { ? ? ? /** ? ? ?* 分塊上傳 ? ? ?* 第一步:獲取RandomAccessFile,隨機訪問文件類的對象 ? ? ?* 第二步:調(diào)用RandomAccessFile的getChannel()方法,打開文件通道 FileChannel ? ? ?* 第三步:獲取當前是第幾個分塊,計算文件的最后偏移量 ? ? ?* 第四步:獲取當前文件分塊的字節(jié)數(shù)組,用于獲取文件字節(jié)長度 ? ? ?* 第五步:使用文件通道FileChannel類的 map()方法創(chuàng)建直接字節(jié)緩沖器 ?MappedByteBuffer ? ? ?* 第六步:將分塊的字節(jié)數(shù)組放入到當前位置的緩沖區(qū)內(nèi) ?mappedByteBuffer.put(byte[] b); ? ? ?* 第七步:釋放緩沖區(qū) ? ? ?* 第八步:檢查文件是否全部完成上傳 ? ? ?* ? ? ?* @param param ? ? ?* @return ? ? ?* @throws Exception ? ? ?*/ ? ? public static ApiResult uploadByMappedByteBuffer(MultipartFileParam param) throws Exception { ? ? ? ? if (param.getIdentifier() == null || "".equals(param.getIdentifier())) { ? ? ? ? ? ? param.setIdentifier(UUID.randomUUID().toString()); ? ? ? ? } ? ? ? ? // 判斷是否上傳 ? ? ? ? if (ObjectUtil.isEmpty(param.getFile())) { ? ? ? ? ? ? return checkUploadStatus(param); ? ? ? ? } ? ? ? ? // 文件名稱 ? ? ? ? String fileName = getFileName(param); ? ? ? ? // 臨時文件名稱 ? ? ? ? String tempFileName = param.getIdentifier() + fileName.substring(fileName.lastIndexOf(".")) + "_tmp"; ? ? ? ? // 獲取文件路徑 ? ? ? ? String filePath = getUploadPath(param); ? ? ? ? // 創(chuàng)建文件夾 ? ? ? ? FileUploadUtils.getAbsoluteFile(filePath, fileName); ? ? ? ? // 創(chuàng)建臨時文件 ? ? ? ? File tempFile = new File(filePath, tempFileName); ? ? ? ? //第一步 獲取RandomAccessFile,隨機訪問文件類的對象 ? ? ? ? RandomAccessFile raf = RandomAccessFileUitls.getModelRW(tempFile); ? ? ? ? //第二步 調(diào)用RandomAccessFile的getChannel()方法,打開文件通道 FileChannel ? ? ? ? FileChannel fileChannel = raf.getChannel(); ? ? ? ? //第三步 獲取當前是第幾個分塊,計算文件的最后偏移量 ? ? ? ? long offset = (param.getChunkNumber() - 1) * param.getChunkSize(); ? ? ? ? //第四步 獲取當前文件分塊的字節(jié)數(shù)組,用于獲取文件字節(jié)長度 ? ? ? ? byte[] fileData = param.getFile().getBytes(); ? ? ? ? //第五步 使用文件通道FileChannel類的 map()方法創(chuàng)建直接字節(jié)緩沖器 ?MappedByteBuffer ? ? ? ? MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length); ? ? ? ? //第六步 將分塊的字節(jié)數(shù)組放入到當前位置的緩沖區(qū)內(nèi) ?mappedByteBuffer.put(byte[] b) ? ? ? ? mappedByteBuffer.put(fileData); ? ? ? ? //第七步 釋放緩沖區(qū) ? ? ? ? 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(); ? ? ? ? //獲取從那個字節(jié)開始讀取文件 ? ? ? ? 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,代表只處理了部分請求,響應了部分數(shù)據(jù) ? ? ? ? ? ? response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); ? ? ? ? ? ? //設置此次相應返回的數(shù)據(jù)長度 ? ? ? ? ? ? response.setContentLength(len); ? ? ? ? ? ? //設置此次相應返回的數(shù)據(jù)范圍 ? ? ? ? ? ? response.setHeader("Content-Range", "bytes " + range + "-" + len + "/" + fileLength); ? ? ? ? ? ? // 將這1MB的視頻流響應給客戶端 ? ? ? ? ? ? outputStream.write(bytes, 0, len); ? ? ? ? ? ? outputStream.close(); ? ? ? ? ? ? //randomAccessFile.close(); ? ? ? ? ? ? System.out.println("返回數(shù)據(jù)區(qū)間:【" + 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); ? ? ? ? //將指定的一個字節(jié)寫入文件中 127, ? ? ? ? confAccessFile.write(Byte.MAX_VALUE); ? ? ? ? byte[] completeStatusList = FileUtils.readFileToByteArray(confFile); ? ? ? ? byte isComplete = Byte.MAX_VALUE; ? ? ? ? //這一段邏輯有點復雜,看的時候思考了好久,創(chuàng)建conf文件文件長度為總分片數(shù),每上傳一個分塊即向conf文件中寫入一個127,那么沒上傳的位置就是默認的0,已上傳的就是Byte.MAX_VALUE 127 ? ? ? ? for (int i = 0; i < completeStatusList.length && isComplete == Byte.MAX_VALUE; i++) { ? ? ? ? ? ? // 按位與運算,將&兩邊的數(shù)轉(zhuǎn)為二進制進行比較,有一個為0結(jié)果為0,全為1結(jié)果為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釋放后再對它進行讀操作的話就會引發(fā)jvm crash,在并發(fā)情況下很容易發(fā)生 ? ? ?* 正在釋放時另一個線程正開始讀取,于是crash就發(fā)生了。所以為了系統(tǒng)穩(wěn)定性釋放前一般需要檢 查是否還有線程在讀或?qū)? ? ? ?* ? ? ?* @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的權(quán)限 ? ? ? ? ? ? ? ? ? ? ? ? getCleanerMethod.setAccessible(true); ? ? ? ? ? ? ? ? ? ? ? ? //在具有指定參數(shù)的 方法對象上調(diào)用此 方法對象表示的底層方法 ? ? ? ? ? ? ? ? ? ? ? ? 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(); ? ? ? ? // 數(shù)據(jù)庫資源地址 ? ? ? ? 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大文件上傳、分片上傳、斷點續(xù)傳、秒傳的實現(xiàn)的文章就介紹到這了,更多相關springboot大文件上傳內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
BufferedInputStream(緩沖輸入流)詳解_動力節(jié)點Java學院整理
這篇文章主要為大家詳細介紹了BufferedInputStream緩沖輸入流的相關資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-05-05Java并發(fā)編程中的Callable、Future和FutureTask詳解
這篇文章主要介紹了Java并發(fā)編程中的Callable、Future和FutureTask詳解,創(chuàng)建線程的2種方式,一種是直接繼承Thread,另外一種就是實現(xiàn)Runnable接口,這2種方式都有一個缺陷就是:在執(zhí)行完任務之后無法獲取執(zhí)行結(jié)果,需要的朋友可以參考下2023-07-07Java Volatile關鍵字實現(xiàn)原理過程解析
這篇文章主要介紹了Java Volatile關鍵字實現(xiàn)原理過程解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-03-03java中toString()、String.valueOf()、(String)?強轉(zhuǎn)的區(qū)別
在實際開發(fā)中,少不了使用這三種方法對某一個類型的數(shù)據(jù)進行轉(zhuǎn)?String?的操作,本文就來介紹了java中toString()、String.valueOf()、(String)?強轉(zhuǎn)的區(qū)別,感興趣的可以了解一下2024-06-06Java開發(fā)環(huán)境配置及Vscode搭建過程
今天通過圖文并茂的形式給大家介紹Java開發(fā)環(huán)境配置及Vscode搭建過程,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧2021-07-07