前言
近期自己针对附件上传进一步学习,为了弥足项目中文件上传的漏洞,保证文件上传功能的健壮性和可用性,现在我将自己在这一块的心得总结如下:
一、pom.xml依赖的引入
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- mongodb --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency> <!-- lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!-- hutool --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>4.5.1</version> </dependency>
二.application.yml配置信息
server: port: 31091 spring: servlet: multipart: max-file-size: 100MB data: mongodb: host: localhost port: 27017 database: feng fileUploadService: impl: fileMongoServiceImpl
三、MongoDB配置类
/** * @Description MongoDB配置类 * @author songwp * @date Apr 17, 2022 * @version 1.0 */ @Configuration public class MongoConfig { /** * 数据库配置信息 */ @Value("${spring.data.mongodb.database}") private String db; /** * GridFSBucket用于打开下载流 * @param mongoClient * @return */ @Bean public GridFSBucket getGridFSBucket(MongoClient mongoClient){ MongoDatabase mongoDatabase = mongoClient.getDatabase(db); return GridFSBuckets.create(mongoDatabase); } }
四、MongoDB文件实体
/** * @Description MongoDB文件实体 * @author songwp * @date Apr 17, 2022 * @version 1.0 */ @Document @Builder @Data public class MongoFile { /** * 主键 */ @Id public String id; /** * 文件名称 */ public String fileName; /** * 文件大小 */ public long fileSize; /** * 上传时间 */ public Date uploadDate; /** * MD5值 */ public String md5; /** * 文件内容 */ private Binary content; /** * 文件类型 */ public String contentType; /** * 文件后缀名 */ public String suffix; /** * 文件描述 */ public String description; /** * 大文件管理GridFS的ID */ private String gridFsId; }
五、返回统一消息处理类
/** * @Description 统一消息 * @author songwp * @date Apr 17, 2022 * @version 1.0 */ public class ResponseMessage<T> { private String status; private String message; private T data; public static ResponseMessage<?> ok() { return create("0", (String)null, (Object)null); } public static ResponseMessage<?> ok(String message) { return create("0", message, (Object)null); } public static <T> ResponseMessage<T> ok(String message, T data) { return create("0", message, data); } public static <T> ResponseMessage<T> ok(T data) { return create("0", (String)null, data); } public static ResponseMessage<?> error() { return create("1", (String)null, (Object)null); } public static ResponseMessage<?> error(String message) { return create("1", message, (Object)null); } public static <T> ResponseMessage<T> error(String message, T data) { return create("1", message, data); } private static <T> ResponseMessage<T> create(String status, String message, T data) { ResponseMessage<T> t = new ResponseMessage(); t.setStatus(status); t.setMessage(message); t.setData(data); return t; } public ResponseMessage() { } public String getStatus() { return this.status; } public String getMessage() { return this.message; } public T getData() { return this.data; } public void setStatus(final String status) { this.status = status; } public void setMessage(final String message) { this.message = message; } public void setData(final T data) { this.data = data; } }
六、统一文件下载vo
/** * @Description 统一文件下载vo * @author songwp * @date Apr 8, 2022 * @version 1.0 */ @Data public class FileExportVo { private String fileId; private String fileName; private String contentType; private String suffix; private long fileSize; @JsonIgnore private byte[] data; public FileExportVo(MongoFile mongoFile) { BeanUtil.copyProperties(mongoFile, this); if (Objects.nonNull(mongoFile.getContent())) { this.data = mongoFile.getContent().getData(); } this.fileId = mongoFile.getId(); } }
七、MD5工具类
/** * @Description MD5工具类 * @date Apr 8, 2022 * @version 1.0 */ public class MD5Util { /** * 获取该输入流的MD5值 */ public static String getMD5(InputStream is) throws NoSuchAlgorithmException, IOException { StringBuffer md5 = new StringBuffer(); MessageDigest md = MessageDigest.getInstance("MD5"); byte[] dataBytes = new byte[1024]; int nread = 0; while ((nread = is.read(dataBytes)) != -1) { md.update(dataBytes, 0, nread); }; byte[] mdbytes = md.digest(); // convert the byte to hex format for (int i = 0; i < mdbytes.length; i++) { md5.append(Integer.toString((mdbytes[i] & 0xff) + 0x100, 16).substring(1)); } return md5.toString(); } }
八、MongoDB文件仓储
/** * @Description MongoDB文件仓储 * @author songwp * @date Apr 17, 2022 * @version 1.0 */ public interface MongoFileRepository extends MongoRepository<MongoFile, String> {
九、文件上传业务接口
/** * @Description 文件上传接口 * @author songwp * @date Apr 17, 2022 * @version 1.0 */ public interface FileUploadService { /** * 文件上传 * @param file * @return */ FileExportVo uploadFile(MultipartFile file) throws Exception; /** * 多文件上传 * @param files * @return */ List<FileExportVo> uploadFiles(List<MultipartFile> files); /** * 文件下载 * @param fileId * @return */ FileExportVo downloadFile(String fileId); /** * 文件删除 * @param fileId */ void removeFile(String fileId); }
十、MongoDB文件上传实现类
/** * @Description MongoDB文件上传实现类 * @author songwp * @date Apr 17, 2022 * @version 1.0 */ @Slf4j @Service("fileMongoServiceImpl") @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class FileMongoServiceImpl implements FileUploadService { private final MongoFileRepository mongoFileRepository; private final MongoTemplate mongoTemplate; private final GridFsTemplate gridFsTemplate; private final GridFSBucket gridFSBucket; /** * 多文件上传 * @param files * @return */ @Override public List<FileExportVo> uploadFiles(List<MultipartFile> files) { return files.stream().map(file -> { try { return this.uploadFile(file); } catch (Exception e) { log.error("文件上传失败", e); return null; } }).filter(Objects::nonNull).collect(Collectors.toList()); } /** * 文件上传 * @param file * @return * @throws Exception */ @Override public FileExportVo uploadFile(MultipartFile file) throws Exception { if (file.getSize() > 16777216) { return this.saveGridFsFile(file); } else { return this.saveBinaryFile(file); } } /** * 文件下载 * @param fileId * @return */ @Override public FileExportVo downloadFile(String fileId) { Optional<MongoFile> option = this.getBinaryFileById(fileId); if (option.isPresent()) { MongoFile mongoFile = option.get(); if(Objects.isNull(mongoFile.getContent())){ option = this.getGridFsFileById(fileId); } } return option.map(FileExportVo::new).orElse(null); } /** * 文件删除 * @param fileId */ @Override public void removeFile(String fileId) { Optional<MongoFile> option = this.getBinaryFileById(fileId); if (option.isPresent()) { if (Objects.nonNull(option.get().getGridFsId())) { this.removeGridFsFile(fileId); } else { this.removeBinaryFile(fileId); } } } /** * 删除Binary文件 * @param fileId */ public void removeBinaryFile(String fileId) { mongoFileRepository.deleteById(fileId); } /** * 删除GridFs文件 * @param fileId */ public void removeGridFsFile(String fileId) { // TODO 根据id查询文件 MongoFile mongoFile = mongoTemplate.findById(fileId, MongoFile.class ); if(Objects.nonNull(mongoFile)){ // TODO 根据文件ID删除fs.files和fs.chunks中的记录 Query deleteFileQuery = new Query().addCriteria(Criteria.where("filename").is(mongoFile.getGridFsId())); gridFsTemplate.delete(deleteFileQuery); // TODO 删除集合mongoFile中的数据 Query deleteQuery = new Query(Criteria.where("id").is(fileId)); mongoTemplate.remove(deleteQuery, MongoFile.class); } } /** * 保存Binary文件(小文件) * @param file * @return * @throws Exception */ public FileExportVo saveBinaryFile(MultipartFile file) throws Exception { String suffix = getFileSuffix(file); MongoFile mongoFile = mongoFileRepository.save( MongoFile.builder() .fileName(file.getOriginalFilename()) .fileSize(file.getSize()) .content(new Binary(file.getBytes())) .contentType(file.getContentType()) .uploadDate(new Date()) .suffix(suffix) .md5(MD5Util.getMD5(file.getInputStream())) .build() ); return new FileExportVo(mongoFile); } /** * 保存GridFs文件(大文件) * @param file * @return * @throws Exception */ public FileExportVo saveGridFsFile(MultipartFile file) throws Exception { String suffix = getFileSuffix(file); String gridFsId = this.storeFileToGridFS(file.getInputStream(), file.getContentType()); MongoFile mongoFile = mongoTemplate.save( MongoFile.builder() .fileName(file.getOriginalFilename()) .fileSize(file.getSize()) .contentType(file.getContentType()) .uploadDate(new Date()) .suffix(suffix) .md5(MD5Util.getMD5(file.getInputStream())) .gridFsId(gridFsId) .build() ); return new FileExportVo(mongoFile); } /** * 上传文件到Mongodb的GridFs中 * @param in * @param contentType * @return */ public String storeFileToGridFS(InputStream in, String contentType){ String gridFsId = IdUtil.simpleUUID(); // TODO 将文件存储进GridFS中 gridFsTemplate.store(in, gridFsId , contentType); return gridFsId; } /** * 获取Binary文件 * @param id * @return */ public Optional<MongoFile> getBinaryFileById(String id) { return mongoFileRepository.findById(id); } /** * 获取Grid文件 * @param id * @return */ public Optional<MongoFile> getGridFsFileById(String id){ MongoFile mongoFile = mongoTemplate.findById(id , MongoFile.class ); if(Objects.nonNull(mongoFile)){ Query gridQuery = new Query().addCriteria(Criteria.where("filename").is(mongoFile.getGridFsId())); try { // TODO 根据id查询文件 GridFSFile fsFile = gridFsTemplate.findOne(gridQuery); // TODO 打开流下载对象 GridFSDownloadStream in = gridFSBucket.openDownloadStream(fsFile.getObjectId()); if(in.getGridFSFile().getLength() > 0){ // TODO 获取流对象 GridFsResource resource = new GridFsResource(fsFile, in); // TODO 获取数据 mongoFile.setContent(new Binary(IoUtil.readBytes(resource.getInputStream()))); return Optional.of(mongoFile); }else { return Optional.empty(); } }catch (IOException e){ log.error("获取MongoDB大文件失败", e); } } return Optional.empty(); } /** * 获取文件后缀 * @param file * @return */ private String getFileSuffix(MultipartFile file) { String suffix = ""; if (Objects.requireNonNull(file.getOriginalFilename()).contains(".")) { suffix = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(".")); } return suffix; }
十一、文件上传接口-控制器
/** * @Description 文件上传接口 * @author songwp * @date Apr 17, 2022 * @version 1.0 */ @Slf4j @RestController @RequestMapping("/file") public class FileUploadController { /** * 文件上传实现类 */ @Resource private FileUploadService fileUploadService; /** * 文件上传 * @param file * @return */ @PostMapping("/upload") public ResponseMessage<?> uploadFile(@RequestParam(value = "file") MultipartFile file) { try { return ResponseMessage.ok("上传成功", fileUploadService.uploadFile(file)); } catch (Exception e) { log.error("文件上传失败:", e); return ResponseMessage.error(e.getMessage()); } } /** * 多文件上传 * @param files * @return */ @PostMapping("/uploadFiles") public ResponseMessage<?> uploadFile(@RequestParam(value = "files") List<MultipartFile> files) { try { return ResponseMessage.ok("上传成功", fileUploadService.uploadFiles(files)); } catch (Exception e) { return ResponseMessage.error(e.getMessage()); } } /** * 文件下载 * @param fileId * @return */ @GetMapping("/download/{fileId}") public ResponseEntity<Object> fileDownload(@PathVariable(name = "fileId") String fileId) { FileExportVo fileExportVo = fileUploadService.downloadFile(fileId); if (Objects.nonNull(fileExportVo)) { return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "fileName="" + fileExportVo.getFileName() + """) .header(HttpHeaders.CONTENT_TYPE, fileExportVo.getContentType()) .header(HttpHeaders.CONTENT_LENGTH, fileExportVo.getFileSize() + "").header("Connection", "close") .body(fileExportVo.getData()); } else { return ResponseEntity.status(HttpStatus.NOT_FOUND).body("file does not exist"); } } /** * 文件删除 * @param fileId * @return */ @DeleteMapping("/remove/{fileId}") public ResponseMessage<?> removeFile(@PathVariable(name = "fileId") String fileId) { fileUploadService.removeFile(fileId); return ResponseMessage.ok("删除成功"); } }
十二、接口测试
1.单个文件上传
2.多文件上传
3.文件下载
4.文件删除