weblog/doc/8、对象存储服务搭建与开发/9.3 文件上传接口开发.md
2025-02-17 10:05:44 +08:00

12 KiB
Raw Blame History

上小节中,我们已经在本地搭建好了 Minio 对象存储的环境。本小节中,我们就来为后端服务添加一个 —— 文件上传接口。

1. 添加 Minio 依赖

首先,在父项目的 pom.xml 文件中添加 Minio 版本管理声明:

    <!-- 版本号统一管理 -->
    <properties>
        // 省略...
        <minio.version>8.2.1</minio.version>
    </properties>
    
       <!-- 统一依赖管理 -->
    <dependencyManagement>
        <dependencies>
			// 省略...

            <!-- 对象存储 Minio -->
            <dependency>
                <groupId>io.minio</groupId>
                <artifactId>minio</artifactId>
                <version>${minio.version}</version>
            </dependency>

        </dependencies>
    </dependencyManagement>

然后,在 weblog-module-admin 模块中的 pom.xml 文件中,添加该依赖:

		<!-- 对象存储 Minio -->
		<dependency>
			<groupId>io.minio</groupId>
			<artifactId>minio</artifactId>
		</dependency>

2. 添加 Minio 配置

编辑 applicaiton-dev.yml 配置文件,添加 minio 相关的配置项:

#=================================================================
# minio
#=================================================================
minio:
  endpoint: http://127.0.0.1:9000
  accessKey: quanxiaoha
  secretKey: quanxiaoha
  bucketName: weblog

解释一下这些配置都是干啥的:

  1. endpoint: http://127.0.0.1:9000:指定 MinIO 服务器的地址。实际部署时,您需要将它替换为您 MinIO 服务器的地址。
  2. accessKey: quanxiaoha:运行容器时,指定的接入 key
  3. secretKey: quanxiaoha:运行容器时,指定的秘钥 key
  4. bucketName: weblog存储桶bucket的名称。

3. 新增 Minio 配置类

接着,在 weblog-module-admin 子模块的 /config 包下,创建一个 MinioProperties 配置类,用来读取刚刚手动配置的 Minio 选项:

@ConfigurationProperties(prefix = "minio")
@Component
@Data
public class MinioProperties {
    private String endpoint;
    private String accessKey;
    private String secretKey;
    private String bucketName;
}

⚠️ 注意:配置类的前缀需要指定为 minio, 且字段名要与配置项名称保持一致。

4. 新增 Minio 客户端配置

继续在 /config 包中,创建一个 MinioConfig 客户端配置类:

@Configuration
public class MinioConfig {
    @Autowired
    private MinioProperties minioProperties;

    @Bean
    public MinioClient minioClient() {
        // 构建 Minio 客户端
        return MinioClient.builder()
                .endpoint(minioProperties.getEndpoint())
                .credentials(minioProperties.getAccessKey(), minioProperties.getSecretKey())
                .build();
    }
}

上述代码中,我们构建了一个 MinioClient 客户端,并使用 @Bean 注解注入到了 Spring 容器中。

5. 封装图片上传工具类

初始化好 Minio 客户端后,在 weblog-module-admin 子模块中,新增一个 /utils 工具包,然后,新建 MinioUtil 工具类,并添加一个上传文件的方法,代码如下:

@Component
@Slf4j
public class MinioUtil {

    @Autowired
    private MinioProperties minioProperties;

    @Autowired
    private MinioClient minioClient;

    /**
     * 上传文件
     * @param file
     * @return
     * @throws Exception
     */
    public String uploadFile(MultipartFile file) throws Exception {
        // 判断文件是否为空
        if (file == null || file.getSize() == 0) {
            log.error("==> 上传文件异常:文件大小为空 ...");
            throw new RuntimeException("文件大小不能为空");
        }

        // 文件的原始名称
        String originalFileName = file.getOriginalFilename();
        // 文件的 Content-Type
        String contentType = file.getContentType();

        // 生成存储对象的名称(将 UUID 字符串中的 - 替换成空字符串)
        String key = UUID.randomUUID().toString().replace("-", "");
        // 获取文件的后缀,如 .jpg
        String suffix = originalFileName.substring(originalFileName.lastIndexOf("."));

        // 拼接上文件后缀,即为要存储的文件名
        String objectName = String.format("%s%s", key, suffix);

        log.info("==> 开始上传文件至 Minio, ObjectName: {}", objectName);
        
        // 上传文件至 Minio
        minioClient.putObject(PutObjectArgs.builder()
                .bucket(minioProperties.getBucketName())
                .object(objectName)
                .stream(file.getInputStream(), file.getSize(), -1)
                .contentType(contentType)
                .build());

        // 返回文件的访问链接
        String url = String.format("%s/%s/%s", minioProperties.getEndpoint(), minioProperties.getBucketName(), objectName);
        log.info("==> 上传文件至 Minio 成功,访问路径: {}", url);
        return url;
    }
}

上述代码中,我们首先对 MultipartFile 进行了判空,防止上传空的文件。然后拿到了该文件的相关属性值,如原始文件名、Content-Type ,通过对原始文件名截取出文件后缀,以及通过 UUID 生成一个随机的文件名,最终通过 minioClient 客户端的 putObject 方法来上传文件,上传成功后,将文件的访问链接拼接好并返回。

6. 接口出入参格式

前置工作都完成后,我们准备开始开发文件上传接口。

6.1 接口地址

POST /admin/file/upload 表单格式提交

6.2 入参

字段名 描述
file 选择一个本地文件

6.3 出参

