文章目录
一 、媒资模块环境搭建
1、 网关gateway
接下来是媒资管理服务,目前为止共三个微服务:内容管理、系统管理、媒资管理:
如此,前端得知道每个微服务实例的地址和端口, 且这样写死IP, 维护也不方便:
- 身份认证和权限校验
- 服务路由和负载均衡
- 请求限流
如此 , 请求统一到网关,有由网关路由到不同的微服务上, 网关在这儿有点像400电话, 根据不同的需求转接电话到不同的业务员 . 这样, 前端代码中只需要写接口的相对路径:
2、Nacos
2.1 认识Nacos
网关想请求路由, 就必须知道每个微服务实例的地址,项目使用Nacos作用服务发现中心和配置中心
:
流程如下:
- 微服务启动, 将自己的信息注册到Nacos, Nacos记录各个微服务的地址
- 网关从Nacos读取服务列表, 包括服务名称和服务地址
- 请求到达网关, 网关将请求路由到具体的微服务
由此也可以看到Nacos的两个作用:
- 服务发现中心 : 微服务
将自身信息注册登记至Nacos,网关从Nacos获取微服务列表
- 服务配置中心 : 微服务众多, 配置信息复杂 ,
微服务的配置信息统一在Nacos配置
Nacos中的两个概念:
2.2 实现服务的发现
- 先在一级父工程的pom文件添加Spring Cloud Alibaba的依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
- 在需要注册的模块的pom文件中添加nacos的服务发现和配置文件依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
- 在需要注册的模块的bookstrap.yml中添加信息:
spring:
application:
name: de-system # 应用名称
cloud:
nacos:
server-addr: localhost:8848
discovery:
namespace: develop
group: DEFAULT_GROUP
2.3 配置中心
通过nacos来管理微服务的相关配置, 配置中有每个微服务独有的, 如:spring.application.name, 也有公共的信息, 如mysql、redis , nacos定位一个具体的配置文件通过:namespace、group、dataid.
- 通过namespace、group找到具体的环境和具体的项目
- 通过dataid找到具体的配置文件
dataid有三部分组成, 如content-service-dev.yaml配置文件:
- 第一部分是配置的应用名,即
spring.application.name
的值 - 第二部分是环境名, 由
spring.profiles.active
指定 - 第三部分即后缀名, nacos支持properties、
yaml
启动项目中传入spring.profiles.active的参数决定引用哪个环境的配置文件,例如:传入spring.profiles.active=dev表示使用dev环境的配置文件即content-service-dev.yaml
这里以content-service工程为例进行配置:
/spring.application.name等不在nacos中配置,而是要在工程的本地进行配置
/因为nacos客户端要根据此值确定配置文件名称
spring:
application:
name: content-service
cloud:
nacos:
server-addr: 192.168.101.65:8848
discovery: # 服务注册
namespace: dev
group: xuecheng-plus-project
config: # 配置文件相关
namespace: dev
group: xuecheng-plus-project
file-extension: yaml
refresh-enabled: true
#profiles默认为dev
profiles:
active: dev
nacos提供了shared-configs
可以引入公用配置 :
...
shared-configs:
- data-id: swagger-${spring.profiles.active}.yaml
group: xuecheng-plus-common
refresh: true
...
2.4 配置优先级
到此 , 微服务的配置统一在nacos进行配置,用到的配置文件有本地的配置文件 bootstrap.yaml和nacos上的配置文件 , 服务启动的时候, SpringBoot读取配置文件的顺序如下:
微服务引入配置文件的形式有:
- 以项目应用名方式引入
- 以扩展配置文件方式引入
- 以共享配置文件 方式引入
- 本地配置文件
当配置有冲突的时候, 优先级:项目应用名配置文件 > 扩展配置文件 > 共享配置文件 > 本地配置文件。
想要让本地配置文件优先级最高,可在Nacos中添加:
#配置本地优先
spring:
cloud:
config:
override-none: true
3、搭建gateway
<dependencies>
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--服务发现中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- 排除 Spring Boot 依赖的日志包冲突 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Spring Boot 集成 log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
</dependencies>
- 配置网关的bootstrap.yaml配置文件
#微服务配置
spring:
application:
name: gateway
profiles:
active: dev
cloud:
nacos:
# 服务注册地址
server-addr: localhost:8848
discovery:
namespace: dev
group: DEFAULT_GROUP
config:
# 配置中心地址
server-addr: localhost:8848
namespace: dev
group: DEFAULT_GROUP
# 配置文件格式
file-extension: yaml
refresh-enabled: true
# 共享配置
shared-configs:
- data-id: application-${spring.profiles.active}.yaml
group: DEFAULT_GROUP
refresh: true
在nacos上配置网关路由策略
server:
port: 63010 # 网关端口
spring:
cloud:
gateway:
# filter:
# strip-prefix:
# enabled: true
routes: # 网关路由配置
- id: content-api # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://content-api # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/content/** # 这个是按照路径匹配,只要以/content/开头就符合要求
# filters:
# - StripPrefix=1
- id: system-api
# uri: http://127.0.0.1:8081
uri: lb://system-api
predicates:
- Path=/system/**
# filters:
# - StripPrefix=1
- id: media-api
# uri: http://127.0.0.1:8081
uri: lb://media-api
predicates:
- Path=/media/**
# filters:
# - StripPrefix=1
# 不校验白名单
ignore:
whites:
- /auth/logout
二、分布式文件系统
2.1 文件系统
操作系统通过文件系统提供的接口取存取文件,用户则通过操作系统来访问磁盘上的文件:
常见的文件系统:FAT16/FAT32、NTFS、HFS、UFS、APFS、XFS、Ext4等. 看一下我Windos使用的文件系统:
2.2 分布式文件系统
直白的说: 一台计算机无法去进行海量文件的存储和响应海量用户的请求, 因此通过网络将若干计算机组织起来共同去完成这个任务
- 一台计算机的文件系统处理能力扩充到多台计算机同时处理
- 一台计算机挂了还有另外副本计算机提供数据
- 每台计算机可以放在不同的地域,这样用户就可以就近访问,提高访问速度
市面上分布式文件系统的相关产品有: NFS、GFS、HDFS:
NFS:
GFS:
- GFS采用主从结构,一个GFS集群由一个master和大量的chunkserver组成
- master存储了数据文件的元数据,如大小名称类型, 一个文件被分成了若干块存储在多个chunkserver中
- 用户从master中获取数据元信息,向chunkserver存储数据
HDFS:
- HDFS采用主从结构,一个HDFS集群由一个名称结点和若干数据结点组成
- 名称结点存储数据的元信息,一个完整的数据文件分成若干块存储在数据结点
- 客户端从名称结点获取数据的元信息及数据分块的信息,得到信息客户端即可从数据块来存取数据
云计算厂家:
在实际项目中, 根据场景来进行技术选型
2.3 MinIO
认识MinIO
-
MinIO适合于存储大容量非结构化的数据, 提供了 Java、Python、GO等多版本SDK支持
-
将数据分块冗余的分散存储在各各节点的磁盘上,所有的可用磁盘组成一个集合
-
当上传一个文件时通过纠删码算法对文件进行分块,文件本身分成4个数据块,还会生成4个校验块,数据块和校验块会分散的存储在这8块硬盘上
-
使用纠删码的好处是不超过一半数量(N/2)的硬盘损坏时,仍然可以恢复数据
使用
- 下载
链接: https://pan.baidu.com/s/16x9K1j1XPxCHKzqWRmh-mw?pwd=9527
提取码: 9527 复制这段内容后打开百度网盘手机App,操作更方便哦
- 启动
在exe文件的目录下打开DOS窗口:
minio.exe server xx xx xx
- 登录
http://localhost:9000进行登录,账号和密码为:minioadmin/minioadmin
- 创建桶bucket, 相当于存储文件的目录,可以创建若干的桶
- 点击upload上传文件, 观察分块存储的效果
MinIO与Java
MinIO提供多个语言版本SDK的支持, Java相关的文档https://docs.min.io/docs/java-client-quickstart-guide.html
- 添加Maven依赖
<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>
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上传文件
public class MinioTest {
static MinioClient minioClient =
MinioClient.builder()
.endpoint("http://192.168.101.65:9000")
.credentials("minioadmin", "minioadmin")
.build();
//上传文件
@Test
public void upload() {
try {
UploadObjectArgs testbucket = UploadObjectArgs.builder()
.bucket("testbucket")
//这样是上传在根目录
//.object("test001.mp4")
.object("001/test001.mp4")//指定子目录
.filename("D:\\develop\\upload\\1mp4.temp")
.contentType("video/mp4")//默认根据扩展名确定文件内容类型,也可以指定
.build();
minioClient.uploadObject(testbucket);
System.out.println("上传成功");
} catch (Exception e) {
e.printStackTrace();
System.out.println("上传失败");
}
}
}
其中contentType可以通过com.j256.simplemagic.ContentType枚举类查看常用的mimeType(媒体类型),下面通过扩展名得到mimeType:
@Test
public void upload() {
//根据扩展名取出mimeType
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(".mp4");
String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//通用mimeType,字节流
if(extensionMatch!=null){
mimeType = extensionMatch.getMimeType();
}
try {
UploadObjectArgs testbucket = UploadObjectArgs.builder()
.bucket("testbucket")
//.object("test001.mp4")
.object("001/test001.mp4")//添加子目录
.filename("D:\\develop\\upload\\1mp4.temp")
.contentType(mimeType)//直接传入获取到的值
.build();
minioClient.uploadObject(testbucket);
System.out.println("上传成功");
} catch (Exception e) {
e.printStackTrace();
System.out.println("上传失败");
}
}
- 测试删除文件
@Test
public void delete(){
try {
minioClient.removeObject(
RemoveObjectArgs.builder().bucket("testbucket").object("001/test001.mp4").build());
System.out.println("删除成功");
} catch (Exception e) {
e.printStackTrace();
System.out.println("删除失败");
}
}
- 测试下载
@Test
public void getFile() {
GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket("testbucket").object("test001.mp4").build();
try(
FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
//设置本地位置,创建输出流
FileOutputStream outputStream = new FileOutputStream(new File("D:\\develop\\upload\\1_2.mp4"));
) {
//流拷贝
IOUtils.copy(inputStream,outputStream);
} catch (Exception e) {
e.printStackTrace();
}
}
校验文件的完整性,对文件计算出md5值,比较原始文件的md5和目标文件的md5 ,注意这里, 别用上面代码中的inputStream, 它在这里受网络影响, 可能有偏差
//校验文件的完整性对文件的内容进行md5
FileInputStream fileInputStream1 = new FileInputStream(new File("D:\\develop\\upload\\1.mp4"));
String source_md5 = DigestUtils.md5Hex(fileInputStream1);
FileInputStream fileInputStream = new FileInputStream(new File("D:\\develop\\upload\\1a.mp4"));
String local_md5 = DigestUtils.md5Hex(fileInputStream);
if(source_md5.equals(local_md5)){
System.out.println("下载成功");
}
四、上传图片
4.1 需求分析
整个上传过程有两点:
- 点击上传课程图片, 前端请求媒资管理服务将文件上传至分布式文件系统,并
且在媒资管理数据库保存文件信息
- 上传图片成功保存图片地址到课程基本信息表中
4.2 数据模型与环境配置
数据模型
其中有字段md5值的字段, 用来判断文件是否已经上传过, 有相同文件,可不用重复上传, 以提高效率
Nacos配置
- 在nacos配置中minio的相关信息,进入media-service-dev.yaml:
minio:
endpoint: http://localhost:9000
accessKey: minioadmin
secretKey: minioadmin
bucket:
files: mediafiles
videofiles: video
- 在media-service工程编写minio的配置类:
@Configuration
public class MinioConfig {
//加@Configuration后从nacos中拿信息
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.accessKey}")
private String accessKey;
@Value("${minio.secretKey}")
private String secretKey;
//创建minioClient的bean,方便以后注入
@Bean
public MinioClient minioClient() {
MinioClient minioClient =
MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
return minioClient;
}
}
4.3 接口定义
- 请求地址: /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
}
- 定义vo类
@Data
public class UploadFileResultVo extends MediaFiles {
}
//注意这里虽然前端要求返回的字段和表对应的po一样
//但别直接用po,万一以后前端要求少返回一个字段, 你又不能动po
- 定义接口
@ApiOperation("上传文件")
@RequestMapping(value = "/upload/coursefile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public UploadFileResultDto upload(@RequestPart("filedata") MultipartFile upload) throws IOException {
return null;
}
4.4 开发mapper层
最后是向media_files表插入一条记录,使用media_files表生成的mapper即可
4.5 开发service层
定义上传文件的Dto,即从前端能拿到的数据:
/**
* @description 上传普通文件请求参数Dto
*/
@Data
public class UploadFileParamsDto {
/** 文件名称*/
private String filename;
/** 文件类型(文档,音频,视频)*/
private String fileType;
/**文件大小*/
private Long fileSize;
/**标签*/
private String tags;
/**上传人*/
private String username;
/**备注*/
private String remark;
}
定义接口中的方法:
/**
* 上传文件
* @param companyId 机构id
* @param uploadFileParamsDto 上传文件信息
* @param localFilePath 文件磁盘路径
* @return 文件信息
*/
public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath);
写service方法的实现类:
@Autowired
MinioClient minioClient;
@Autowired
MediaFilesMapper mediaFilesMapper;
//普通文件桶
@Value("${minio.bucket.files}")
private String bucket_Files;
//获取文件默认存储目录路径 年/月/日
private String getDefaultFolderPath() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String folder = sdf.format(new Date()).replace("-", "/")+"/";
return folder;
}
//获取文件的md5
private String getFileMd5(File file) {
try (FileInputStream fileInputStream = new FileInputStream(file)) {
String fileMd5 = DigestUtils.md5Hex(fileInputStream);
return fileMd5;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private String getMimeType(String extension){
if(extension==null)
extension = "";
//根据扩展名取出mimeType
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
//通用mimeType,字节流
String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
if(extensionMatch!=null){
mimeType = extensionMatch.getMimeType();
}
return mimeType;
}
/**
* @description 将文件写入minIO
* @param localFilePath 文件地址
* @param bucket 桶
* @param objectName 对象名称
* @return void
* @author Mr.M
* @date 2022/10/12 21:22
*/
public boolean addMediaFilesToMinIO(String localFilePath,String mimeType,String bucket, String objectName) {
try {
UploadObjectArgs testbucket = UploadObjectArgs.builder()
.bucket(bucket)
.object(objectName)
.filename(localFilePath)
.contentType(mimeType)
.build();
minioClient.uploadObject(testbucket);
log.debug("上传文件到minio成功,bucket:{},objectName:{}",bucket,objectName);
System.out.println("上传成功");
return true;
} catch (Exception e) {
e.printStackTrace();
log.error("上传文件到minio出错,bucket:{},objectName:{},错误原因:{}",bucket,objectName,e.getMessage(),e);
XueChengPlusException.cast("上传文件到文件系统失败");
}
return false;
}
/**
* @description 将文件信息添加到文件表
* @param companyId 机构id
* @param fileMd5 文件md5值
* @param uploadFileParamsDto 上传文件的信息
* @param bucket 桶
* @param objectName 对象名称
* @return com.xuecheng.media.model.po.MediaFiles
*/
@Transactional
public MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName){
//从数据库查询文件
MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
//无重复md5, 开始插入
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;
}
@Transactional
@Override
public UploadFileResultVo uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath) {
File file = new File(localFilePath);
if (!file.exists()) {
XueChengPlusException.cast("文件不存在");
}
//文件名称
String filename = uploadFileParamsDto.getFilename();
//文件扩展名
String extension = filename.substring(filename.lastIndexOf("."));
//文件mimeType
String mimeType = getMimeType(extension);
//文件的md5值
String fileMd5 = getFileMd5(file);
//文件的默认目录
String defaultFolderPath = getDefaultFolderPath();
//存储到minio中的对象名(带目录)
String objectName = defaultFolderPath + fileMd5 + exension;
//将文件上传到minio
boolean b = addMediaFilesToMinIO(localFilePath, mimeType, bucket_files, objectName);
//文件大小
uploadFileParamsDto.setFileSize(file.length());
//将文件信息存储到数据库
MediaFiles mediaFiles = addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_files, objectName);
//准备返回数据
UploadFileResultVo uploadFileResultVo = new UploadFileResultVo();
BeanUtils.copyProperties(mediaFiles, uploadFileResultVo);
return uploadFileResultVo;
}
注意这里的几个点:
- 对于
公共代码抽取成单独的方法
的习惯 - @Slf4j注解和
log.error("上传文件到minio出错,bucket:{},objectName:{},错误原因:{}",bucket,objectName,e.getMessage(),e);
的使用 - 使用日期当作各级目录, 使用md5值当作文件名
- 使用@Value注解从Nacos配置文件中读值
@Value("${minio.bucket.files}")
4.6 完善controller层
@ApiOperation("上传文件")
@RequestMapping(value = "/upload/coursefile",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@ResponseBody
public UploadFileResultVo upload(@RequestPart("filedata") MultipartFile upload,@RequestParam(value = "folder",required=false) String folder,@RequestParam(value = "objectName",required=false) String objectName) throws IOException {
//机构id暂时写死,还没加登录模块
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;
}
- 注意这里获取临时文件后拷贝上传文件到临时文件, 来获得文件路径
File tempFile = File.createTempFile("minio", "temp");
4.7 Service层事务优化
上面的代码中,给整个uploadFile文件开启事务(包括文件上传和文件信息入库),即调用uploadFile方法前会开启数据库事务,如果上传文件时间很长,此时数据库事务的持续时间就会很长,数据库链接释放慢,最后导致数据库链接不够用。优化为:只在addMediaFilesToDb方法添加事务控制即可。
@Transactional
public MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName){
...
int insert = mediaFilesMapper.insert(mediaFiles);
int a = 1/0; //模拟发生异常
....
}
测试发现,事务控制失败。失败的原因是一个非事务方法调同类一个事务方法,事务无法控制。
之前在uploadFile方法上添加@Transactional注解时:代理对象MediaFileServiceProxy会在方法执行前开启事务:
而@Transactional注解改到addMediaFilesToDb上后,controller再调uploadFile方法,则代理对象不再进行事务控制。
判断该方法是否可以事务控制必须保证是通过代理对象调用此方法,且此方法上添加了@Transactional注解。现在事务注解在addMediaFilesToDb方法,在调用这个方法的地方打断点,debug看到调用这个同类中方法的对象(this)并不是代理对象:
根据“事务控制必须保证是通过代理对象调用此方法,且此方法上添加了@Transactional注解”,在MediaFileService的实现类中注入MediaFileService的代理对象(自己注入自己):
@Autowired
MediaFileService currentProxy;
原uploadFile方法中对事务方法addMediaFilesToDb的调用改为:
.....
//写入文件表
MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_files, objectName);
....
关于非事务方法调用事务方法的另一种解决思路:【方法2】
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/146065.html