发布时间:2025-03-10

MinIO 是一个高性能的分布式对象存储系统。 它是软件定义的,在行业标准硬件上运行,并且 100% 开源,主要许可证是 GNU AGPL v3
MinIO 的不同之处在于它从一开始就被设计为私有/混合云对象存储的标准。 因为 MinIO 是专门为对象而构建的,所以单层架构可以毫不妥协地实现所有必要的功能。 结果是一个同时具有高性能、可扩展性和轻量级的云原生对象服务器
Minio已经是一个包含后台的应用,几乎是即开即用的为念服务应用,使用起来也是相当简单的。
接下来我们将要实现一个支持验证etag的图片获取流程:
1. 前端发起请求先检查etag是否存在(etag会对应一个图片信息用于利用浏览器缓存,案例中使用的是预先加载生成blob的方式)
2. 后端接收请求,先通过minio获取该同名图片的etag。如果获取的而etag和前端传入的相同,则表示图片无修改,无需通过minio再获取图片资源,直接使用浏览器缓存即可。如果不相同,则表示图片不匹配,可能已经进行过更新,则获取新的图片,传递给前端。
3. 前端拿到图片信息,从响应中获取etag,并使用Image预加载图片并生成blob存入storage中,便于刷新后使用浏览器缓存。
docker run -p 9000:9000 -e "MINIO_ACCESS_KEY=minio-access-key" -e "MINIO_SECRET_KEY=minio-secret-key" -v /data:/data minio/minio server /data2.1 在pom.xml中引入minio api
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.10</version>
</dependency>2.2 配置文件构建minioClient
MinioConfig.java
@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() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}MinioService.java
@Autowired
private MinioClient minioClient;2.3 getFile方法的实现
MinioService.java
@Async
public CompletableFuture<ResponseEntity<InputStreamResource>> getFile(
String fileName,
String bucketName,
HttpServletRequest request
) {
try {
// etag 支持
// 1. 获取文件元数据(包含 ETag)
StatObjectResponse stat = minioClient.statObject(
StatObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.build()
);
String etag = stat.etag(); // 从 MinIO 元数据获取 ETag
// 2. 检查客户端 ETag(If-None-Match)
String clientEtag = request.getHeader("If-None-Match");
if (clientEtag != null && clientEtag.equals(etag)) {
// ETag 匹配,返回 304 未修改
return CompletableFuture.completedFuture(ResponseEntity.status(HttpStatus.NOT_MODIFIED)
.eTag(etag)
.build());
}
// 3. 从 MinIO 获取文件流
InputStream inputStream = minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.build()
);
// 4. 设置 HTTP 响应头,支持浏览器访问
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString()));
headers.add(HttpHeaders.CACHE_CONTROL, "public, max-age=3600");
ResponseEntity<InputStreamResource> response = ResponseEntity.ok()
.headers(headers)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.eTag(etag)
.body(new InputStreamResource(inputStream));
return CompletableFuture.completedFuture(response);
} catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidResponseException | ServerException | XmlParserException | IOException | IllegalArgumentException | InvalidKeyException | NoSuchAlgorithmException e) {
return CompletableFuture.completedFuture(ResponseEntity.notFound().build());
}
}2.4 uploadFile的实现(单文件)
@Async
public CompletableFuture<ResponseEntity<ApiResponse<Object>>> uploadFile(@RequestParam("file") MultipartFile file, @RequestParam("bucketName") String bucketName) {
try {
// 1. 确保存储桶存在
boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!found) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
// 2. 生成唯一文件名
String fileName = UUID.randomUUID() + "-" + file.getOriginalFilename();
// 3. 上传到 MinIO
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build()
);
// 4. 这里上传完成后需要生成api访问
HashMap<String, Object> responseMap = new HashMap<>();
responseMap.put("fileName", fileName);
responseMap.put("bucketName", bucketName);
String url = ApiPrefix.files + "/get/" + bucketName + "/" + fileName;
responseMap.put("url", url);
return CompletableFuture.completedFuture(ApiResponseUtil.success(responseMap));
} catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidResponseException | ServerException | XmlParserException | IOException | IllegalArgumentException | InvalidKeyException | NoSuchAlgorithmException e) {
return CompletableFuture.completedFuture(ApiResponseUtil.error("上传失败: " + e.getMessage()));
}
}3.1 这里以单图片上传和加载为例
3.2 一般情况下直接使用返回的url调用后端接口即可
<img src="api/get/{bucketName}/{fileName}" />3.3 预加载并利用http缓存避免重复连接minio获取资源(这里是背景大图的获取和缓存)
// 检查 blob URL 是否有效
const checkUrlValidity = (url: string): Promise<boolean> => {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(true);
img.onerror = () => resolve(false);
img.src = url;
setTimeout(() => resolve(false), 2000); // 超时判定为失效
});
};
const getBackground = async (fileUrl: string, storageKey: string) => {
const headers: HttpRequestHeader = {}
const { etag, url } = getLocalStorage<BgInStorage>(storageKey) || {};
// 1. 验证缓存是否有效
let isValid = false;
if (url) {
isValid = await checkUrlValidity(url);
}
// 2. header设置etag
if (etag && isValid) {
headers['If-None-Match'] = etag;
}
const response = await fetch(fileUrl, { headers });
if (response.status === HttpStatusCode.NotModified) {
return url;
}
if (response.ok) {
// 3. 保存新的 ETag
const etag = response.headers.get('ETag') || '';
// 4. 处理文件流
const blob = await response.blob();
const url = URL.createObjectURL(blob);
setLocalStorage<BgInStorage>(storageKey, {
etag,
url,
});
// 5. 返回处理好的blob,便于显示
return url;
}
}