1.简单文件的上传
1.1.大体结构图
1.2.数据库表的结构
**注:**数据库表只保存上传的信息,具体的文件保存在OSS中
1.3.Controller层
@PostMapping(value = "up-load")
@ApiOperation(value = "文件上传-简单上传",notes = "文件上传-简单上传")
@ApiImplicitParams({
@ApiImplicitParam(paramType = "form", name = "file", value = "文件对象", required = true, dataTypeClass = MultipartFile.class)
})
@ApiOperationSupport(includeParameters = {"fileVO.businessType","fileVO.bucketName","fileVO.storeFlag","fileVO.autoCatalog"})
//1.MultipartFile是Spring框架处理文件的接口
public ResponseResult<FileVO> upLoad(
@RequestParam("file") MultipartFile file,
FileVO fileVO) throws IOException {
fileVO.setCompanyNo(SubjectContent.getCompanyNo());
//2.我自定义的一个文件的构造类,下文会给出
UploadMultipartFile uploadMultipartFile = UploadMultipartFile
.builder()
.originalFilename(file.getOriginalFilename())
.fileByte(IOUtils.toByteArray(file.getInputStream()))
.build();
//执行文件上传
FileVO fileVOResult = fileService.upLoad(uploadMultipartFile, fileVO);
return ResponseResultBuild.successBuild(fileVOResult);
}
//自定义文件类,将文件的内容直接转化为字节,因为文件较小,所以这么处理很方便
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UploadMultipartFile implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "文件名称")
public String originalFilename;
@ApiModelProperty(value = "文件数组")
public byte[] fileByte;
}
1.4.Service层
@Override
@Caching(
evict = {
@CacheEvict(value = FileCacheConstant.PAGE,allEntries = true),
@CacheEvict(value = FileCacheConstant.BUSINESS_KEY,allEntries = true)},
put={@CachePut(value =FileCacheConstant.BASIC,key = "#result.id")})
@Transactional
public FileVO upLoad(UploadMultipartFile multipartFile, FileVO fileVO) throws ProjectException {
//1.流式处理,因为后续阿里云的操作都需要流
ByteArrayInputStream byteArrayInputStream =new ByteArrayInputStream(multipartFile.getFileByte());
try {
//2.文件重命名
String filename = identifierGenerator.nextId(fileVO)+"-"+multipartFile.getOriginalFilename();;
fileVO.setFileName(filename);
//3.文件后缀名
String suffix = fileVO.getFileName().substring(fileVO.getFileName().lastIndexOf("."));
fileVO.setSuffix(suffix);
//3.调用简单上传,其实就是调用阿里云的OSS,具体的教程可以看其他教程
String pathUrl = fileStorageAdapter.uploadFile(fileVO, byteArrayInputStream);
//4.用到我自定义的转化类型方法,将数据保存在数据库中
File file = BeanConv.toBean(fileVO, File.class);
file.setStatus(FileConstant.STATUS_SUCCEED);
file.setPathUrl(pathUrl);
boolean flag = save(file);
if (!flag){
throw new ProjectException(FileEnum.UPLOAD_FAIL);
}
//5.补全完整路径,这一部分是返回给前端的数据,让其有地址能显示出来
pathUrl = fileUrlContext.getFileUrl(fileVO.getStoreFlag(), pathUrl);
fileVO.setId(file.getId());
fileVO.setPathUrl(pathUrl);
//6.这是垃圾文件处理的内容,后面具体会提到
Long messageId = (Long) identifierGenerator.nextId(fileVO);
MqMessage mqMessage = MqMessage.builder()
.id(messageId)
.title("file-message")
.content(JSONObject.toJSONString(fileVO))
.messageType("file-request")
.produceTime(Timestamp.valueOf(LocalDateTime.now()))
.sender("system")
.build();
Message<MqMessage> message = MessageBuilder.withPayload(mqMessage).setHeader("x-delay", fileDelayTime).build();
fileSource.fileOutput().send(message);
return fileVO;
//7.异常处理情况
}catch (Exception e) {
log.error("文件上传异常:{}", ExceptionsUtil.getStackTraceAsString(e));
throw new ProjectException(FileEnum.UPLOAD_FAIL);
}finally {
if (byteArrayInputStream != null) {
try {
byteArrayInputStream.close();
} catch (Exception e) {
log.error("文件上传操作失败:{}", ExceptionsUtil.getStackTraceAsString(e));
}
}
}
}
1.5.效果图
可以看到,目前是没有business_id这个字段的,因为我还没有提交,只有提交才能有business_id字段如果在此时我没有点击保存而是直接退出,这就成了垃圾文件,所以这就涉及到了垃圾自动回收的功能,具体的垃圾回收在介绍完所有文件的上传机制的时候统一讲述。
2.简单文件的替换操作
2.1.Controller层
@PutMapping(value = "replace-bind-batch-file")
@ApiOperation(value = "移除业务原图片,并绑定新的图片到业务上",notes = "移除业务原图片,并绑定新的图片到业务上")
@ApiImplicitParam(name = "fileVOs",value = "附件对象",required = true,dataType = "FileVO")
//与简单文件上传不一样,替换操作的参数是带有BusinessId的
public ResponseResult<Boolean> replaceBindBatchFile(@RequestBody List<FileVO> fileVOs){
Boolean flag = fileService.replaceBindBatchFile(fileVOs);
return ResponseResultBuild.successBuild(flag);
}
2.2.Service层
@Override
@Transactional
@Caching(evict = {@CacheEvict(value = FileCacheConstant.PAGE,allEntries = true),
@CacheEvict(value = FileCacheConstant.BASIC,allEntries = true),
@CacheEvict(value = FileCacheConstant.LIST,allEntries = true),
@CacheEvict(value = FileCacheConstant.BUSINESS_KEY,key = "#fileVOs.get(0).getBusinessId()")})
public Boolean replaceBindBatchFile(List<FileVO> fileVOs) {
try {
//1.查询新老图片id集合,注意,只有BusinessId才能被查出来,垃圾文件如果没删除也删不出来
QueryWrapper<File> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(File::getBusinessId,fileVOs.get(0).getBusinessId());
List<File> oldList = list(queryWrapper);
List<Long> oldIds = oldList.stream().map(File::getId).collect(Collectors.toList());
List<Long> newIds = fileVOs.stream().map(FileVO::getId).collect(Collectors.toList());
//2.我的前端最多可以传三张图片,所以这里处理的是集合的差
List<Long> delIds = oldIds.stream().filter(n -> {
return !newIds.contains(n);
}).collect(Collectors.toList());
//3.删除图片,这个方法既删除了OSS中的图片,也删除了数据库的记录
if (!EmptyUtil.isNullOrEmpty(delIds)){
deleteInIds(delIds);
}
//4.绑定新图片
List<FileVO> newFiles = fileVOs.stream().filter(n -> {
return !oldIds.contains(n.getId());
}).collect(Collectors.toList());
if (!EmptyUtil.isNullOrEmpty(newFiles)){
List<FileVO> fileVOsResult = bindBatchFile(newFiles);
return !EmptyUtil.isNullOrEmpty(fileVOsResult);
}
return true;
}catch (Exception e){
log.error("查询文件分页异常:{}", ExceptionsUtil.getStackTraceAsString(e));
throw new ProjectException(FileEnum.BIND_FAIL);
}
}
2.3.实际效果图
3.大文件的分片上传
3.1.大体结构图
3.2.文件初始化
前端调佣初始化接口,告知三方对象存储我需要进行文件分片上传,三方对象存储收到信息后返回uploadId。
controller层
@PostMapping(value = "initiate-multipart-up-load")
@ApiOperation(value = "文件分片上传-初始化",notes = "文件分片上传-初始化")
@ApiImplicitParam(name = "fileVO",value = "文件对象",required = true,dataType = "FileVO")
public ResponseResult<FileVO> initiateMultipartUpload(
@RequestBody FileVO fileVO){
fileVO.setCompanyNo(SubjectContent.getCompanyNo());
//初始化上传Id
FileVO fileVOResult = fileService.initiateMultipartUpload(fileVO);
return ResponseResultBuild.successBuild(fileVOResult);
}
Service层
@Override
@Caching(evict = {
@CacheEvict(value = FileCacheConstant.PAGE,allEntries = true),
@CacheEvict(value = FileCacheConstant.BUSINESS_KEY,allEntries = true)},
put={@CachePut(value =FileCacheConstant.BASIC,key = "#result.id")})
@Transactional
public FileVO initiateMultipartUpload(FileVO fileVO) {
try {
//1.文件重命名
String filename = identifierGenerator.nextId(fileVO)+"-"+fileVO.getFileName();
fileVO.setFileName(filename);
//2.文件后缀名
String suffix = fileVO.getFileName().substring(fileVO.getFileName().lastIndexOf("."));
fileVO.setSuffix(suffix);
//3.分片上传-初始化,具体的代码下文给出
File file = fileStorageAdapter.initiateMultipartUpload(fileVO);
//4.保存数据库
file.setBusinessType(fileVO.getBusinessType());
file.setSuffix(suffix);
file.setStoreFlag(fileVO.getStoreFlag());
file.setMd5(fileVO.getMd5());
file.setCompanyNo(fileVO.getCompanyNo());
file.setStatus(FileConstant.STATUS_SENDING);
boolean flag = save(file);
if (!flag){
throw new ProjectException(FileEnum.UPLOAD_FAIL);
}
//5.补全完整路径
FileVO fileVOResult = BeanConv.toBean(file, FileVO.class);
String pathUrl = fileUrlContext.getFileUrl(fileVOResult.getStoreFlag(), fileVOResult.getPathUrl());
fileVOResult.setPathUrl(pathUrl);
//6.这里依然是垃圾文件处理的机制,如果我们只提交了文件没有将其保存的话不会有业务id,所以是垃圾文件,会被我们定时清除
Long messageId = (Long) identifierGenerator.nextId(fileVOResult);
MqMessage mqMessage = MqMessage.builder()
.id(messageId)
.title("file-message")
.content(JSONObject.toJSONString(fileVOResult))
.messageType("file-request")
.produceTime(Timestamp.valueOf(LocalDateTime.now()))
.sender("system")
.build();
Message<MqMessage> message = MessageBuilder.withPayload(mqMessage).setHeader("x-delay", fileDelayTime).build();
fileSource.fileOutput().send(message);
return fileVOResult;
} catch (Exception e) {
log.error("文件上传初始化异常:{}", ExceptionsUtil.getStackTraceAsString(e));
throw new ProjectException(FileEnum.INIT_UPLOAD_FAIL);
}
}
//3.具体OSS部分初始化代码
@Override
public File initiateMultipartUpload(String suffix, String filename, String bucketName, boolean autoCatalog) {
// 是否自动生成存储路径并设置文件路径和名称(Key)
String key = filename;
InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, key);
// 如果需要在初始化分片时设置请求头,请参考以下示例代码。
ObjectMetadata metadata = fileMetaHandler(suffix);
metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
// 指定该Object的网页缓存行为。
metadata.setCacheControl("no-cache");
// 指定该Object被下载时的名称。
metadata.setContentDisposition("inline;filename="+key);
// 指定该Object的内容编码格式。
metadata.setContentEncoding(OSSConstants.DEFAULT_CHARSET_NAME);
//指定请求
request.setObjectMetadata(metadata);
// 初始化分片。
InitiateMultipartUploadResult upresult = ossClient.initiateMultipartUpload(request);
// 设置权限(公开读)
ossClient.setBucketAcl(bucketName, CannedAccessControlList.PublicRead);
// 返回uploadId,它是分片上传事件的唯一标识。您可以根据该uploadId发起相关的操作,例如取消分片上传、查询分片上传等。
return File.builder().bucketName(bucketName).pathUrl(key).fileName(key).uploadId(upresult.getUploadId()).build();
}
3.3.文件分片
前端把大文件拆分多个小文件,每次携带uploadId及分片文件进行上传,三方对象存储收到信息后返回分片partETag信息
controller层
@PostMapping(value = "up-load-part")
@ApiOperation(value = "文件分片上传-上传分片",notes = "文件分片上传-上传分片")
@ApiImplicitParams({
@ApiImplicitParam(paramType = "form", name = "file", value = "文件对象", required = true, dataTypeClass = MultipartFile.class)
})
public ResponseResult<String> uploadPart(
@RequestParam("file") MultipartFile file,
FilePartVO filePartVO)throws IOException {
filePartVO.setCompanyNo(SubjectContent.getCompanyNo());
//构建文件上次对象
UploadMultipartFile uploadMultipartFile = UploadMultipartFile
.builder()
.originalFilename(file.getOriginalFilename())
.fileByte(IOUtils.toByteArray(file.getInputStream()))
.build();
//上传分片返回partETagJson
String partETagJson = fileService.uploadPart(uploadMultipartFile,filePartVO);
return ResponseResultBuild.successBuild(partETagJson);
}
Service层
@Override
@Transactional
public String uploadPart(UploadMultipartFile multipartFile, FilePartVO filePartVO) {
try {
//1.上传分片数据,上传到OSS中,具体代码下文给出
String partETagString = fileStorageAdapter.uploadPart(filePartVO, new ByteArrayInputStream(multipartFile.getFileByte()));
//2.保存分片信息,数据库中保存分片信息
FilePart filePart = BeanConv.toBean(filePartVO, FilePart.class);
filePart.setUploadResult(partETagString);
filePartService.save(filePart);
return partETagString;
}catch (Exception e) {
log.error("文件分片上传异常:{}", ExceptionsUtil.getStackTraceAsString(e));
throw new ProjectException(FileEnum.UPLOAD_PART_FAIL);
}
}
//1.具体的OSS操作代码
@Override
public String uploadPart(String upLoadId, String filename, int partNumber, long partSize, String bucketName, InputStream inputStream) {
//封装分片上传请求
UploadPartRequest uploadPartRequest = new UploadPartRequest();
uploadPartRequest.setUploadId(upLoadId);
//part大小 1-10000
uploadPartRequest.setPartNumber(partNumber);
uploadPartRequest.setPartSize(partSize);
//文件上传的bucketName
uploadPartRequest.setBucketName(bucketName);
//分片文件
uploadPartRequest.setInputStream(inputStream);
uploadPartRequest.setKey(filename);
// 每个分片不需要按顺序上传,甚至可以在不同客户端上传,OSS会按照分片号排序组成完整的文件。
UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
log.info("{}文件第 {} 片上传成功,上传结果:{}", upLoadId, uploadPartRequest.getPartNumber(),JSON.toJSON(uploadPartResult));
return JSONObject.toJSONString(uploadPartResult.getPartETag());
}
3.4.合并分片
controller层
@PostMapping(value = "complete-multipart-up-load")
@ApiOperation(value = "文件分片上传-合并分片",notes = "文件分片上传-合并分片")
@ApiImplicitParam(name = "fileVO",value = "文件对象",required = true,dataType = "FileVO")
public ResponseResult<String> completeMultipartUpload(
@RequestBody FileVO fileVO)throws IOException {
//问上传分片返回partETagJson
String eTagJson = fileService.completeMultipartUpload(fileVO);
return ResponseResultBuild.successBuild(eTagJson);
}
Service层
@Override
@Transactional
public String completeMultipartUpload(FileVO fileVO) {
try {
//1.移除分片记录
Boolean flag = filePartService.deleteFilePartByUpLoadId(fileVO.getUploadId());
if (!flag){
throw new ProjectException(FileEnum.COMPLETE_PART_FAIL);
}
//2.修改文件记录状态
fileVO.setStatus(FileConstant.STATUS_SUCCEED);
flag = updateById(BeanConv.toBean(fileVO,File.class));
if (!flag){
throw new ProjectException(FileEnum.COMPLETE_PART_FAIL);
}
//3.合并结果,调用阿里OSS,具体代码在下面
return fileStorageAdapter.completeMultipartUpload(fileVO);
} catch (Exception e) {
log.error("文件分片上传异常:{}", ExceptionsUtil.getStackTraceAsString(e));
throw new ProjectException(FileEnum.COMPLETE_PART_FAIL);
}
}
//3.具体代码
@Override
public String completeMultipartUpload(String upLoadId, List<String> partETags, String filename, String bucketName) {
StopWatch st = new StopWatch();
st.start();
//转换jsonarray为list
List<PartETag> partETagList =Lists.newArrayList();
partETags.forEach(n->{
partETagList.add(JSONObject.parseObject(n,PartETag.class));
});
CompleteMultipartUploadRequest completeMultipartUploadRequest =
new CompleteMultipartUploadRequest(bucketName, filename, upLoadId, partETagList);
log.info("{}文件上传完成,开始合并,partList:{}", upLoadId, partETags);
// 完成分片上传。
CompleteMultipartUploadResult completeMultipartUploadResult = ossClient.completeMultipartUpload(completeMultipartUploadRequest);
st.stop();
log.info("{}文件上传完成,上传结果:{},耗时:{}", upLoadId, JSON.toJSON(completeMultipartUploadResult), st.getTotalTimeMillis());
return completeMultipartUploadResult.getETag();
}
4.垃圾文件的处理
4.1.利用XXL-JOB进行定时清理
在使用下面的代码之前,需要自行配置XXL-JOB
执行器任务
@Component
public class ClearFileHandlerJob {
@Autowired
FileBusinessFeign fileBusinessFeign;
@XxlJob(value = "clear-file")
public ReturnT<String> execute(String param) {
Boolean responseWrap = fileBusinessFeign.clearFile();
if (responseWrap){
ReturnT.SUCCESS.setMsg("计划任务:清理垃圾文件-成功");
return ReturnT.SUCCESS;
}
ReturnT.FAIL.setMsg("计划任务:清理垃圾文件-失败");
return ReturnT.FAIL;
}
}
微服务接口
@FeignClient(value = "file-web",fallback = FileBusinessHystrix.class)
public interface FileBusinessFeign {
/**
* @Description 定时清理文件
* @return
*/
@DeleteMapping("file-feign/clear-file")
Boolean clearFile();
}
控制层处理
@RestController
@RequestMapping("file-feign")
@Api(tags = "附件feign-controller")
@Slf4j
public class FileBusinessFeignController {
@Autowired
IFileService fileService;
/**
* @Description 定时清理文件
* @return Boolean
*/
@DeleteMapping("clear-file")
@ApiOperation(value = "删除业务对应附件",notes = "删除业务对应附件")
public Boolean clearFile(){
return fileService.clearFile();
}
}
业务服务层
@Override
@Caching(evict = {@CacheEvict(value = FileCacheConstant.PAGE,allEntries = true),
@CacheEvict(value = FileCacheConstant.BUSINESS_KEY,allEntries = true),
@CacheEvict(value = FileCacheConstant.BASIC,allEntries = true)})
@Transactional
public Boolean clearFile() {
try {
//查询需要清理的文件
List<FileVO> fileList = needClearFile();
if (EmptyUtil.isNullOrEmpty(fileList)){
return true;
}
List<Long> fileListIds = fileList.stream().map(FileVO::getId).collect(Collectors.toList());
//移除数据库信息
Boolean flag = removeByIds(fileListIds);
//移除对象存储数据
for (FileVO fileVO : fileList) {
fileStorageAdapter.delete(fileVO.getStoreFlag(),fileVO.getBucketName(),fileVO.getPathUrl());
}
return flag;
}catch (Exception e){
log.error("定时清理文件异常:{}", ExceptionsUtil.getStackTraceAsString(e));
throw new ProjectException(FileEnum.CLEAR_FILE_TASK_FAIL);
}
}
4.2.利用延迟任务延迟清理
逻辑:当文件上传对象存储且保存数据库后,上传方法最后会发送延迟信息当前文件记录信息到RabbitMQ中,task-listener10分钟后消费到消息,然后通过openfegin调用file-web中的查询处理方法,如果当前文件记录还是没有businessId,则认为是垃圾文件进行删除
具体的实现不予给出,前面延迟视频的代码可以参考,不过直接使用新推出的delayed-message-exchange插件会更加方便哦