SpringBoot整合OnlyOffice
- SpringBoot整合OnlyOffice實現在線編輯
- 1. 搭建私有的OnlyOffice的服務
- 2. SpringBoot進行交互
- 2.1 環境
- 2.2 我們的流程
- 2.3 接口規劃
- 2.3.1 獲取編輯器配置的接口
- 2.3.2 文件下載地址
- 2.3.3 文件下載地址
- 3. 總結
- 4. 注意
- 4.1 你的項目的地址一定一定要和onlyoffice可以正常通訊,如果不行則一直不可能成功。
- 4.2 TOKEN是可以可選項,建議一開始不要使用,后面有需要的時候再去添加。
- 4.3 一定要看一下官網文檔,文檔真的很全很重要
- 4.4 協同的話只要參數就是一個KEY,如果需要超過20個的限制直接重新編譯即可,大神一大堆,很容易就可以找到。
SpringBoot整合OnlyOffice實現在線編輯
公司有一個需求,就是實現 *Word* , *Excel* ,等文件的在線編輯,市場上面進行了多方面的選型,考慮了 *[OpenOffice](https://openoffice.apache.org/)* , *[Office Online](https://www.microsoft.com/zh-cn/microsoft-365/free-office-online-for-the-web?legRedir=true&CorrelationId=13c8a865-b9b0-48ff-b3ed-3ea9ec31cd55)*, 但是最終還是選擇了 *[OnlyOffice](https://www.onlyoffice.com/zh/)* 這個產品。
他的一個很大的優勢在于開源,支持協同,社區比較活躍。api比較全面,還有中文的文檔。還有一點比較好的就是支持協同,并且支持協同,雖然協同在社區版中存在限制,但是支持代碼修改,可以重新編譯。社區的大佬很多,很贊。唯一遺憾的就是效率比較低,在使用私有對象存儲的時候存在延遲。其他的沒有使用到,所以不進行評論。中文文檔:[https://api.onlyoffice.com/zh/editors/basic](https://api.onlyoffice.com/zh/editors/basic)
1. 搭建私有的OnlyOffice的服務
搭建過程這里就不進行涉獵了,建議使用docker進行搭建,下載官方鏡像包即可,(現在dockerhub被墻,自行解決,不建議自己再次打包,因為我在嘗試的時候總是出現莫名奇妙的問題可能是我的問題。推薦使用官網原版鏡像)。根據官方文檔一步步操作即可。搭建過程中,如果是自己玩建議不要開啟 **JWT** ,生產環境建議開一下。但是開的成本就是你對接的時候需要獲取token然后在進行交互。
2. SpringBoot進行交互
2.1 環境
java: 17
boot: 3.0.5
頁面:一個h5頁面即可
需要的其他依賴
<!-- ... 其他的依賴自行添加即可,不重要,比如 fastjson2,jackson 等 --><!-- 這個JAR 主要的作用是與OnlyOffice交互的時候生成token使用的 -->
<dependency><groupId>com.inversoft</groupId><artifactId>prime-jwt</artifactId><version>1.3.1</version></dependency>
2.2 我們的流程
我們使用一個 H5 頁面即可,頁面通過加載一個 app.js 。然后通過一個 config 進行渲染,就可以實現一個編輯。app.js 是核心js文件
- only office 我只使用他的一個編輯的功能(這是一個核心,就是編輯文件,文件的來源和存儲與它無關)
- 被編輯的文件從哪里獲取?從 config 對象中的配置獲取,這里就需要自行實現。
- 編輯后的文件如何獲取?config對象中有一個回調地址,這個地址會給到服務器一個編輯的狀態,并且攜帶一個獲取編輯后文件的url(這個url就是only office 服務中的一個文件下載地址),根據這個url來獲取編輯后的文件。然后在對這個文件進行存儲。
回調的實現參考:https://api.onlyoffice.com/zh/editors/callback#status
2.3 接口規劃
一共設計三個接口,
- 獲取編輯器的配置
- 獲取需要編輯的文件流
- 編輯后保存文件的回調
保存后的文件:注意,這里編輯后的文件并不是在回調里面以流的形式給,而是在回調接口里面給服務器一個狀態,根據狀態去獲取一個下載編輯后文件的一個地址,然后根據地址去主動的獲取文件。
2.3.1 獲取編輯器配置的接口
/*** 被編輯文件的下載連接* 這里就是自己服務的配置地址* only office 調用你的服務的地址,一定是 onlyoffice服務可以ping通的你的項目地址。ping不通=白搭*/
@Value("${only.office.downUrl}")
private String downFileUrl = "";
/*** 這里是回調地址:例如 http://192.168.0.10:8080/office/edit/callback/{fileId}* 自行定義即可(就是后面自己編寫的接口,但是一定要通可onlyoffice服務互通)* only office 調用你的服務的地址,一定是 onlyoffice服務可以ping通的你的項目地址。ping不通=白搭*/
@Value("${only.office.callBackUrl}")
private String editCallBackUrl = "";@Operation(summary = "根據文件的ID來獲取在線編輯的配置和token")
@PostMapping("/token/{fileId}")
@Parameters({@Parameter(name = "fileId", description = "不是對象ID是文件的ID", in = ParameterIn.PATH)
})
public ResultVo<?> getToken(@PathVariable String fileId) {String fileKey ;if (redisUtil.hHasKey(RedisName.ONLY_OFFICE_FILE_KYE,fileId)) {fileKey = redisUtil.hget(RedisName.ONLY_OFFICE_FILE_KYE,fileId).toString();//return ResultVo.error(CustomExceptionType.ONLY_OFFICE_COORDINATION_ERROR);}else{fileKey = fileId + RandomUtil.randomNumbers(10);}String json = """{"document": {"title": "%s","key": "%s","fileType":"%s","lang": "zh-CN","permissions": {"comment": true,"commentGroups": {"edit": ["Group2", "Group1"],"remove": [""],"view": ""},"copy": true,"deleteCommentAuthorOnly": false,"download": true,"edit": true,"editCommentAuthorOnly": false,"fillForms": true,"modifyContentControl": true,"modifyFilter": true,"print": true,"review": true,"reviewGroups": ["Group1", "Group2", ""]},"url": "%s"},"editorConfig": {"customization":{"autosave": true,"forcesave": true}"lang": "zh-CN","callbackUrl": "%s","onEditing": {"mode": "fast","change": true},"mode": "edit","user": {"group": "Group1","id": "%s","name": "%s"}}}""";// TODO 這里文件的key可以通過redis進行保存,這樣可以支持多人在線協同,現在不做處理json = String.format(json, fileInfo.getFileName(),fileKey,// TODO 這里是文件類型,自行定義'xlsx',// TODO 這里是文件下載地址,fileId 為文件的唯一標識,自行定義downFileUrl + fileId, // TODO 這里是定義回調地址,fileId 為文件的唯一標識用來區分是那個文件編輯的回調。editCallBackUrl + fileId,"userid","username");Map<String, Object> map = JSONObject.parseObject(json, new TypeToken<Map<String, Object>>() {}.getType());// TODO 這里是獲取onlyoffice 交互的token,自己寫的建議直接注釋// String token = jwtManager.createToken(map);// map.put("token", token);// TODO 這個key可以直接注釋,這里主要作用是協同redisUtil.hset(RedisName.ONLY_OFFICE_FILE_KYE,fileId,fileKey,60*60*24);return ResultVo.success(map);
}
2.3.2 文件下載地址
這個接口的作用就是獲取一個文件流,根據ID來獲取一個文件流
這里的地址就是上一個接口中下載文件的地址。
@GetMapping("down/file/{fileId}")
@Operation(summary = "根據參數下載一個文件")
public void downFolderById(@PathVariable String fileId, HttpServletResponse response){// TODO 1. 根據文件的唯一ID來獲取數據庫中的記錄EtmfFileInfo fileInfo = fileInfoOpt.getById(fileId);// TODO 2. 根據下載路徑從 minio 中獲取文件流 (因為我們使用的是minio,其他的自行切換即可)try (InputStream inputStream = smoMinIoUtils.downloadFile(fileInfo.getFileUrl())) {downFileInfo(response, fileInfo, inputStream);} catch (ServerException | ErrorResponseException | InsufficientDataException | IOException |NoSuchAlgorithmException | InvalidKeyException | InvalidResponseException | XmlParserException |InternalException e) {JwtUtil.responseError(response, 500L, "文件下載失敗:" + e.getMessage());}
}public static void downFileInfo(HttpServletResponse response, EtmfFileInfo fileInfo, InputStream inputStream) throws IOException {response.setCharacterEncoding("UTF-8");response.setContentType("application/octet-stream; charset=UTF-8");response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileInfo.getFileName(), StandardCharsets.UTF_8));ServletOutputStream stream = response.getOutputStream();IOUtils.copy(inputStream,stream);stream.flush();stream.close();
}
2.3.3 文件下載地址
這里是文件的回調地址,主要就是獲取一個狀態碼,然后根據狀態碼判定是否保存文件。
@Operation(summary = "文件編輯之后的回調")
@Parameters({@Parameter(name = "fileId", description = "文件的ID", in = ParameterIn.PATH)
})
@PostMapping("/edit/callback/{fileId}")
public void editCallBack(@PathVariable String fileId, HttpServletRequest request, HttpServletResponse response) {try {PrintWriter writer = response.getWriter();String body;try {Scanner scanner = new Scanner(request.getInputStream());scanner.useDelimiter("\\A");body = scanner.hasNext() ? scanner.next() : "";scanner.close();} catch (Exception ex) {writer.write("get request.getInputStream error:" + ex.getMessage());return;}if (body.isEmpty()) {writer.write("empty request.getInputStream");return;}JSONObject jsonObj = JSON.parseObject(body);int status = (Integer) jsonObj.get("status");log.debug("================文件編輯獲取到的參數是:{}", JSON.toJSONString(jsonObj));int saved = 0;if (List.of(2,3,6).contains(status)) {String downloadUri = (String) jsonObj.get("url");log.debug("================文件進行保存處理,需要保存的狀態值是:{},可以獲取到文件的路徑是:{}", status,downloadUri);try {URL url = new URL(downloadUri);// 根據文件下載地址來獲取編輯后的文件流HttpURLConnection connection = (HttpURLConnection) url.openConnection();InputStream stream = connection.getInputStream();if (stream == null) {throw new Exception("Stream is null");}// TODO 根據文件的唯一標識獲取數據庫中文件記錄EtmfFileInfo fileInfo = fileInfoOpt.getById(fileId);// TODO 根據文件流創建一個文件File savedFile = new File(fileInfo.getFileName());try (FileOutputStream out = new FileOutputStream(savedFile)) {int read;final byte[] bytes = new byte[1024];while ((read = stream.read(bytes)) != -1) {out.write(bytes, 0, read);}out.flush();}// TODO 根據文件上傳到 MINIO中boolean b = smoMinIoUtils.uploadFile(fileInfo.getFileUrl(), savedFile);log.info("編輯文件后,文件上傳狀態:{},上傳的文件是:{},Id是:{}",b,fileInfo.getFileName(),fileId);savedFile.delete();connection.disconnect();} catch (Exception ex) {saved = 1;ex.printStackTrace();}finally {// 正常保存的時候剔除掉redis緩存if (status == TWO) {redisUtil.hdel(RedisName.ONLY_OFFICE_FILE_KYE,fileId);}}}writer.write("{\"error\":" + saved + "}");writer.flush();writer.close();log.debug("======================編輯完成--------------返回值是:{}","{\"error\":" + saved + "}");} catch (IOException e) {e.printStackTrace();throw new SmoGlobalException(CustomExceptionType.OTHER_ERROR);}
}
3. 總結
文件的在線編輯主要就是依托與onlyoffice實現的,而編輯器的配置是通過我們的接口來定義的,接口中的配置可以自由的定義編輯器的文件類型,窗口大小,文件來源,回調地址,保存類型等等。
你需要編輯的文件可以放在任意的位置,只要你的接口可以通過流的方式給到onlyofiice編輯器即可。
文件編輯后的處理都是在回調中處理的,最好先看一下文檔的回調寫法。回調的時候記得打印日志,觀察一下接口的內容,一定要記得是通過回調中的url參數來獲取編輯后的文件流的,并不是通過回調接口直接把文件流給到你。我在這里沒有注意看饒了彎路。所以提醒一下。