一、文件上传
1、接口定义
首先分析接口:
请求地址:/media/upload/coursefile
请求参数:
Content-Type: multipart/form-data;boundary=.....
FormData: filedata=??
定义接口如下:
@RequestMapping(value = "/upload/coursefile",consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
public UploadFileResultDto upload(@RequestPart("filedata") MultipartFile filedata,//文件数据
@RequestParam("folder") String folder,//文件路径
@RequestParam("objectName") String objectName){
return mediaFileService.uploadFile();
}
文件数据可能是各种格式的,所以用MultipartFile接收,并加上注解@RequestPart
由于接口定义时未指定是Post请求还是Get请求,所以该接口用@RequestMapping注解
2、service层实现业务逻辑
代码比较复杂,我们分成两部分说明
(1)将文件上传到Minio的桶中
要上传到Minio,首先需要获取桶的名称和桶内的文件路径
桶的名称我们通过读取配置信息获取
@Value("${minio.bucket.files}")
private String bucket_Files;
桶内的文件路径又分为两部分,folder和objectName
folder是目录名,如果参数中folder不为空,直接用就行,如果为空,就使用默认的方法(如下)构建目录名
private String getFileFolder(Date date, boolean year, boolean month, boolean day) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
//获取当前日期字符串
String dateString = sdf.format(new Date());
//取出年、月、日
String[] dateStringArray = dateString.split("-");
StringBuffer folderString = new StringBuffer();
if (year) {
folderString.append(dateStringArray[0]);
folderString.append("/");
}
if (month) {
folderString.append(dateStringArray[1]);
folderString.append("/");
}
if (day) {
folderString.append(dateStringArray[2]);
folderString.append("/");
}
return folderString.toString();
}
最后还要对folder做检查,因为可能不带"/"导致路径格式不正确
if (folder.indexOf("/") < 0) {
folder = folder + "/";
}
与此类似,objectName不为空就直接使用,为空则用MD5值+文件扩展名的形式构建
//生成文件id,文件的md5值
String fileId = DigestUtils.md5Hex(bytes);
//构造objectname
if (StringUtils.isEmpty(objectName)) {
objectName = fileId + filename.substring(filename.lastIndexOf("."));
}
最后将folder和objectName合并即为最后的路径名
objectName = folder + objectName;
上传
try {
//转为流
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
PutObjectArgs putObjectArgs = PutObjectArgs.builder().bucket(bucket_Files).object(objectName)
//-1表示文件分片按5M(不小于5M,不大于5T),分片数量最大10000,
.stream(byteArrayInputStream, byteArrayInputStream.available(), -1)
.contentType(uploadFileParamsDto.getContentType())
.build();
minioClient.putObject(putObjectArgs);
(2)将数据插入mediaFile表中
//从数据库查询文件
mediaFiles = mediaFilesMapper.selectById(fileId);
if (mediaFiles == null) {
mediaFiles = new MediaFiles();
//拷贝基本信息
BeanUtils.copyProperties(uploadFileParamsDto, mediaFiles);
mediaFiles.setId(fileId);
mediaFiles.setFileId(fileId);
mediaFiles.setCompanyId(companyId);
mediaFiles.setUrl("/" + bucket_Files + "/" + objectName);
mediaFiles.setBucket(bucket_Files);
mediaFiles.setCreateDate(LocalDateTime.now());
mediaFiles.setStatus("1");
mediaFiles.setAuditStatus("002003");
//保存文件信息到文件表
int insert = mediaFilesMapper.insert(mediaFiles);
if (insert < 0) {
XueChengPlusException.cast("保存文件信息失败");
}
UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);
return uploadFileResultDto;
(3)代码优化
显然上面的业务逻辑代码过于繁杂,我们进行拆分优化,本身比较简单,直接上代码
先将上传文件到Minio的代码抽离
/**
* @param bytes 文件字节数组
* @param bucket 桶
* @param objectName 对象名称
* @param contentType 内容类型
* @return void
* @description 将文件写入minIO
* @author Mr.M
* @date 2022/10/12 21:22
*/
public void addMediaFilesToMinIO(byte[] bytes, String bucket, String objectName, String contentType) {
try {
//转为流
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
PutObjectArgs putObjectArgs = PutObjectArgs.builder().bucket(bucket_Files).object(objectName)
//-1表示文件分片按5M(不小于5M,不大于5T),分片数量最大10000,
.stream(byteArrayInputStream, byteArrayInputStream.available(), -1)
.contentType(contentType)
.build();
minioClient.putObject(putObjectArgs);
//从数据库查询文件
} catch (Exception e) {
log.error("上传文件失败:{}", e.getMessage());
XueChengPlusException.cast("上传过程中出错");
}
}
再将写入文件信息到数据库的代码抽离
/**
* @param companyId 机构id
* @param fileMd5 文件md5值
* @param uploadFileParamsDto 上传文件的信息
* @param bucket 桶
* @param objectName 对象名称
* @return com.xuecheng.media.model.po.MediaFiles
* @description 将文件信息添加到文件表
* @author smeehc
*/
public MediaFiles addMediaFilesToDb(Long companyId, String fileMd5, UploadFileParamsDto uploadFileParamsDto, String bucket, String objectName) {
MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
if (mediaFiles == null) {
mediaFiles = new MediaFiles();
//拷贝基本信息
BeanUtils.copyProperties(uploadFileParamsDto, mediaFiles);
mediaFiles.setId(fileMd5);
mediaFiles.setFileId(fileMd5);
mediaFiles.setCompanyId(companyId);
mediaFiles.setUrl("/" + bucket + "/" + objectName);
mediaFiles.setBucket(bucket);
mediaFiles.setCreateDate(LocalDateTime.now());
mediaFiles.setStatus("1");
mediaFiles.setAuditStatus("002003");
//保存文件信息到文件表
int insert = mediaFilesMapper.insert(mediaFiles);
if (insert < 0) {
XueChengPlusException.cast("保存文件信息失败");
}
}
return mediaFiles;
}
最后修改一下upload方法
@Transactional
@Override
public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName) {
//生成文件id,文件的md5值
String fileId = DigestUtils.md5Hex(bytes);
//文件名称
String filename = uploadFileParamsDto.getFilename();
//构造objectname
if (StringUtils.isEmpty(objectName)) {
objectName = fileId + filename.substring(filename.lastIndexOf("."));
}
if (StringUtils.isEmpty(folder)) {
//通过日期构造文件存储路径
folder = getFileFolder(new Date(), true, true, true);
} else if (folder.indexOf("/") < 0) {
folder = folder + "/";
}
//对象名称
objectName = folder + objectName;
MediaFiles mediaFiles = null;
try {
addMediaFilesToMinIO(bytes,bucket_Files,objectName,uploadFileParamsDto.getContentType());
mediaFiles = addMediaFilesToDb(companyId,fileId,uploadFileParamsDto,bucket_Files,objectName);
UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);
return uploadFileResultDto;
} catch (Exception e){
XueChengPlusException.cast("上传过程中出错");
}
return null;
}
二、@Transactional
可以简单认为,在数据库进行两次或多次写操作时,需要加上@Transactional注解表示这是一个事务,这跟事务的基本要素有关
事务基本要素
- 原子性(Atomicity): 事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
- 一致性(Consistency): 事务开始前和结束后,数据库的完整性约束没有被破坏。比如A向B转账,不可能A扣了钱,B却没收到。
- 隔离性(Isolation): 同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
- 持久性(Durability): 事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
注意:该注解一般加载接口的实现类上的public方法上
如果想要被事务控制,有两个条件
1、方法被代理对象调用
2、方法上加入@Transactional
当无事务方法调用有事务方法时是否是用代理对象调用?
分析源码可知,当无事务方法调用有事务方法时,spring会向其传入被代理对象(即impl对象),而不是代理对象,这样调用时不满足方法被代理对象调用这个点,所以有事务方法此时不能被事务控制
解决方案:将代理对象注入serviceImpl中,通过代理对象去调用有事务方法
@Autowired
MediaFileService currentProxy;
什么时候spring事务会失效
- 在方法中捕获异常而没有抛出去
- 非事务方法调用事务方法
- 事务方法内部调用事务方法
- @Transactional标记的不是public方法
- 抛出的异常与rollbackfor指定的异常(默认为RuntimeException)不匹配
- 数据库表不支持事务
- spring的传播行为导致事务失效,有些传播行为不支持事务
三、分块(视频)文件上传,下载,合并
视频上传流程图
需要注意的点
一、合并视频上传成功后,要把分块文件和合并文件删除
二、上传分块前要检查分块是否存在,检查方式是通过字符串拼接找到分块的位置,查看是
否存在
三、合并文件前要下载所有分块文件到临时文件存放点,然后将分块文件内容写入合并文件
中