接下来是媒资管理服务,目前为止共三个微服务:内容管理、系统管理、媒资管理:
如此,前端得知道每个微服务实例的地址和端口, 且这样写死IP, 维护也不方便:
如此 , 请求统一到网关,有由网关路由到不同的微服务上, 网关在这儿有点像400电话, 根据不同的需求转接电话到不同的业务员 . 这样, 前端代码中只需要写接口的相对路径:
2.1 认识Nacos
网关想请求路由, 就必须知道每个微服务实例的地址,项目使用Nacos作用服务发现中心和配置中心
:
流程如下:
由此也可以看到Nacos的两个作用:
将自身信息注册登记至Nacos,网关从Nacos获取微服务列表
微服务的配置信息统一在Nacos配置
Nacos中的两个概念:
2.2 实现服务的发现
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>${spring-cloud-alibaba.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
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.
dataid有三部分组成, 如content-service-dev.yaml配置文件:
spring.application.name
的值spring.profiles.active
指定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
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-loggingartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-log4j2artifactId>
dependency>
dependencies>
#微服务配置
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
操作系统通过文件系统提供的接口取存取文件,用户则通过操作系统来访问磁盘上的文件:
常见的文件系统:FAT16/FAT32、NTFS、HFS、UFS、APFS、XFS、Ext4等. 看一下我Windos使用的文件系统:
直白的说: 一台计算机无法去进行海量文件的存储和响应海量用户的请求, 因此通过网络将若干计算机组织起来共同去完成这个任务
市面上分布式文件系统的相关产品有: NFS、GFS、HDFS:
NFS:
GFS:
HDFS:
云计算厂家:
在实际项目中, 根据场景来进行技术选型
认识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
MinIO与Java
MinIO提供多个语言版本SDK的支持, Java相关的文档https://docs.min.io/docs/java-client-quickstart-guide.html
<dependency>
<groupId>io.miniogroupId>
<artifactId>minioartifactId>
<version>8.4.3version>
dependency>
<dependency>
<groupId>com.squareup.okhttp3groupId>
<artifactId>okhttpartifactId>
<version>4.8.1version>
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());
}
}
}
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("下载成功");
}
整个上传过程有两点:
且在媒资管理数据库保存文件信息
数据模型
其中有字段md5值的字段, 用来判断文件是否已经上传过, 有相同文件,可不用重复上传, 以提高效率
Nacos配置
minio:
endpoint: http://localhost:9000
accessKey: minioadmin
secretKey: minioadmin
bucket:
files: mediafiles
videofiles: video
@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;
}
}
Content-Type: multipart/form-data;
{
"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
}
@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;
}
最后是向media_files表插入一条记录,使用media_files表生成的mapper即可
定义上传文件的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;
}
注意这里的几个点:
公共代码抽取成单独的方法
的习惯log.error("上传文件到minio出错,bucket:{},objectName:{},错误原因:{}",bucket,objectName,e.getMessage(),e);
的使用@Value("${minio.bucket.files}")
@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");
上面的代码中,给整个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】