minio的使用
MinIO提供多个语言版本SDK的支持,下边找到java版本的文档:
地址:https://docs.min.io/docs/java-client-quickstart-guide.html
最低需求Java 1.8或更高版本:
maven依赖如下:
minio使用okhttp进行消息传递:
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.4.3</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.8.1</version>
</dependency>
在media-service工程添加此依赖。
参数说明:
需要三个参数才能连接到minio服务。
参数 | 说明 |
Endpoint | 对象存储服务的URL |
Access Key | Access key就像用户ID,可以唯一标识你的账户。 |
Secret Key | Secret key是你账户的密码。 |
官方的示例代码如下:
import io.minio.BucketExistsArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.UploadObjectArgs;
import io.minio.errors.MinioException;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class FileUploader {
public static void main(String[] args)throws IOException, NoSuchAlgorithmException, InvalidKeyException {
try {
// Create a minioClient with the MinIO server playground, its access key and secret key.
MinioClient minioClient =
MinioClient.builder()
.endpoint("https://play.min.io")
.credentials("Q3AM3UQ867SPQQA43P2F", "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG")
.build();
// Make 'asiatrip' bucket if not exist.
boolean found =
minioClient.bucketExists(BucketExistsArgs.builder().bucket("asiatrip").build());
if (!found) {
// Make a new bucket called 'asiatrip'.
minioClient.makeBucket(MakeBucketArgs.builder().bucket("asiatrip").build());
} else {
System.out.println("Bucket 'asiatrip' already exists.");
}
// Upload '/home/user/Photos/asiaphotos.zip' as object name 'asiaphotos-2015.zip' to bucket
// 'asiatrip'.
minioClient.uploadObject(
UploadObjectArgs.builder()
.bucket("asiatrip")
.object("asiaphotos-2015.zip")
.filename("/home/user/Photos/asiaphotos.zip")
.build());
System.out.println(
"'/home/user/Photos/asiaphotos.zip' is successfully uploaded as "
+ "object 'asiaphotos-2015.zip' to bucket 'asiatrip'.");
} catch (MinioException e) {
System.out.println("Error occurred: " + e);
System.out.println("HTTP trace: " + e.httpTrace());
}
}
}
测试minio
package com.xuecheng.media;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import io.minio.GetObjectArgs;
import io.minio.MinioClient;
import io.minio.RemoveObjectArgs;
import io.minio.UploadObjectArgs;
import org.apache.commons.compress.utils.IOUtils;
import org.springframework.http.MediaType;
import org.springframework.util.DigestUtils;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilterInputStream;
public class Test {
//准备minio客户端
MinioClient minioClient =
MinioClient.builder()
.endpoint("http://192.168.101.65:9000")
.credentials("minioadmin", "minioadmin")
.build();
//上传文件
@org.junit.jupiter.api.Test
public void upload() throws Exception{
//根据扩展名取出mimeType
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(".mp4");
String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//通用mimeType,字节流
if(extensionMatch!=null){
mimeType = extensionMatch.getMimeType();
}
//准备数据
UploadObjectArgs testbucket = UploadObjectArgs.builder()
.bucket("testbucket")
.object("test/01/1a.mp4")//文件要存放的路径和文件名
.filename("D:\\BaiduNetdiskDownload\\学成在线\\学成在线项目—资料\\day05 媒资管理 Nacos Gateway MinIO\\资料\\1.mp4")
.contentType(mimeType)//指定融媒文件的类型
.build();
minioClient.uploadObject(testbucket);
}
//删除文件
@org.junit.jupiter.api.Test
public void delete() throws Exception{
//准备数据
RemoveObjectArgs testbucket = RemoveObjectArgs.builder()
.bucket("testbucket")
.object("test/01/1a.mp4")//文件要存放的路径和文件名
.build();
minioClient.removeObject(testbucket);
}
//查询(下载)文件
@org.junit.jupiter.api.Test
public void get() throws Exception{
GetObjectArgs testbucket = GetObjectArgs.builder()
.bucket("testbucket")
.object("test/01/1a.mp4")
.build();
//读取数据获取到输入流
FilterInputStream inputStream = minioClient.getObject(testbucket);
//创建输出流将数据文件写出
FileOutputStream outputStream = new FileOutputStream("D:\\BaiduNetdiskDownload\\学成在线\\学成在线项目—资料\\day05 媒资管理 Nacos Gateway MinIO\\资料\\12.mp4");
//IOUtils将输入流复制到输出流,写入到磁盘
IOUtils.copy(inputStream,outputStream);
//使用dm5进行校验
//读取原本文件的输入流
FileInputStream fileInputStream1 = new FileInputStream("D:\\BaiduNetdiskDownload\\学成在线\\学成在线项目—资料\\day05 媒资管理 Nacos Gateway MinIO\\资料\\1.mp4");
//读取下载的文件的输入流
FileInputStream fileInputStream2 = new FileInputStream("D:\\BaiduNetdiskDownload\\学成在线\\学成在线项目—资料\\day05 媒资管理 Nacos Gateway MinIO\\资料\\12.mp4");
String s1 = DigestUtils.md5DigestAsHex(fileInputStream1);
String s2 = DigestUtils.md5DigestAsHex(fileInputStream2);
if (s1.equalsIgnoreCase(s2)){
System.out.println("下载完成");
}
}
}
根基扩展名获取文件的融媒类型的依赖:
<!-- 根据扩展名取mimetype-->
<dependency>
<groupId>com.j256.simplemagic</groupId>
<artifactId>simplemagic</artifactId>
<version>1.17</version>
</dependency>
上传图片
初始化minio上传
环境准备
- 在minio配置bucket,bucket名称为:mediafiles,并设置bucket的权限为公开。
- 在nacos配置中minio的相关信息,进入media-service-dev.yaml:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.101.65:3306/xcplus_media?serverTimezone=UTC&userUnicode=true&useSSL=false&
username: root
password: mysql
cloud:
config:
override-none: true
minio:
endpoint: http://192.168.101.65:9000
accessKey: minioadmin
secretKey: minioadmin
bucket:
files: mediafiles
videofiles: video
xxl:
job:
admin:
addresses: http://192.168.101.65:8088/xxl-job-admin
executor:
appname: media-process-service
address:
ip:
port: 9999
logpath: /data/applogs/xxl-job/jobhandler
logretentiondays: 30
accessToken: default_token
videoprocess:
ffmpegpath: D:/soft/ffmpeg/ffmpeg.exe
由于minio客户端需要配置相关信息,为了避免死代码将数据配置在配置文件中:
minio:
endpoint: http://192.168.101.65:9000
accessKey: minioadmin
secretKey: minioadmin
bucket:
files: mediafiles
videofiles: video
由于写在配置文件中需要使用配置类读取配置文件,同时要建立一个客户端的Bean容器,方便调用:
package com.xuecheng.media.config;
import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @description minio配置
* @author Mr.M
* @date 2022/9/12 19:32
* @version 1.0
*/
@Configuration
public class MinioConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.accessKey}")
private String accessKey;
@Value("${minio.secretKey}")
private String secretKey;
@Bean
public MinioClient minioClient() {
MinioClient minioClient =
MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
return minioClient;
}
}
接口定义
根据需求分析,下边进行接口定义,此接口定义为一个通用的上传文件接口,可以上传图片或其它文件。
首先分析接口:
请求地址:/media/upload/coursefile
请求内容:Content-Type: multipart/form-data;
form-data; name="filedata"; filename="具体的文件名称", 此为和前端协商的文件名称,用于保存
响应参数:文件信息,如下
{
"id": "a16da7a132559daf9e1193166b3e7f52",
"companyId": 1232141425,
"companyName": null,
"filename": "1.jpg",
"fileType": "001001",
"tags": "",
"bucket": "/testbucket/2022/09/12/a16da7a132559daf9e1193166b3e7f52.jpg",
"fileId": "a16da7a132559daf9e1193166b3e7f52",
"url": "/testbucket/2022/09/12/a16da7a132559daf9e1193166b3e7f52.jpg",
"timelength": null,
"username": null,
"createDate": "2022-09-12T21:57:18",
"changeDate": null,
"status": "1",
"remark": "",
"auditStatus": null,
"auditMind": null,
"fileSize": 248329
}
为了方便以后对返回数据的修改,定义一个dto用于返回数据
package com.xuecheng.media.model.dto;
import com.xuecheng.media.model.po.MediaFiles;
import lombok.Data;
import lombok.ToString;
/**
* @description 上传普通文件成功响应结果
* @author Mr.M
* @date 2022/9/12 18:49
* @version 1.0
*/
@Data
public class UploadFileResultDto extends MediaFiles {
}
定义接口如下:
@ApiOperation("上传文件")
@RequestMapping(value = "/upload/coursefile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public UploadFileResultDto upload(@RequestPart("filedata") MultipartFile upload) throws IOException {
return null;
}
代码实现
注入客户端,读取配置文件中的桶名称:
//minio客户端
@Autowired
MinioClient minioClient;
//读取配置文件数据
@Value("${minio.bucket.files}")
private String bucketFiles;
代码实现:(service代码)
/**
* 上传图片
* @param companyId 机构id
* @param uploadFileParamsDto 上传文件信息
* @param localFilePath 文件磁盘路径
* @return
*/
@Transactional
@Override
public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath) {
File file = new File(localFilePath);
if (!file.exists()) {
XueChengPlusException.cast("文件不存在");
}
//上传图片到minio
//获取文件后缀名
String filename = uploadFileParamsDto.getFilename();//获取文件名
String extension = filename.substring(filename.lastIndexOf("."));
//获取文件扩展名
//根据扩展名取出mimeType
String mimeType = getMimeType(extension);
//拼接文件路径
//bucket/日期数据/文件md5.文件后缀名
String datePath = getDatePath();
//获取文件MD5值
String fileMd5 = getFileMd5(localFilePath);
//拼装文件路径
// String objectName = bucketFiles + "/" + datePath + fileMd5 + extension;
String objectName =datePath + fileMd5 + extension;
//准备数据
Boolean result = updataFile(mimeType, bucketFiles, objectName, localFilePath);
if (!result){
throw new XueChengPlusException("上传文件失败");
}
//保存图片数据到数据库
//文件大小
uploadFileParamsDto.setFileSize(file.length());
//将文件信息存储到数据库
MediaFiles mediaFiles = addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucketFiles, objectName);
//准备返回数据
UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);
return uploadFileResultDto;
}
/**
* @description 将文件信息添加到文件表
* @param companyId 机构id
* @param fileMd5 文件md5值
* @param uploadFileParamsDto 上传文件的信息
* @param bucket 桶
* @param objectName 对象名称
* @return com.xuecheng.media.model.po.MediaFiles
* @author Mr.M
* @date 2022/10/12 21:22
*/
@Transactional
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.setFilePath(objectName);
mediaFiles.setCreateDate(LocalDateTime.now());
mediaFiles.setAuditStatus("002003");
mediaFiles.setStatus("1");
//保存文件信息到文件表
int insert = mediaFilesMapper.insert(mediaFiles);
if (insert < 0) {
log.error("保存文件信息到数据库失败,{}",mediaFiles.toString());
XueChengPlusException.cast("保存文件信息失败");
}
log.debug("保存文件信息到数据库成功,{}",mediaFiles.toString());
}
return mediaFiles;
}
//获取文件的md5值
private String getFileMd5(String localFilePath) {
String fileMd5;
try {
//文件的路径,获取文件的md5值
fileMd5 = DigestUtils.md5DigestAsHex(new FileInputStream(localFilePath));
} catch (Exception e) {
log.error("获取文件md5值失败:{},具体错误信息:{}",e.getMessage(),e);
throw new RuntimeException(e);
}
return fileMd5;
}
//获取日期分级
private String getDatePath() {
//获取时间格式化器
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
//将当前日期按照格式化器进行转换
String dateString = sdf.format(new Date());
//将日期字符串进行转换 - 转换为 /
return dateString.replace("-","/")+"/";
}
//上传文件
private boolean updataFile(String mimeType,String bucket,String objectName,String localFilePath) {
try {
UploadObjectArgs testbucket = UploadObjectArgs.builder()
.bucket(bucket)
.object(objectName)//文件要存放的路径和文件名
.filename(localFilePath)
.contentType(mimeType)//指定融媒文件的类型
.build();
minioClient.uploadObject(testbucket);
return true;
}catch (Exception e) {
log.error("上传文件失败",e);
return false;
}
}
private static String getMimeType(String fileName_lest) {
//判断是否为空
if (fileName_lest==null){
fileName_lest = "";
}
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(fileName_lest);
String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//通用mimeType,字节流
if(extensionMatch!=null){
mimeType = extensionMatch.getMimeType();
}
return mimeType;
}
使用dto存储相关数据:
package com.xuecheng.media.model.dto;
import com.xuecheng.media.model.po.MediaFiles;
import lombok.Data;
import lombok.ToString;
/**
* @description 上传普通文件请求参数
* @author Mr.M
* @date 2022/9/12 18:49
* @version 1.0
*/
@Data
public class UploadFileParamsDto {
/**
* 文件名称
*/
private String filename;
/**
* 文件类型(文档,音频,视频)
*/
private String fileType;
/**
* 文件大小
*/
private Long fileSize;
/**
* 标签
*/
private String tags;
/**
* 上传人
*/
private String username;
/**
* 备注
*/
private String remark;
}
修改接口代码:
@ApiOperation("上传文件")
@RequestMapping(value = "/upload/coursefile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public UploadFileResultDto upload(@RequestPart("filedata") MultipartFile filedata) throws IOException {
//将前端传来的文件暂存到硬盘,用来获取文件路径
Long companyId = 1232141425L;
UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto();
//文件大小
uploadFileParamsDto.setFileSize(filedata.getSize());
//图片
uploadFileParamsDto.setFileType("001001");
//文件名称
uploadFileParamsDto.setFilename(filedata.getOriginalFilename());//文件名称
//文件大小
long fileSize = filedata.getSize();
uploadFileParamsDto.setFileSize(fileSize);
//创建临时文件
File tempFile = File.createTempFile("minio", "temp");
//上传的文件拷贝到临时文件
filedata.transferTo(tempFile);
//文件路径
String absolutePath = tempFile.getAbsolutePath();
//上传文件
UploadFileResultDto uploadFileResultDto = mediaFileService.uploadFile(companyId, uploadFileParamsDto, absolutePath);
return uploadFileResultDto;
}
Http测试
### 上传文件
POST {{media_host}}/media/upload/coursefile
Content-Type: multipart/form-data; boundary=WebAppBoundary
--WebAppBoundary
Content-Disposition: form-data; name="filedata"; filename="1.png"
Content-Type: application/octet-stream
< d:/www/1.png
改进事物
由于事物注解添加在uploadFile方法上,由于需要进行网络上传,可能会占用大量时间造成数据库线程耗尽,因此修改到提交数据库方法上,但是测试得到事物失败。
我们人为在int insert = mediaFilesMapper.insert(mediaFiles);下边添加一个异常代码int a=1/0;
测试是否事务控制。很遗憾,事务控制失败。
方法上已经添加了@Transactional注解为什么该方法不能被事务控制呢?
如果是在uploadFile方法上添加@Transactional注解就可以控制事务,去掉则不行。
现在的问题其实是一个非事务方法调同类一个事务方法,事务无法控制,这是为什么?
下边分析原因:
如果在uploadFile方法上添加@Transactional注解,代理对象执行此方法前会开启事务,如下图:
如果在uploadFile方法上没有@Transactional注解,代理对象执行此方法前不进行事务控制,如下图:
所以判断该方法是否可以事务控制必须保证是通过代理对象调用此方法,且此方法上添加了@Transactional注解。
现在在addMediaFilesToDb方法上添加@Transactional注解,也不会进行事务控制是因为并不是通过代理对象执行的addMediaFilesToDb方法。为了判断在uploadFile方法中去调用addMediaFilesToDb方法是否是通过代理对象去调用,我们可以打断点跟踪。
我们发现在uploadFile方法中去调用addMediaFilesToDb方法不是通过代理对象去调用。
事物生效的条件是,注解和代理对象同时成立
如何解决呢?通过代理对象去调用addMediaFilesToDb方法即可解决。
在MediaFileService的实现类中注入MediaFileService的代理对象,如下:
@Autowired
MediaFileService currentProxy;
将addMediaFilesToDb方法提成接口。
/**
* @description 将文件信息添加到文件表
* @param companyId 机构id
* @param fileMd5 文件md5值
* @param uploadFileParamsDto 上传文件的信息
* @param bucket 桶
* @param objectName 对象名称
* @return com.xuecheng.media.model.po.MediaFiles
* @author Mr.M
* @date 2022/10/12 21:22
*/
public MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName);
调用addMediaFilesToDb方法的代码处改为如下:
.....
//写入文件表
MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_files, objectName);
....
再次测试事务是否可以正常控制。