【Java实战篇】Day3.在线教育网课平台

文章目录

  • 一 、媒资模块环境搭建
    • 1、 网关gateway
    • 2、Nacos
    • 3、搭建gateway
  • 二、分布式文件系统
    • 2.1 文件系统
    • 2.2 分布式文件系统
    • 2.3 MinIO
  • 四、上传图片
    • 4.1 需求分析
    • 4.2 数据模型与环境配置
    • 4.3 接口定义
    • 4.4 开发mapper层
    • 4.5 开发service层
    • 4.6 完善controller层
    • 4.7 Service层事务优化

一 、媒资模块环境搭建

1、 网关gateway

接下来是媒资管理服务,目前为止共三个微服务:内容管理、系统管理、媒资管理:

【Java实战篇】Day3.在线教育网课平台_第1张图片
如此,前端得知道每个微服务实例的地址和端口, 且这样写死IP, 维护也不方便:

在这里插入图片描述
考虑引入网关, (复习下网关的几个作用:

  • 身份认证和权限校验
  • 服务路由和负载均衡
  • 请求限流

【Java实战篇】Day3.在线教育网课平台_第2张图片
如此 , 请求统一到网关,有由网关路由到不同的微服务上, 网关在这儿有点像400电话, 根据不同的需求转接电话到不同的业务员 . 这样, 前端代码中只需要写接口的相对路径:
【Java实战篇】Day3.在线教育网课平台_第3张图片

2、Nacos

2.1 认识Nacos

网关想请求路由, 就必须知道每个微服务实例的地址,项目使用Nacos作用服务发现中心和配置中心:
【Java实战篇】Day3.在线教育网课平台_第4张图片

流程如下:

  • 微服务启动, 将自己的信息注册到Nacos, Nacos记录各个微服务的地址
  • 网关从Nacos读取服务列表, 包括服务名称和服务地址
  • 请求到达网关, 网关将请求路由到具体的微服务

由此也可以看到Nacos的两个作用:

  • 服务发现中心 : 微服务将自身信息注册登记至Nacos,网关从Nacos获取微服务列表
  • 服务配置中心 : 微服务众多, 配置信息复杂 , 微服务的配置信息统一在Nacos配置

Nacos中的两个概念:

  • namespace:用于区分环境、比如:开发环境、测试环境、生产环境
    【Java实战篇】Day3.在线教育网课平台_第5张图片
  • group:用于区分项目,比如:xuecheng-plus项目、xuecheng2.0项目

2.2 实现服务的发现

  • 先在一级父工程的pom文件添加Spring Cloud Alibaba的依赖
<dependency>
    <groupId>com.alibaba.cloudgroupId>
    <artifactId>spring-cloud-alibaba-dependenciesartifactId>
    <version>${spring-cloud-alibaba.version}version>
    <type>pomtype>
    <scope>importscope>
dependency>

  • 在需要注册的模块的pom文件中添加nacos的服务发现和配置文件依赖
<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>

  • 在需要注册的模块的bookstrap.yml中添加信息:
spring:
  application:
    name: de-system  # 应用名称
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        namespace: develop
        group: DEFAULT_GROUP
  • 重启服务,查看nacos, 可以看到注册成功,以及该服务实例的IP和端口等信息
    【Java实战篇】Day3.在线教育网课平台_第6张图片

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工程为例进行配置:

  • 点击新建
    【Java实战篇】Day3.在线教育网课平台_第7张图片
  • 输入dataid、group、以及配置内容:
    【Java实战篇】Day3.在线教育网课平台_第8张图片
  • 点击发布
/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可以引入公用配置 :

  • 定义公用配置:
    【Java实战篇】Day3.在线教育网课平台_第9张图片
  • 在工程本地配置中引入公用配置:
...
		shared-configs:
          - data-id: swagger-${spring.profiles.active}.yaml
            group: xuecheng-plus-common
            refresh: true
...

2.4 配置优先级

到此 , 微服务的配置统一在nacos进行配置,用到的配置文件有本地的配置文件 bootstrap.yaml和nacos上的配置文件 , 服务启动的时候, SpringBoot读取配置文件的顺序如下:
【Java实战篇】Day3.在线教育网课平台_第10张图片
微服务引入配置文件的形式有:

  • 以项目应用名方式引入
  • 以扩展配置文件方式引入
  • 以共享配置文件 方式引入
  • 本地配置文件

当配置有冲突的时候, 优先级:项目应用名配置文件 > 扩展配置文件 > 共享配置文件 > 本地配置文件。想要让本地配置文件优先级最高,可在Nacos中添加:

#配置本地优先
spring:
 cloud:
  config:
    override-none: true

3、搭建gateway

  • 创建一个工程:
    【Java实战篇】Day3.在线教育网课平台_第11张图片
  • 在pom文件中引入相关依赖
<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>

  • 配置网关的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
  • 启动网关工程, 在http-client-env.json中配置网关的地址
    【Java实战篇】Day3.在线教育网课平台_第12张图片

二、分布式文件系统

2.1 文件系统

操作系统通过文件系统提供的接口取存取文件,用户则通过操作系统来访问磁盘上的文件:

【Java实战篇】Day3.在线教育网课平台_第13张图片
常见的文件系统:FAT16/FAT32、NTFS、HFS、UFS、APFS、XFS、Ext4等. 看一下我Windos使用的文件系统:

【Java实战篇】Day3.在线教育网课平台_第14张图片

2.2 分布式文件系统

官话:
在这里插入图片描述

直白的说: 一台计算机无法去进行海量文件的存储和响应海量用户的请求, 因此通过网络将若干计算机组织起来共同去完成这个任务

【Java实战篇】Day3.在线教育网课平台_第15张图片
这样的好处有:

  • 一台计算机的文件系统处理能力扩充到多台计算机同时处理
  • 一台计算机挂了还有另外副本计算机提供数据
  • 每台计算机可以放在不同的地域,这样用户就可以就近访问,提高访问速度

市面上分布式文件系统的相关产品有: NFS、GFS、HDFS:

NFS:

在这里插入图片描述
网络文件系统, 客户端通过网络访问NFS服务器的硬盘

【Java实战篇】Day3.在线教育网课平台_第16张图片

GFS:

【Java实战篇】Day3.在线教育网课平台_第17张图片

  • GFS采用主从结构,一个GFS集群由一个master和大量的chunkserver组成
  • master存储了数据文件的元数据,如大小名称类型, 一个文件被分成了若干块存储在多个chunkserver中
  • 用户从master中获取数据元信息,向chunkserver存储数据

【Java实战篇】Day3.在线教育网课平台_第18张图片

HDFS:

【Java实战篇】Day3.在线教育网课平台_第19张图片

  • HDFS采用主从结构,一个HDFS集群由一个名称结点和若干数据结点组成
  • 名称结点存储数据的元信息,一个完整的数据文件分成若干块存储在数据结点
  • 客户端从名称结点获取数据的元信息及数据分块的信息,得到信息客户端即可从数据块来存取数据

【Java实战篇】Day3.在线教育网课平台_第20张图片

云计算厂家:
  • 阿里云对象存储服务OOS
    在这里插入图片描述
  • 百度对象存储BOS

在实际项目中, 根据场景来进行技术选型

2.3 MinIO

认识MinIO

  • MinIO适合于存储大容量非结构化的数据, 提供了 Java、Python、GO等多版本SDK支持

  • MinIO集群采用去中心化共享架构,每个结点是对等关系,通过Nginx可对MinIO进行负载均衡访问
    【Java实战篇】Day3.在线教育网课平台_第21张图片

  • 将数据分块冗余的分散存储在各各节点的磁盘上,所有的可用磁盘组成一个集合

  • 当上传一个文件时通过纠删码算法对文件进行分块,文件本身分成4个数据块,还会生成4个校验块,数据块和校验块会分散的存储在这8块硬盘上

  • 使用纠删码的好处是不超过一半数量(N/2)的硬盘损坏时,仍然可以恢复数据

使用

  • 下载
链接: https://pan.baidu.com/s/16x9K1j1XPxCHKzqWRmh-mw?pwd=9527 
提取码: 9527 复制这段内容后打开百度网盘手机App,操作更方便哦
  • 启动
在exe文件的目录下打开DOS窗口:
minio.exe server xx xx xx

【Java实战篇】Day3.在线教育网课平台_第22张图片

  • 登录
    http://localhost:9000进行登录,账号和密码为:minioadmin/minioadmin
    【Java实战篇】Day3.在线教育网课平台_第23张图片
  • 创建桶bucket, 相当于存储文件的目录,可以创建若干的桶
    【Java实战篇】Day3.在线教育网课平台_第24张图片
  • 点击upload上传文件, 观察分块存储的效果
    【Java实战篇】Day3.在线教育网课平台_第25张图片

MinIO与Java

MinIO提供多个语言版本SDK的支持, Java相关的文档https://docs.min.io/docs/java-client-quickstart-guide.html

  • 添加Maven依赖
<dependency>
    <groupId>io.miniogroupId>
    <artifactId>minioartifactId>
    <version>8.4.3version>
dependency>
<dependency>
    <groupId>com.squareup.okhttp3groupId>
    <artifactId>okhttpartifactId>
    <version>4.8.1version>
dependency>

  • 设置bucket的权限为public
    【Java实战篇】Day3.在线教育网课平台_第26张图片
    【Java实战篇】Day3.在线教育网课平台_第27张图片

  • 官方示例程序

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 需求分析

【Java实战篇】Day3.在线教育网课平台_第28张图片

整个上传过程有两点:

  • 点击上传课程图片, 前端请求媒资管理服务将文件上传至分布式文件系统,并且在媒资管理数据库保存文件信息
  • 上传图片成功保存图片地址到课程基本信息表中

【Java实战篇】Day3.在线教育网课平台_第29张图片

4.2 数据模型与环境配置

数据模型

【Java实战篇】Day3.在线教育网课平台_第30张图片
其中有字段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会在方法执行前开启事务:
【Java实战篇】Day3.在线教育网课平台_第31张图片
而@Transactional注解改到addMediaFilesToDb上后,controller再调uploadFile方法,则代理对象不再进行事务控制。

【Java实战篇】Day3.在线教育网课平台_第32张图片
判断该方法是否可以事务控制必须保证是通过代理对象调用此方法,且此方法上添加了@Transactional注解。现在事务注解在addMediaFilesToDb方法,在调用这个方法的地方打断点,debug看到调用这个同类中方法的对象(this)并不是代理对象:

【Java实战篇】Day3.在线教育网课平台_第33张图片

根据“事务控制必须保证是通过代理对象调用此方法,且此方法上添加了@Transactional注解”,在MediaFileService的实现类中注入MediaFileService的代理对象(自己注入自己):

@Autowired
MediaFileService currentProxy;

原uploadFile方法中对事务方法addMediaFilesToDb的调用改为:

.....
//写入文件表
MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_files, objectName);
 ....

关于非事务方法调用事务方法的另一种解决思路:【方法2】

你可能感兴趣的:(Spring,java,微服务,开发语言)