文件上传接入阿里云OSS
目的:将文件交给阿里云进行管理,可避免文件对本地服务器资源的占用,阿里云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有如下几种上传文件方式供参考。
2023-11-01 记录:
1、新建springboot项目使用阿里云oss测试上传图片总是出错:看了官网才发现,如果是java9以下的版本只需要引入
<dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.15.1</version> </dependency>
但是如果使用的jdk版本大于9则需要额外引入三个pom
<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency> <!-- no more than 2.3.3--> <dependency> <groupId>org.glassfish.jaxb</groupId> <artifactId>jaxb-runtime</artifactId> <version>2.3.3</version> </dependency>
引入后,正常使用问题解决。