1. 模块需求分析
1.1 模块介绍
媒资管理系统是每个在线教育平台所必须具备的,查阅百度百科对它的定义如下:
媒体资源管理(Media Asset Management,MAM)系统是建立在多媒体、网络、数据库和数字存储等先进技术基础上的一个对各种媒体及内容(如视/音频资料、文本文件、图表等)进行数字化存储、管理以及应用的总体解决方案,包括数字媒体的采集、编目、管理、传输和编码转换等所有环节。其主要是满足媒体资源拥有者收集、保存、查找、编辑、发布各种信息的要求,为媒体资源的使用者提供访问内容的便捷方法,实现对媒体资源的高效管理,大幅度提高媒体资源的价值。
每个教学机构都可以在媒资系统管理自己的教学资源,包括:视频、教案等文件。
目前媒资管理的主要管理对象是视频、图片、文档等,包括:媒资文件的查询、文件上传、视频处理等。
下图是课程编辑与发布的整体流程,通过下图可以看到媒资管理在整体流程的位置:
1.2 业务流程
1.2.1 上传图片
教学机构人员在课程信息编辑页面上传课程图片,课程图片统一记录在媒资管理系统。
下图是上传图片的界面:
1.2.2 上传视频
- 教学机构人员进入媒资管理列表查询自己上传的媒资文件。
点击“媒资管理”
进入媒资管理列表页面查询本机构上传的媒资文件。
- 教育机构用户在”媒资管理”页面中点击 “上传视频” 按钮。
点击“上传视频”打开上传页面
- 选择要上传的文件,自动执行文件上传,视频上传成功会自动处理。
1.2.3 处理视频
对需要转码处理的视频系统会自动对其处理,处理后生成视频的URL。
处理视频没有用户界面,完全是后台自动执行。
1.2.4 审核视频
审核媒资包括程序自动审核和人工审核,程序可以通过鉴黄接口(https://www.aliyun.com/product/lvwang?spm=5176.19720258.J_3207526240.51.e93976f4rSq796)审核视频,对有异议的视频由人工进行审核。
- 运营用户登入运营平台并进入媒资管理页面,查找待审核媒资
- 点击列表中媒资名称链接,可预览该媒资,若是视频,则播放视频。
- 点击列表中某媒资后的”审核” 按钮,既完成媒资的审批过程。
点击“审核”,选择审核结果,输入审核意见。
1.2.5 绑定媒资
课程计划创建好后需要绑定媒资文件,比如:如果课程计划绑定了视频文件,进入课程在线学习界面后点课程计划名称则在线播放视频。如下图:
如何将课程计划绑定媒资呢?
- 教育机构用户进入课程管理页面并编辑某一个课程,在”课程大纲”标签页的某一小节后可点击”添加视频“。
- 弹出添加视频对话框,可通过视频关键字搜索已审核通过的视频媒资。
- 选择视频媒资,点击提交按钮,完成课程计划绑定媒资流程。
课程计划关联视频后如下图:
2. 搭建模块环境
2.1 架构的问题分析
当前要开发的是媒资管理服务,目前为止共三个微服务:内容管理、系统管理、媒资管理,如下图:
后期还会添加更多的微服务,当前这种由前端直接请求微服务的方式存在弊端:
如果在前端对每个请求地址都配置绝对路径,非常不利于系统维护,比如下边代码中请求系统管理服务的地址使用的是localhost
当系统上线后这里需要改成公网的域名,如果这种地址非常多则非常麻烦。
基于这个问题可以采用网关来解决,如下图:
这样在前端的代码中只需要指定每个接口的相对路径,如下所示:
在前端代码的一个固定的地方在接口地址前统一加网关的地址,每个请求统一到网关,由网关将请求转发到具体的微服务。
为什么所有的请求先到网关呢?
有了网关就可以对请求进行路由,路由到具体的微服务,减少外界对接微服务的成本,比如:400电话,路由的试可以根据请求路径进行路由、根据host地址进行路由等, 当微服务有多个实例时可以通过负载均衡算法进行路由,如下:
另外,网关还可以实现权限控制、限流等功能。
项目采用Spring Cloud Gateway作为网关,网关在请求路由时需要知道每个微服务实例的地址,项目使用Nacos作用服务发现中心和配置中心,整体的架构图如下:
流程如下:
微服务启动,将自己注册到Nacos,Nacos记录了各微服务实例的地址。
网关从Nacos读取服务列表,包括服务名称、服务地址等。
请求到达网关,网关将请求路由到具体的微服务。
要使用网关首先搭建Nacos,Nacos有两个作用:
服务发现中心。
- 微服务将自身注册至Nacos,网关从Nacos获取微服务列表。
配置中心。
- 微服务众多,它们的配置信息也非常复杂,为了提供系统的可维护性,微服务的配置信息统一在Nacos配置。
2.2 搭建 Nacos
2.2.1 服务发现中心
Spring Cloud :一套规范
- Spring Cloud alibaba: nacos服务注册中心,配置中心
根据上节讲解的网关的架构图,要使用网关首先搭建Nacos。
首先搭建Nacos服务发现中心。
在搭建Nacos服务发现中心之前需要搞清楚两个概念:namespace和group
namespace:用于区分环境、比如:开发环境、测试环境、生产环境。
group:用于区分项目,比如:xuecheng-plus项目、xuecheng2.0项目
首先在nacos配置namespace:
登录成功,点击左侧菜单“命名空间”进入命名空间管理界面,
首先完成各服务注册到Naocs,下边将内容管理服务注册到nacos中。
1) 在xuecheng-plus-parent中添加依赖管理
1 2 3 4 5 6 7
| <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>
|
- 在内容管理模块的接口工程、系统管理模块的接口工程中添加如下依赖
1 2 3 4
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
|
- 配置nacos的地址
1 2 3 4 5 6 7 8 9 10
| spring: application: name: system-service cloud: nacos: server-addr: 192.168.101.65:8848 discovery: namespace: dev group: xuecheng-plus-project
|
在内容管理的接口工程的配置文件中配置如下信息:
1 2 3 4 5 6 7 8 9
| spring: application: name: content-api cloud: nacos: server-addr: 192.168.101.65:8848 discovery: namespace: dev group: xuecheng-plus-project
|
- 重启内容管理服务、系统管理服务。
待微服务启动成功,进入Nacos服务查看服务列表
在 “开发环境” 命名空间下有两个服务这说明内容管理微服务和系统管理微服务在Nacos注册成功。
点击其它一个微服务的“详情”
2.2.2 配置中心
2.2.2.1 配置三要素
搭建完成Nacos服务发现中心,下边搭建Nacos为配置中心,其目的就是通过Nacos去管理项目的所有配置。
1️⃣先将项目中的配置文件分分类:
每个项目特有的配置
项目所公用的配置
通过namespace、group找到具体的环境和具体的项目。
通过dataid找到具体的配置文件,dataid有三部分组成
比如:content-service-dev.yaml配置文件 由(content-service)-(dev). (yaml)三部分组成
content-service:第一部分,它是在application.yaml中配置的应用名,即spring.application.name的值。
dev:第二部分,它是环境名,通过spring.profiles.active指定,
- Yaml: 第三部分,它是配置文件 的后缀,目前nacos支持properties、yaml等格式类型,本项目选择yaml格式类型。
2️⃣所以,如果我们要配置content-service工程的配置文件:
- 在开发环境中配置content-service-dev.yaml
- 在测试环境中配置content-service-test.yaml
- 在生产环境中配置content-service-prod.yaml
我们启动项目中传入spring.profiles.active的参数决定引用哪个环境的配置文件,例如:传入spring.profiles.active=dev表示使用dev环境的配置文件即content-service-dev.yaml。
2.2.2.2 配置 content-service
1️⃣下边以开发环境为例对content-service工程的配置文件进行配置,进入nacos,进入开发环境。
- 点击加号,添加一个配置
- 输入data id、group以及配置文件内容。
2️⃣为什么没在nacos中配置下边的内容 ?
1 2 3
| spring: application: name: content-service
|
- 因为刚才说了dataid第一部分就是spring.application.name,nacos 客户端要根据此值确定配置文件 名称,
- 所以spring.application.name不在nacos中配置,而是要在工程的本地进行配置。
3️⃣在content-service工程的test/resources 中添加bootstrap.yaml,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| spring: application: name: content-service cloud: nacos: server-addr: 192.168.101.65:8848 discovery: namespace: ty_xc_dev group: xuecheng-plus-project config: namespace: ty_xc_dev group: xuecheng-plus-project file-extension: yaml refresh-enabled: true
profiles: active: dev
|
4️⃣在内容管理模块的接口工程和service工程配置依赖:
1 2 3 4
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
|
配置完成,运行content-service工程 的单元测试文件,能否正常测试,跟踪单元测试方法可以正常读取数据库的数据,说明从nacos读取配置信息正常。
通过运行观察控制台打印出下边的信息,NacosRestTemplate.java通过Post方式与nacos服务端交互读取配置信息。
2.2.2.3 配置 content-api
以相同的方法配置content-api工程的配置文件,在nacos中的开发环境中配置content-api-dev.yaml,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| server: servlet: context-path: /content port: 63040
logging: config: classpath:log4j2-dev.xml
swagger: title: "学成在线内容管理系统" description: "内容系统管理系统对课程相关信息进行业务管理数据" base-package: com.xuecheng.content enabled: true version: 1.0.0
|
在content-api工程 的本地配置bootstrap.yaml,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| spring: application: name: content-api cloud: nacos: server-addr: 192.168.101.65:8848 discovery: namespace: ty_xc_dev group: xuecheng-plus-project config: namespace: ty_xc_dev group: xuecheng-plus-project file-extension: yaml refresh-enabled: true extension-configs: - data-id: content-service-${spring.profiles.active}.yaml group: xuecheng-plus-project refresh: true profiles: active: dev
|
注意:因为api接口工程依赖了service工程 的jar,所以这里使用extension-configs扩展配置文件 的方式引用service工程所用到的配置文件。
1 2 3 4
| extension-configs: - data-id: content-service-${spring.profiles.active}.yaml group: xuecheng-plus-project refresh: true
|
如果添加多个扩展文件,继续在下添加即可,如下:
1 2 3 4 5 6 7
| extension-configs: - data-id: content-service-${spring.profiles.active}.yaml group: xuecheng-plus-project refresh: true - data-id: 填写文件 dataid group: xuecheng-plus-project refresh: true
|
启动content-api工程,查询控制台是否打印出了请求nacos的日志,如下:
并使用Httpclient测试课程查询接口是否可以正常查询。
2.2.3 公用配置
1️⃣还有一个优化的点是如何在nacos中配置项目的公用配置呢?
2️⃣单独在xuecheng-plus-common分组下创建xuecheng-plus的公用配置,
3️⃣删除接口工程中对swagger的配置。
- 项目使用shared-configs可以引入公用配置。在接口工程的本地配置文件 中引入公用配置,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| spring: application: name: content-api cloud: nacos: server-addr: 192.168.101.65:8848 discovery: namespace: ty_xc_dev group: xuecheng-plus-project config: namespace: ty_xc_dev group: xuecheng-plus-project file-extension: yaml refresh-enabled: true extension-configs: - data-id: content-service-${spring.profiles.active}.yaml group: xuecheng-plus-project refresh: true shared-configs: - data-id: swagger-${spring.profiles.active}.yaml group: xuecheng-plus-common refresh: true - data-id: logging-${spring.profiles.active}.yaml group: xuecheng-plus-common refresh: true profiles: active: dev
|
4️⃣配置完成,重启content-api接口工程,访问http://localhost:63040/content/swagger-ui.html 查看swagger接口文档是否可以正常访问,查看控制台log4j2日志输出是否正常。
2.2.4 配置优先级
1️⃣到目前为止已将所有微服务的配置统一在nacos进行配置,用到的配置文件有本地的配置文件 bootstrap.yaml和nacos上的配置文件,SpringBoot读取配置文件 的顺序如下:
引入配置文件的形式有:
以项目应用名方式引入
以扩展配置文件方式引入
以共享配置文件 方式引入
本地配置文件
各配置文件的==优先级==:项目应用名配置文件 > 扩展配置文件 > 共享配置文件 > 本地配置文件。
2️⃣有时候我们在测试程序时直接在本地加一个配置进行测试,比如下边的例子:
- 我们想启动两个内容管理微服务,此时需要在本地指定不同的端口,通过VM Options参数,在IDEA配置启动参数
通过-D指定参数名和参数值,参数名即在bootstrap.yml中配置的server.port。
- 启动ContentApplication2,发现端口仍然是63040,这说明本地的配置没有生效。
3️⃣这时我们想让本地最优先,可以在nacos配置文件 中配置如下即可实现:
1 2 3 4 5
| spring: cloud: config: override-none: true
|
4️⃣再次启动ContentApplication2,端口为63041。
2.2.5 导入配置文件
课程资料中提供了系统用的所有配置文件nacos_config_export.zip,下边将nacos_config_export.zip导入nacos。
进入具体的命名空间,点击“导入配置”
2.3 搭建 Gateway
本项目使用Spring Cloud Gateway作为网关,下边创建网关工程。
1️⃣新建一个网关工程。
2️⃣修改pom.xml,添加相关依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.xuecheng</groupId> <artifactId>xuecheng-plus-parent</artifactId> <version>0.0.1-SNAPSHOT</version> <relativePath>../xuecheng-plus-parent</relativePath> </parent> <artifactId>xuecheng-plus-gateway</artifactId> <version>0.0.1-SNAPSHOT</version> <name>xuecheng-plus-gateway</name> <description>xuecheng-plus-gateway</description>
<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> <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>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency>
</dependencies>
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
</project>
|
3️⃣配置网关的bootstrap.yaml配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| spring: application: name: gateway cloud: nacos: server-addr: 192.168.101.65:8848 discovery: namespace: ty_xc_dev group: xuecheng-plus-project config: namespace: ty_xc_dev group: xuecheng-plus-project file-extension: yaml refresh-enabled: true shared-configs: - data-id: logging-${spring.profiles.active}.yaml group: xuecheng-plus-common refresh: true
profiles: active: dev
|
4️⃣在nacos上配置网关路由策略:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| server: port: 63010 spring: cloud: gateway:
routes: - id: content-api uri: lb://content-api predicates: - Path=/content/**
- id: system-api uri: lb://system-api predicates: - Path=/system/**
- id: media-api uri: lb://media-api predicates: - Path=/media/**
- id: search-service uri: lb://search predicates: - Path=/search/**
- id: auth-service uri: lb://auth-service predicates: - Path=/auth/**
- id: checkcode uri: lb://checkcode predicates: - Path=/checkcode/**
- id: learning-api uri: lb://learning-api predicates: - Path=/learning/**
- id: orders-api uri: lb://orders-api predicates: - Path=/orders/**
|
5️⃣启动网关工程,通过网关工程访问微服务进行测试。
- 在http-client-env.json中配置网关的地址
1 2 3 4 5 6 7 8 9 10 11 12 13
| { "dev": { "access_token": "", "gateway_host": "localhost:63010", "content_host": "localhost:63040", "system_host": "localhost:63110", "media_host": "localhost:63050", "search_host": "localhost:63080", "auth_host": "localhost:63070", "checkcode_host": "localhost:63075", "learning_host": "localhost:63020" } }
|
6️⃣使用httpclient测试课程查询 接口,如下:
1 2 3 4 5 6 7 8 9
| ### 查询课程信息 POST {{gateway_host}}/content/course/list?pageNo=1&pageSize=2 Content-Type: application/json
{ "auditStatus": "202004", "courseName": "java", "publishStatus": "" }
|
运行,观察是否可以正常访问接口 ,如下所示可以正常请求接口。
7️⃣网关工程搭建完成即可将前端工程中的接口地址改为网关的地址
启动前端工程,测试之前开发内容管理模块的功能。
观察网关控制台,通过网关转发课程查询的日志如下:
2.4 搭建媒资工程
至此网关、Nacos已经搭建完成,下边将媒资工程导入项目。
从课程资料中获取媒资工程 xuecheng-plus-media,拷贝到项目工程根目录。
右键pom.xml转为maven工程。
下边做如下配置:
创建媒资数据库xc_media,并导入资料目录中的xcplus_media.sql
修改nacos上的media-service-dev.yaml配置文件中的数据库链接信息
重启media-api工程只要能正常启动成功即可,稍后根据需求写接口。
3. 分布式文件系统
3.1 什么是分布式文件系统
1️⃣要理解分布式文件系统首先了解什么是文件系统。
查阅百度百科:
文件系统是负责管理和存储文件的系统软件,操作系统通过文件系统提供的接口去存取文件,用户通过操作系统访问磁盘上的文件。
下图指示了文件系统所处的位置:
常见的文件系统:FAT16/FAT32、NTFS、HFS、UFS、APFS、XFS、Ext4等 。
2️⃣现在有个问题,一此短视频平台拥有大量的视频、图片,这些视频文件、图片文件该如何存储呢?
我们阅读百度百科去理解分布式文件系统的定义:
通过概念可以简单理解为:一个计算机无法存储海量的文件,通过网络将若干计算机组织起来共同去存储海量的文件,
- 去接收海量用户的请求,这些组织起来的计算机通过网络进行通信,如下图:
好处:
一台计算机的文件系统处理能力扩充到多台计算机同时处理。
一台计算机挂了还有另外副本计算机提供数据。
每台计算机可以放在不同的地域,这样用户就可以就近访问,提高访问速度。
3️⃣市面上有哪些分布式文件系统的产品呢?
- NFS
阅读百度百科:
特点:
1)在客户端上映射NFS服务器的驱动器。
2)客户端通过网络访问NFS服务器的硬盘完全透明。
- GFS
1)GFS采用主从结构,一个GFS集群由一个master和大量的chunkserver组成。
2)master存储了数据文件的元数据,一个文件被分成了若干块存储在多个chunkserver中。
3)用户从master中获取数据元信息,向chunkserver存储数据。
- HDFS
HDFS,是Hadoop Distributed File System的简称,是Hadoop抽象文件系统的一种实现。
- HDFS是一个高度容错性的系统,适合部署在廉价的机器上。
- HDFS能提供高吞吐量的数据访问,非常适合大规模数据集上的应用。
- HDFS的文件分布在集群机器上,同时提供副本进行容错及可靠性保证。
- 例如客户端写入读取文件的直接操作都是分布在集群各个机器上的,没有单点性能压力。
下图是HDFS的架构图:
1)HDFS采用主从结构,一个HDFS集群由一个名称结点和若干数据结点组成。
2)名称结点存储数据的元信息,一个完整的数据文件分成若干块存储在数据结点。
3)客户端从名称结点获取数据的元信息及数据分块的信息,得到信息客户端即可从数据块来存取数据。
4. 云计算厂家
阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。其数据设计持久性不低于 99.9999999999%(12 个 9),服务设计可用性(或业务连续性)不低于 99.995%。
官方网站:https://www.aliyun.com/product/oss
百度对象存储BOS提供稳定、安全、高效、高可扩展的云存储服务。您可以将任意数量和形式的非结构化数据存入BOS,并对数据进行管理和处理。BOS支持标准、低频、冷和归档存储等多种存储类型,满足多场景的存储需求。
官方网站:https://cloud.baidu.com/product/bos.html
3.2 MinIo
3.2.1 介绍
本项目采用MinIO构建分布式文件系统,
MinIO 是一个非常轻量的服务,可以很简单的和其他应用的结合使用,它兼容亚马逊 S3 云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等。
它一大特点就是轻量,使用简单,功能强大,支持各种平台,单个文件最大5TB,兼容 Amazon S3接口,提供了 Java、Python、GO等多版本SDK支持。
官网:https://min.io
中文:https://www.minio.org.cn/,http://docs.minio.org.cn/docs/
MinIO集群采用去中心化共享架构,每个结点是对等关系,通过Nginx可对MinIO进行负载均衡访问。
去中心化有什么好处?
在大数据领域,通常的设计理念都是无中心和分布式。
- Minio分布式模式可以帮助你搭建一个高可用的对象存储服务,你可以使用这些存储设备,而不用考虑其真实物理位置。
它将分布在不同服务器上的多块硬盘组成一个对象存储服务。由于硬盘分布在不同的节点上,分布式Minio避免了单点故障。如下图:
Minio使用纠删码技术来保护数据,它是一种恢复丢失和损坏数据的数学算法,
- 它将数据分块冗余的分散存储在各各节点的磁盘上,所有的可用磁盘组成一个集合,
- 上图由8块硬盘组成一个集合,当上传一个文件时会通过纠删码算法计算对文件进行分块存储,除了将文件本身分成4个数据块,
- 还会生成4个校验块,数据块和校验块会分散的存储在这8块硬盘上。
使用纠删码的好处是即便丢失一半数量(N/2)的硬盘,仍然可以恢复数据。
- 比如上边集合中有4个以内的硬盘损害仍可保证数据恢复,不影响上传和下载,
- 如果多于一半的硬盘坏了则无法恢复。
3.2.2 测试docker 环境
开发阶段和生产阶段统一使用Docker下的MINIO。
在下发的虚拟机中已安装了MinIO的镜像和容器,执行sh /data/soft /restart.sh启动Docker下的MinIO
启动完成登录MinIO查看是否正常。
访问http://192.168.101.65:9000
本项目创建两个buckets:
mediafiles: 普通文件
video:视频文件
3.2.4 SDK
上传文件
MinIO提供多个语言版本SDK的支持,下边找到java版本的文档:
地址:https://docs.min.io/docs/java-client-quickstart-guide.html
最低需求Java 1.8或更高版本:
maven依赖如下:
1 2 3 4 5 6 7 8 9 10
| <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是你账户的密码。 |
官方的示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| 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 { MinioClient minioClient = MinioClient.builder() .endpoint("https://play.min.io") .credentials("Q3AM3UQ867SPQQA43P2F", "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG") .build(); boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket("asiatrip").build()); if (!found) { minioClient.makeBucket(MakeBucketArgs.builder().bucket("asiatrip").build()); } else { System.out.println("Bucket 'asiatrip' already exists."); } 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()); } } }
|
参考示例在media-service工程中 测试上传文件功能,
首先创建一个用于测试的bucket
在xuecheng-plus-media-service工程 的test下编写测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| package com.xuecheng.media;
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 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("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("上传失败"); }
}
}
|
执行upload方法,分别测试向桶的根目录上传文件以及子目录上传文件。
说明:
1 2 3
| ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(".mp4"); String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
完善上边的代码 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Test public void upload() { ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(".mp4"); String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE; if(extensionMatch!=null){ mimeType = extensionMatch.getMimeType(); } try { UploadObjectArgs testbucket = UploadObjectArgs.builder() .bucket("testbucket") .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("上传失败"); }
}
|
删除文件
下边测试删除文件
参考:https://docs.min.io/docs/java-client-api-reference#removeObject
1 2 3 4 5 6 7 8 9 10 11
| @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("删除失败"); } }
|
查询文件
通过查询文件查看文件是否存在minio中。
参考:https://docs.min.io/docs/java-client-api-reference#getObject
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Java
@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,一致则说明完整
1 2 3 4 5 6 7 8
| 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. 上传图片
4.1 需求分析
4.1.1 业务流程
课程图片是宣传课程非常重要的信息,在新增课程界面上传课程图片,也可以修改课程图片。
如下图:
上传课程图片总体上包括两部分:
上传课程图片前端请求媒资管理服务将文件上传至分布式文件系统,并且在媒资管理数据库保存文件信息。
上传图片成功保存图片地址到课程基本信息表中。
详细流程如下:
前端进入上传图片界面
上传图片,请求媒资管理服务。
媒资管理服务将图片文件存储在MinIO。
媒资管理记录文件信息到数据库。
前端请求内容管理服务保存课程信息,在内容管理数据库保存图片地址。
4.1.2 数据模型
涉及到的数据表有:课程信息表中的图片字段、媒资数据库的文件表,下边主要看媒资数据库的文件表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| create table media_files ( id varchar(32) not null comment '文件id,md5值' primary key, company_id bigint null comment '机构ID', company_name varchar(255) null comment '机构名称', filename varchar(255) not null comment '文件名称', file_type varchar(12) null comment '文件类型(图片、文档,视频)', tags varchar(120) null comment '标签', bucket varchar(128) null comment '存储目录', file_path varchar(512) null comment '存储路径', file_id varchar(32) not null comment '文件id', url varchar(1024) null comment '媒资文件访问地址', username varchar(60) null comment '上传人', create_date datetime null comment '上传时间', change_date datetime null comment '修改时间', status varchar(12) default '1' null comment '状态,1:正常,0:不展示', remark varchar(32) null comment '备注', audit_status varchar(12) null comment '审核状态', audit_mind varchar(255) null comment '审核意见', file_size bigint null comment '文件大小', constraint unique_fileid unique (file_id) comment '文件id唯一索引 ' ) comment '媒资信息' charset = utf8 row_format = DYNAMIC;
|
4.2 准备环境
1️⃣首先在minio配置bucket,bucket名称为:mediafiles,并设置bucket的权限为公开。
2️⃣在nacos配置中minio的相关信息,进入media-service-dev.yaml:
1 2 3 4 5 6 7
| minio: endpoint: http://192.168.101.65:9000 accessKey: minioadmin secretKey: minioadmin bucket: files: mediafiles videofiles: video
|
3️⃣在media-service工程编写minio的配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| 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;
@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; } }
|
4.3 接口定义
根据需求分析,下边进行接口定义,此接口定义为一个通用的上传文件接口,可以上传图片或其它文件。
1️⃣首先分析接口:
请求地址:/media/upload/coursefile
请求内容:Content-Type: multipart/form-data;
form-data; name=”filedata”; filename=”具体的文件名称”
响应参数:文件信息,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| { "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 }
|
2️⃣定义上传响应模型类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package com.xuecheng.media.model.dto;
import com.xuecheng.media.model.po.MediaFiles;
@Data public class UploadFileResultDto extends MediaFiles {
}
|
3️⃣定义接口如下:
1 2 3 4 5 6
| @ApiOperation("上传图片") @RequestMapping(value = "/upload/coursefile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public UploadFileResultDto upload(@RequestPart("filedata")MultipartFile filedata) throws IOException {
return null; }
|
4️⃣接口定义好后可以用httpclient工具测试一下
1 2 3 4 5 6 7 8 9
| ### 上传文件 POST {{media_host}}/media/upload/coursefile Content-Type: multipart/form-data; boundary=WebAppBoundary
--WebAppBoundary Content-Disposition: form-data; name="filedata"; filename="1.jpg" Content-Type: application/octet-stream
< d:/develop/upload/1.jpg
|
4.4 接口开发
4.4.1 DAO 开发
根据需求分析DAO层实现向media_files表插入一条记录,使用media_files表生成的mapper即可。
4.4.2 Service 开发
Service方法需要提供一个更加通用的保存文件的方法。
1️⃣定义请求参数类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| package com.xuecheng.media.model.dto;
import lombok.Data;
@Data public class UploadFileParamsDto {
private String filename;
private String fileType;
private Long fileSize;
private String tags;
private String username;
private String remark; }
|
2️⃣定义service方法:
1 2 3 4 5 6 7 8
|
UploadFileResultDto upload(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath);
|
3️⃣实现方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
| package com.xuecheng.media.service.impl;
@Service @Slf4j public class MediaFileServiceImpl implements MediaFileService {
@Resource private MediaFilesMapper mediaFilesMapper;
@Resource private MinioClient minioClient;
@Resource private MediaFileService currentProxy;
@Value("${minio.bucket.files}") private String bucket_mediafiles;
@Value("${minio.bucket.videofiles}") private String bucket_video;
@Override public PageResult<MediaFiles> queryMediaFiels(Long companyId, PageParams pageParams, QueryMediaParamsDto queryMediaParamsDto) {
LambdaQueryWrapper<MediaFiles> queryWrapper = new LambdaQueryWrapper<>();
Page<MediaFiles> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize()); Page<MediaFiles> pageResult = mediaFilesMapper.selectPage(page, queryWrapper); List<MediaFiles> list = pageResult.getRecords(); long total = pageResult.getTotal(); PageResult<MediaFiles> mediaListResult = new PageResult<>(list, total, pageParams.getPageNo(), pageParams.getPageSize()); return mediaListResult;
}
@Override public UploadFileResultDto upload(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath) {
String filename = uploadFileParamsDto.getFilename(); String extension = filename.substring(filename.lastIndexOf("."));
String mimeType = getMimeType(extension);
String defaultFolderPath = getDefaultFolderPath(); String fileMd5 = getFileMd5(new File(localFilePath)); String objectName = defaultFolderPath + fileMd5 + extension;
boolean result = addMediaFilesToMinIO(localFilePath, mimeType, bucket_mediafiles, objectName); if(!result) { XueChengPlusException.cast("上传文件失败"); }
MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_mediafiles, objectName); if(mediaFiles == null) { XueChengPlusException.cast("文件上传后保存信息失败"); } UploadFileResultDto uploadFileResultDto = new UploadFileResultDto(); BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);
return uploadFileResultDto; }
@Transactional @Override 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.setCompanyId(companyId); mediaFiles.setBucket(bucket); mediaFiles.setFilePath(objectName); mediaFiles.setFileId(fileMd5); mediaFiles.setUrl("/" + bucket + "/" + objectName); mediaFiles.setCreateDate(LocalDateTime.now()); mediaFiles.setStatus("1"); mediaFiles.setAuditStatus("002003");
int insert = mediaFilesMapper.insert(mediaFiles); if(insert <= 0) { log.debug("向数据库保存文件失败, bucket:{}, objectName:{},", bucket, objectName); return null; } return mediaFiles; } return mediaFiles; } 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 getDefaultFolderPath() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); String folder = sdf.format(new Date()).replace("-", "/") + "/"; return folder; }
private boolean addMediaFilesToMinIO(String localFilePath, String mimeType, String bucket, String objectName) { try { UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder() .bucket(bucket) .filename(localFilePath) .object(objectName) .contentType(mimeType) .build(); minioClient.uploadObject(uploadObjectArgs); log.debug("上传文件到minio成功bucket:{}, objectName:{},", bucket, objectName); return true; } catch (Exception e) { e.printStackTrace(); log.error("上传文件出错, bucket:{}, objectName:{}, 错误信息:{}", bucket, objectName, e.getMessage()); } return false; }
private String getMimeType(String extension) { ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension); String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE; if(extensionMatch != null) { mimeType = extensionMatch.getMimeType(); } return mimeType; } }
|
4.4.3 完善接口层
完善接口层代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| @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.setFilename(filedata.getOriginalFilename()); uploadFileParamsDto.setFileSize(filedata.getSize()); uploadFileParamsDto.setFileType("001001");
File tempFile = File.createTempFile("minio", ".temp"); filedata.transferTo(tempFile);
String localFilePath = tempFile.getAbsolutePath();
UploadFileResultDto uploadFileResultDto = mediaFileService.upload(companyId, uploadFileParamsDto, localFilePath);
return uploadFileResultDto; }
|
4.4.4 接口测试
1️⃣首先使用httpclient测试
1 2 3 4 5 6 7 8 9
| ### 上传文件 POST {{media_host}}/media/upload/coursefile Content-Type: multipart/form-data; boundary=WebAppBoundary
--WebAppBoundary Content-Disposition: form-data; name="filedata"; filename="1.jpg" Content-Type: application/octet-stream
< d:/develop/upload/1.jpg
|
2️⃣再进行前后端联调测试
- 在新增课程、编辑课程界面上传图,保存课程信息后再次进入编辑课程界面,查看是否可以正常保存课程图片信息。
- 上图图片完成后,进入媒资管理,查看文件列表中是否有刚刚上传的图片信息。
4.4.5 Service 事务优化
上边的service方法优化后并测试通过,现在思考==关于uploadFile方法的是否应该开启事务==。
1️⃣目前是在uploadFile方法上添加@Transactional,当调用uploadFile方法前会开启数据库事务,
- 如果上传文件过程时间较长那么数据库的事务持续时间就会变长,
- 这样数据库链接释放就慢,最终导致数据库链接不够用。
我们只将addMediaFilesToDb方法添加事务控制即可,uploadFile方法上的@Transactional注解去掉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @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; }
|
2️⃣我们人为在 int insert = mediaFilesMapper.insert(mediaFiles);下边添加一个==异常代码int a=1/0==;
方法上已经添加了@Transactional注解为什么该方法不能被事务控制呢?
- 如果是在uploadFile方法上添加@Transactional注解就可以控制事务,去掉则不行。
现在的问题其实是一个非事务方法调同类一个事务方法,事务无法控制,这是为什么?
3️⃣下边分析原因:
- 如果在uploadFile方法上添加@Transactional注解,代理对象执行此方法前会开启事务,如下图:
- 如果在uploadFile方法上没有@Transactional注解,代理对象执行此方法前不进行事务控制,如下图:
- 所以判断该方法是否可以事务控制必须保证是==通过代理对象调用此方法,且此方法上添加了@Transactional注解==。
现在在addMediaFilesToDb方法上添加@Transactional注解,
- 也不会进行事务控制是因为并不是通过代理对象执行的addMediaFilesToDb方法。
- 为了判断在uploadFile方法中去调用addMediaFilesToDb方法是否是通过代理对象去调用,我们可以打断点跟踪。
- 我们发现在uploadFile方法中去调用addMediaFilesToDb方法不是通过代理对象去调用。
4️⃣如何解决呢?通过代理对象去调用addMediaFilesToDb方法即可解决。
- 在MediaFileService的实现类中注入MediaFileService的代理对象,如下:
1 2
| @Resource private MediaFileService currentProxy;
|
- 将addMediaFilesToDb方法提成接口。
1 2 3 4 5 6 7 8 9 10 11 12 13
|
MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName);
|
- 调用addMediaFilesToDb方法的代码处改为如下:
1 2 3 4
| .....
MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_files, objectName); ....
|
5️⃣再次测试事务是否可以正常控制。