目的:將文件交給阿里雲進行管理,可避免文件對本地服務器資源的佔用,阿里雲OSS還可根據讀寫偏好選擇合適的文件存儲類型服務器,文件異地備份等
一、阿里雲OSS基礎瞭解(前提)
1、存儲空間(Bucket)
用於存儲對象(Object)的容器,同一個存儲空間的內部是扁平的,沒有文件系統的目錄等概念,所有的對象都必須隸屬於某個存儲空間。存儲空間具有各種配置屬性,包括地域、訪問權限、存儲類型等。可根據實際需求,創建不同存儲空間存儲不同數據。(百度的官話)
簡而言之:Bucket就簡單理解爲C盤,D盤就可以了,可以在不同的Bucket下創建文件夾存儲文件。
2、存儲對象(Object)
是 OSS 存儲數據的基本單元,也被稱爲 OSS 的文件。對象由元信息(Object Meta)、用戶數據(Data)和文件名(Key)組成。對象由存儲空間內部唯一的 Key 來標識。對象元信息是一組鍵值對,表示了對象的一些屬性,比如最後修改時間、大小等信息,支持在元信息中存儲一些自定義的信息。對象的生命週期是從上傳成功到被刪除爲止。(還是官話)
簡而言之:就是要存儲的文件。
二、環境準備及測試
1、pom座標導入
<!-- 阿里雲OSS --> <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.10.2</version> </dependency>
2、阿里雲OSS API操作返回值類
package cc.mrbird.febs.finance.domain.dto; import lombok.Data; /** * @Author: sunguoqiang * @Description: 阿里雲上傳結果集 * @DateTime: 2022/8/3 16:27 **/ @Data public class AliyunOssResult { /** * code:200成功 * code: 400失敗 */ private int code; /** * 上傳成功的返回url */ private String url; /** * 提示信息 */ private String msg; }
3、阿里雲OSS API操作工具類(直接調用阿里雲OSS API的類)
package cc.mrbird.febs.finance.util; import cc.mrbird.febs.finance.domain.dto.AliyunOssResult; import cc.mrbird.febs.finance.exception.FileUploadException; import com.aliyun.oss.OSS; import com.aliyun.oss.OSSClientBuilder; import com.aliyun.oss.model.*; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.io.*; import java.net.URL; import java.util.Date; import java.util.List; /** * @Author: sunguoqiang * @Description: TODO * @DateTime: 2022/8/3 16:23 **/ @Component @Slf4j public class AliyunOSSUtil { @Value("${aliyunOss.endpoint}") private String endpoint; @Value("${aliyunOss.accessKeyId}") private String accessKeyId; @Value("${aliyunOss.accessKeySecret}") private String accessKeySecret; @Value("${aliyunOss.bucketName}") private String bucketName; @Value("${aliyunOss.urlPrefix}") private String urlPrefix; private OSS ossClient; /** * 初始化OssClient */ @PostConstruct public void generateOSS() { OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); if (ossClient != null) { this.ossClient = ossClient; } else { log.error("OSS對象實例化失敗."); throw new RuntimeException("OSS對象實例化失敗."); } } /** * 判斷阿里雲bucket下是否存在filePath文件夾,不存在則創建 * * @param filePath */ public void isExistAndCreateFolder(String filePath) { if (!isExists(filePath)) { boolean mkdirs = createFolder(filePath); if (!mkdirs) { throw new FileUploadException("附件文件夾創建失敗!"); } } } /** * 上傳文件,以IO流方式 * * @param inputStream 輸入流 * @param objectName 唯一objectName(在oss中的文件名字) */ public AliyunOssResult upload(InputStream inputStream, String objectName) { AliyunOssResult aliyunOssResult = new AliyunOssResult(); try { // 上傳內容到指定的存儲空間(bucketName)並保存爲指定的文件名稱(objectName)。 PutObjectResult putObject = ossClient.putObject(bucketName, objectName, inputStream); // 關閉OSSClient。 ossClient.shutdown(); aliyunOssResult.setCode(200); aliyunOssResult.setUrl(urlPrefix + objectName); aliyunOssResult.setMsg("上傳成功"); } catch (Exception e) { e.printStackTrace(); aliyunOssResult.setCode(400); aliyunOssResult.setMsg("上傳失敗"); } return aliyunOssResult; } /** * 獲取oss文件 * * @param folderName * @return */ public OSSObject get(String folderName) { OSSObject ossObject = ossClient.getObject(bucketName, folderName); return ossObject; } /** * 刪除OSS中的單個文件 * * @param objectName 唯一objectName(在oss中的文件名字) */ public void delete(String objectName) { try { ossClient.deleteObject(bucketName, objectName); // 關閉OSSClient。 ossClient.shutdown(); } catch (Exception e) { e.printStackTrace(); } } /** * 批量刪除OSS中的文件 * * @param objectNames oss中文件名list */ public void delete(List<String> objectNames) { try { // 批量刪除文件。 DeleteObjectsResult deleteObjectsResult = ossClient.deleteObjects(new DeleteObjectsRequest(bucketName).withKeys(objectNames)); List<String> deletedObjects = deleteObjectsResult.getDeletedObjects(); // 關閉OSSClient。 ossClient.shutdown(); } catch (Exception e) { e.printStackTrace(); } } /** * 獲取文件臨時url * * @param objectName oss中的文件名 * @param effectiveTime 有效時間(ms) */ public String getUrl(String objectName, long effectiveTime) { // 設置URL過期時間 Date expiration = new Date(new Date().getTime() + effectiveTime); GeneratePresignedUrlRequest generatePresignedUrlRequest; generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucketName, objectName); generatePresignedUrlRequest.setExpiration(expiration); URL url = ossClient.generatePresignedUrl(generatePresignedUrlRequest); return url.toString(); } /** * oss拷貝文件 * * @param sourcePath * @param targetPath */ public void copyFileSourceToTarget(String sourcePath, String targetPath) throws FileNotFoundException { try { CopyObjectResult copyObjectResult = ossClient.copyObject(bucketName, sourcePath, bucketName, targetPath); } catch (Exception e) { throw new FileUploadException("文件轉移操作異常."); } } /** * 根據文件路徑獲取輸出流 * * @param filePath * @return * @throws IOException */ public InputStream getInputStream(String filePath) throws IOException { if (filePath == null || filePath.isEmpty()) { log.error("方法[getInputStream]參數[filePath]不能爲空."); return null; } OSSObject object = ossClient.getObject(bucketName, filePath); InputStream input = object.getObjectContent(); byte[] bytes = toByteArray(input); return new ByteArrayInputStream(bytes); } /** * InputStream流轉byte數組 * * @param input * @return * @throws IOException */ private static byte[] toByteArray(InputStream input) throws IOException { ByteArrayOutputStream output = new ByteArrayOutputStream(); byte[] buffer = new byte[input.available()]; int n = 0; while (-1 != (n = input.read(buffer))) { output.write(buffer, 0, n); } return output.toByteArray(); } /** * 創建文件夾 * * @param folderName * @return */ private Boolean createFolder(String folderName) { if (folderName == null || folderName.isEmpty()) { log.error("方法[createFolder]參數[folderName]不能爲空."); return false; } // 防止folderName文件夾因爲末尾未加【/】導致將目錄當做文件創建 if (!folderName.substring(folderName.length() - 1).equals("/")) { folderName = folderName + "/"; } // 創建文件夾 try { ossClient.putObject(bucketName, folderName, new ByteArrayInputStream(new byte[0])); log.info("附件文件夾[" + folderName + "]創建成功."); return true; } catch (Exception e) { log.error("附件文件夾[" + folderName + "]創建失敗."); return false; } } /** * 判斷文件夾是否存在 * * @param folderName * @return */ private Boolean isExists(String folderName) { return ossClient.doesObjectExist(bucketName, folderName); } /** * 根據文件路徑獲取File * * @param filePath * @return * @throws IOException */ public File getFile(String filePath) throws IOException { if (filePath == null || filePath.isEmpty()) { log.error("方法[getFile]參數[filePath]不能爲空."); return null; } File file = new File(filePath); InputStream inputStream = getInputStream(filePath); copyInputStreamToFile(inputStream, file); return file; } /** * InputStream -> File * * @param inputStream * @param file * @throws IOException */ private static void copyInputStreamToFile(InputStream inputStream, File file) throws IOException { try (FileOutputStream outputStream = new FileOutputStream(file)) { int read; byte[] bytes = new byte[1024]; while ((read = inputStream.read(bytes)) != -1) { outputStream.write(bytes, 0, read); } } } }
4、如何使用?
1、控制器
@PostMapping("/avatar") public Result<String> updateAvatar(@RequestParam("file") MultipartFile multipartFile, String username) throws Exception { return Result.success(userService.updateAvatar(multipartFile, username)); }
2、service層
@Override public String updateAvatar(MultipartFile multipartFile, String username) throws Exception { String avatarUrl = fileService.uploadAvatar(multipartFile); User user = new User(); user.setAvatar(avatarUrl); LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getUsername, username); this.baseMapper.update(user, queryWrapper); return avatarUrl; }
第一句調用FileService的上傳頭像方法。
3、FileService上傳頭像方法
@Override public String uploadAvatar(MultipartFile multipartFile) { // 設置文件上傳位置 String currentDate = DateUtil.formatNowLocalDate("yyyyMMdd"); String actualFileName = FileUtils.forStringFilter(System.nanoTime() + multipartFile.getOriginalFilename()); // 預設上傳文件到阿里雲oss的返回值 AliyunOssResult uploadResult = null; try { InputStream inputStream = new ByteArrayInputStream(multipartFile.getBytes()); String filePath = basePath + separator + SYSTEM + separator + currentDate; // 判斷是否存在文件夾,不存在則創建 aliyunOSSUtil.isExistAndCreateFolder(filePath); // 上傳文件 uploadResult = aliyunOSSUtil.upload(inputStream, filePath + separator + actualFileName); } catch (IOException e) { log.error("附件上傳出現錯誤:{}", e.getMessage(), e); throw new SystemException("服務器異常"); } return uploadResult.getUrl(); }
FileService中的保存文件方法都會將各種上傳的文件通過AliyunOSSUtil工具類上傳至阿里雲OSS。
三、報銷接入遇到的問題
1、如果FileService內部方法獲取的是MultipartFile類型文件需要上傳。
@Override @Transactional(rollbackFor = Exception.class) public FileInfoDTO uploadAttachFile(List<MultipartFile> fileList, Integer source, Integer fileType, String spNo) { try { for (int i = 0; i < fileList.size(); i++) { MultipartFile part = fileList.get(i); String originalFileName = part.getOriginalFilename(); String actualFileName = FileUtils.forStringFilter(System.nanoTime() + part.getOriginalFilename()); // 設置文件上傳位置 String currentDate = DateUtil.formatNowLocalDate("yyyyMMdd"); String filePath = basePath + separator + UPLOAD + separator + currentDate; // 判斷是否存在文件夾,不存在則創建 aliyunOSSUtil.isExistAndCreateFolder(filePath); InputStream inputStream = new ByteArrayInputStream(part.getBytes()); // 上傳文件 AliyunOssResult ossResult = aliyunOSSUtil.upload(inputStream, filePath + separator + actualFileName); String mediaId = null; // 附件上傳服務器之後還會再上傳企業微信服務器(暫時忽略這一步) if (FileSourceEnum.isNeedUploadWeChat(source) && i < UPLOAD_FILE_NUM_LIMIT - attachFileList.size() && FileTypeEnum.isApply(fileType)) { mediaId = weChatManager.uploadFileWithInputStream(part); mediaIdList.add(mediaId); } // 存儲附件表 this.generateAndAddAttachFile(originalFileName, currentDate + separator + actualFileName, source, fileType, part.getSize(), mediaId, spNo, attachFileIdList, ossResult.getUrl()); } return fileInfoDTO; } catch (IOException e) { log.error("附件上傳出現錯誤:{}", e.getMessage(), e); throw new SystemException("服務器異常"); } }
由於AliyunOSSUtil中上傳方法使用的是InputStream上傳的方式,因此可將MultipartFile類型轉換成InputStream進行上傳。
2、如果需要上傳的文件是iText程序中生成的。
如果文件不是前端傳遞,而是程序中運行時生成的,而且不能將運行時生成的文件保存在服務器中。例如iText運行時生成文件需要上傳至阿里雲oss。
public String generatePreApplyPdf(String spNo) throws DocumentException, FileNotFoundException { Map<String, String> userMap = userService.toMap(null); Map<long, String> deptMap = deptService.toMap(null); PreApplyInfo preApplyInfo = preApplyInfoService.findBySpNo(spNo); Document document = new Document(); document.setPageSize(PageSize.A4.rotate()); ByteArrayOutputStream bos = new ByteArrayOutputStream(); PdfWriter pdfWriter = generatePdfWriter(document, bos); document.open(); fillTitle(document, preApplyInfo); if (FeeTypeEnum.isTravel(preApplyInfo.getFeeType())) { fillTravelHeader(document, userMap.get(preApplyInfo.getApplicant()), cn.hutool.core.date.DateUtil.format(preApplyInfo.getApplyTime(), DatePatternEnum.CS_D.getPattern())); fillTravelLines(document, preTravelDetailService.listPreTravelDetailBySpNo(spNo), preApplyInfo); document.add(generateBlankParagraph()); fillTravelApprovalInfo(document, auditService.listPreApplyApprovalNodeVO(spNo), userMap); fillTravelRemark(document); } else { fillOtherHead(document, preApplyInfo, userMap, deptMap); fillOtherLines(document, preApplyInfo, preApplyLineService.listPreApplyDetailBySpNo(spNo)); fillOtherApprovalInfo(document, auditService.listPreApplyApprovalNodeVO(spNo), userMap); } document.close(); pdfWriter.close(); // 阿里雲oss上傳pdf ByteArrayInputStream swapStream = new ByteArrayInputStream(bos.toByteArray()); String pdfFileFolder = basePath + separator + PDF + separator; String pdfFileName = basePath + separator + PDF + separator + spNo + PDF_SUFFIX; aliyunOSSUtil.isExistAndCreateFolder(pdfFileFolder); AliyunOssResult aliyunOssResult = aliyunOSSUtil.upload(swapStream, pdfFileName); return aliyunOssResult.getCode() == 200 ? aliyunOssResult.getUrl() : "PDF文件生成失敗."; }
第八行:generatePdfWriter方法
public PdfWriter generatePdfWriter(Document document, OutputStream out) throws FileNotFoundException, DocumentException { PdfWriter writer = PdfWriter.getInstance(document, out); writer.setPageEvent(new FootHelper()); return writer; }
1、該部分是將 document(程序運行時生成的pdf內容) 寫入 out(輸出流) 中。
2、當執行到22、23行時:
document.close()
pdfWriter.close()3、表明PDF生成完畢,已經將document中的內容寫入out輸出流中。
4、25行 ByteArrayInputStream swapStream = new ByteArrayInputStream(bos.toByteArray()) 將輸出流轉成輸入流,有了輸入流就可以調用AliyunOSSUtil工具類進行上傳文件。
3、在上述附件需要上傳兩個位置(阿里雲服務器、企業微信服務器)
阿里雲上傳可以調用工具類進行操作,上傳企業微信服務器用到RestTemplate進行操作。由於文件不在本地存儲,因此無法得到File類型文件,文件類型可能是MultipartFile類型、InputStream類型,因此RestTemplate有如下幾種上傳文件方式供參考。