{
  "success": true,
  "message": null,
  "errorCode": null,
  "data": {
    "url": "http://127.0.0.1:9000/weblog/d73ec5ffb1074aacb07c0899663068dd.jpg" // 文件的访问地址
  }
}

7. 新增上传文件返参 VO

weblog-module-admin 模块中的 /model/vo 包下,新增名为 /file 的包,用于放置文件模块相关的 VO 类,然后,创建 UploadFileRspVO 文件上传返参实体类:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UploadFileRspVO {

    /**
     * 文件的访问链接
     */
    private String url;

}

8. 新增上传文件 service

上传文件的工具类封装好后,我们在 weblog-module-admin 模块中的 service 包中添加 AdminFileService 接口,并在其中定义一个文件上传方法:

public interface AdminFileService {
    /**
     * 上传文件
     * @param file
     * @return
     */
    Response uploadFile(MultipartFile file);
}

然后,在 /impl 包中,创建该接口的实现类 AdminFileServiceImpl , 代码如下:

@Service
@Slf4j
public class AdminFileServiceImpl implements AdminFileService {

    @Autowired
    private MinioUtil minioUtil;

    /**
     * 上传文件
     *
     * @param file
     * @return
     */
    @Override
    public Response uploadFile(MultipartFile file) {
        try {
            // 上传文件
            String url = minioUtil.uploadFile(file);

            // 构建成功返参,将图片的访问链接返回
            return Response.success(UploadFileRspVO.builder().url(url).build());
        } catch (Exception e) {
            log.error("==> 上传文件至 Minio 错误: ", e);
            // 手动抛出业务异常,提示 “文件上传失败”
            throw new BizException(ResponseCodeEnum.FILE_UPLOAD_FAILED);
        }
    }
}

代码比较简单,我们将 MinioUtil 注入后,直接调用工具方法中的 uploadFile() 方法,并构建成功返参,将图片的访问链接返回。

8.1 添加文件上传失败全局枚举

上述代码中,当上传文件捕获到异常时,则手动抛出了上传文件失败异常。这里,我们还需要编辑 weblog-module-common 模块中的 ResponseCodeEnum 全局枚举类,添加对应的枚举值,如下:

FILE_UPLOAD_FAILED("20008", "文件上传失败!"),

9. 新增 Controller 接口

接着,在 /controller 包下,创建 AdminFileController 控制器,并新增文件上传接口 /amdin/file/upload, 代码如下:

@RestController
@RequestMapping("/admin")
@Api(tags = "Admin 文件模块")
public class AdminFileController {

    @Autowired
    private AdminFileService fileService;

    @PostMapping("/file/upload")
    @ApiOperation(value = "文件上传")
    @ApiOperationLog(description = "文件上传")
    public Response uploadFile(@RequestParam MultipartFile file) {
        return fileService.uploadFile(file);
    }

}

注意,文件上传接口并非使用 JSON 格式进行提交,而是使用 Form 表达来提交的,所以入参的注解使用的 @RequestParam , 并使用了 MultipartFile 类接收上传的文件。

10. 测试看看

完成以上工作后,文件上传接口就开发完毕了。接下来,我们通过浏览器访问 localhost:8080/doc.html , 调试一波此接口,看看功能是否正常:

选择一张本地图片后,点击发送,可以看到后端响参成功,并返回了图片的访问地址。复制该图片的访问链接,直接在浏览器中,访问该图片地址,看看能否正常访问:

OK图片访问没有任何问题表明文件上传接口功能正常。

11. 大文件上传失败问题

当你尝试上传一个体积较大的文件时,你可能会在控制台中看到如下异常信息:

Caused by: org.apache.tomcat.util.http.fileupload.impl.FileSizeLimitExceededException: The field file exceeds its maximum permitted size of 1048576 bytes.
	at org.apache.tomcat.util.http.fileupload.impl.FileItemStreamImpl$1.raiseError(FileItemStreamImpl.java:117) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
	at org.apache.tomcat.util.http.fileupload.util.LimitedInputStream.checkLimit(LimitedInputStream.java:76) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
	at org.apache.tomcat.util.http.fileupload.util.LimitedInputStream.read(LimitedInputStream.java:135) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
	at java.io.FilterInputStream.read(FilterInputStream.java:107) ~[na:1.8.0_311]
	at org.apache.tomcat.util.http.fileupload.util.Streams.copy(Streams.java:97) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
	at org.apache.tomcat.util.http.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:288) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
	at org.apache.catalina.connector.Request.parseParts(Request.java:2932) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
	... 96 common frames omitted

这个错误表明在尝试上传文件时,文件的大小超过了 Spring Boot 设置的最大允许大小限制。具体而言,错误消息中的 "The field file exceeds its maximum permitted size of 1048576 bytes" 意味着上传的文件大小超过了 1048576 字节1MB。为了解决这个问题可以编辑 application.yml 文件,添加如下配置, 来手动设置上传文件的大小:

spring:
  servlet:
    multipart:
      max-file-size: 10MB # 限制单个上传文件的最大大小为 10MB。如果上传的文件大小超过这个值将会被拒绝上传。
      max-request-size: 10MB # 限制整个上传请求的最大大小为 10MB。这包括所有上传文件的大小之和。如果请求总大小超过这个值将会被拒绝。

12. 本小节对应源码下载

https://t.zsxq.com/12T2XU3sv

13. 结语

本小节中,小哈带着大家手动封装了一个 Minio 的文件上传工具类,并新增了一个文件上传接口,最后,通过 Knife4j 调试了该接口,选择了一张本地图片进行上传,接口调试通过。