前言
在项目开发中,我们使用 Minio 作为图片存储服务。随着时间推移,存储的图片文件越来越多,其中大量历史图片已不再需要。为了优化存储空间并降低成本,需要实现一个定时清理功能,定期删除指定日期前的图片文件。
一、依赖坐标
核心依赖:Minio 和定时任务(Spring Boot 的起步依赖已经自带定时任务支持)。
<!--minio-->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.1</version>
</dependency>
依赖说明:
| 组件 |
描述 |
| Minio SDK |
提供与 Minio 服务交互的 API,支持对象存储操作(如上传、下载文件) |
| Spring Boot Starter |
内置定时任务支持(无需额外依赖),简化任务调度和后台处理 |
二、删除方法
1、工具类:MinioUtil
1.1 方法介绍
| 方法签名 |
作用描述 |
返回值类型 |
幂等性保证 |
deleteDateFoldersBefore(LocalDate endExclusive) |
删除指定日期区间 [retainSince, endExclusive) 内的所有日期目录 |
实际删除的对象数量 |
多次调用结果一致 |
deleteSingleFolder(String prefix) |
删除单个前缀路径下的全部对象 |
本次删除的对象数量 |
同上 |
文件格式为:/bucketName/yyyy-MM-dd/xxx.jpeg
1.2 方法代码
参数说明:
| 参数 |
类型 |
说明 |
endExclusive |
LocalDate |
截止日期(不含此日期) |
retainSince |
LocalDate |
保留起始日期(最早不删除的日期) |
prefix |
String |
Minio 对象前缀(即目录路径) |
private String bucketName = "";
private LocalDate retainSince = LocalDate.of(2025, 6, 1); // 用于判断的起始时间
/**
* 删除早于指定日期的所有日期目录(yyyy-MM-dd/)
*
* @param endExclusive 截止日期(不含)
* @return 实际删除的对象总数
*/
public int deleteDateFoldersBefore(LocalDate endExclusive) {
if (endExclusive == null) {
throw new IllegalArgumentException("指定日期不能为空");
}
LocalDate today = LocalDate.now();
if (!endExclusive.isBefore(today)) {
return 0;
}
int totalDeleted = 0;
// 从 endExclusive-1 天开始往前删
for (LocalDate d = endExclusive.minusDays(1); !d.isBefore(retainSince); d = d.minusDays(1)) {
totalDeleted += deleteSingleFolder(d.format(DateTimeFormatter.ISO_LOCAL_DATE) + "/");
}
return totalDeleted;
}
/**
* 删除单个目录(前缀)下的全部对象
*/
private int deleteSingleFolder(String prefix) {
try {
List<DeleteObject> objects = new ArrayList<>();
minioClient.listObjects(ListObjectsArgs.builder()
.bucket(bucketName)
.prefix(prefix)
.recursive(true)
.build())
.forEach(r -> {
try {
objects.add(new DeleteObject(r.get().objectName()));
} catch (Exception ignored) {
log.warn("文件名获取失败");
}
});
if (objects.isEmpty()) {
return 0;
}
Iterable<Result<DeleteError>> results = minioClient.removeObjects(
RemoveObjectsArgs.builder()
.bucket(bucketName)
.objects(objects)
.build());
for (Result<DeleteError> res : results) {
DeleteError deleteError = res.get(); // 无异常即成功
}
return objects.size();
} catch (Exception e) {
log.warn("删除目录 {} 失败: {}", prefix, e.toString());
return 0;
}
}
1.3 完整代码
以下是 MinioUtil 中与清理功能相关的核心部分(包含初始化、删除方法和文件名生成):
import cn.hutool.core.lang.UUID;
import com.fc.properties.MinioProperties;
import io.minio.*;
import io.minio.errors.ErrorResponseException;
import io.minio.http.Method;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.utils.IOUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 文件操作工具类
*/
@RequiredArgsConstructor
@Component
@Slf4j
public class MinioUtil {
private final MinioProperties minioProperties;
private MinioClient minioClient;
private String bucketName;
private LocalDate retainSince = LocalDate.of(2025, 6, 1);
// 初始化 Minio 客户端
@PostConstruct
public void init() {
try {
// 创建客户端
minioClient = MinioClient.builder()
.endpoint(minioProperties.getUrl())
.credentials(minioProperties.getUsername(), minioProperties.getPassword())
.build();
bucketName = minioProperties.getBucketName();
// 检查桶是否存在,不存在则创建
boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!bucketExists) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
} catch (Exception e) {
throw new RuntimeException("Minio 初始化失败", e);
}
}
// 其余上传、下载、签名 URL 等方法与清理功能无关,此处省略。
/**
* 生成唯一文件名(带日期路径 + UUID)
*/
private String generateUniqueFilename(String extension) {
String dateFormat = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
String uuid = UUID.randomUUID().toString().replace("-", "");
return dateFormat + "/" + uuid + extension;
}
/**
* 删除早于指定日期的所有日期目录(yyyy-MM-dd/)
*
* @param endExclusive 截止日期(不含)
* @return 实际删除的对象总数
*/
public int deleteDateFoldersBefore(LocalDate endExclusive) {
if (endExclusive == null) {
throw new IllegalArgumentException("指定日期不能为空");
}
LocalDate today = LocalDate.now();
if (!endExclusive.isBefore(today)) {
return 0;
}
int totalDeleted = 0;
for (LocalDate d = endExclusive.minusDays(1); !d.isBefore(retainSince); d = d.minusDays(1)) {
totalDeleted += deleteSingleFolder(d.format(DateTimeFormatter.ISO_LOCAL_DATE) + "/");
}
return totalDeleted;
}
/**
* 删除单个目录(前缀)下的全部对象
*/
private int deleteSingleFolder(String prefix) {
try {
List<DeleteObject> objects = new ArrayList<>();
minioClient.listObjects(ListObjectsArgs.builder()
.bucket(bucketName)
.prefix(prefix)
.recursive(true)
.build())
.forEach(r -> {
try {
objects.add(new DeleteObject(r.get().objectName()));
} catch (Exception ignored) {
log.warn("文件名获取失败");
}
});
if (objects.isEmpty()) {
return 0;
}
Iterable<Result<DeleteError>> results = minioClient.removeObjects(
RemoveObjectsArgs.builder()
.bucket(bucketName)
.objects(objects)
.build());
for (Result<DeleteError> res : results) {
DeleteError deleteError = res.get();
}
return objects.size();
} catch (Exception e) {
log.warn("删除目录 {} 失败: {}", prefix, e.toString());
return 0;
}
}
}
1.3.1 性能陷阱
迭代器懒加载机制
listObjects 返回分页迭代器(每 1000 条自动分页),无需手动处理 marker
removeObjects 返回 Iterable<Result<DeleteError>>,需遍历结果才能触发 HTTP 请求(懒执行)
- 海量对象场景:添加
.maxKeys(batchSize) 限制单次返回数量,避免 OOM
异常隔离
r.get() 可能抛出 InsufficientDataException/InternalException 等异常
- 处理策略:捕获异常后仅记录日志,确保当前批次继续执行
批大小限制
- MinIO 服务端单次请求上限:1000 条对象
- 超限错误:
ErrorResponseException: DeleteObjects max keys 1000
1.3.2 幂等性设计
- 重复删除同一路径:静默忽略,不报错
- 已删除对象:自动跳过处理
1.3.3 常见错误码处理
| 错误码 |
原因 |
解决方案 |
NoSuchBucket |
桶名错误 |
启动时校验桶是否存在 |
AccessDenied |
AK/SK 权限不足 |
补充 s3:DeleteObject 权限 |
SlowDown |
服务端限流 |
指数退避重试策略 |
2、测试类:MinioTest
import com.fc.utils.MinioUtil;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDate;
@SpringBootTest
public class MinioTest {
@Autowired
private MinioUtil minioUtil;
@Test
public void testDelete() {
int count = minioUtil.deleteDateFoldersBefore(LocalDate.of(2025, 8, 2));
System.out.println(count);
}
}
三、定时配置
在 Spring Boot 中,我们可以通过注解轻松实现定时任务的调度。下面介绍具体的配置步骤。
1. 注解实现
1.1 在主启动类添加 @EnableScheduling 开启定时任务支持

@SpringBootApplication
@EnableTransactionManagement // 开启注解方式的事务管理
@EnableScheduling // 开启定时任务
@EnableCaching
public class VehicleApplication {
public static void main(String[] args) {
SpringApplication.run(VehicleApplication.class, args);
}
}
1.2 创建定时任务类
使用 @Component 注册 Bean,在方法上添加 @Scheduled:
import com.fc.utils.MinioUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
@Component // 注册Bean
@RequiredArgsConstructor
@Slf4j
public class MinioCleanTask {
private final MinioUtil minioUtil;
/**
* 定时清理 MinIO 中早于当前日期的日期目录(格式:yyyy-MM-dd/)
* 执行时间:每月1号凌晨3点
*/
@Scheduled(cron = "0 0 3 1 * ?")
public void minioClean() {
try {
log.info("MinIO 清理任务开始执行...");
// 明确语义:删除早于今天的所有日期目录(不含今天)
LocalDate today = LocalDate.now();
log.info("当前日期:{}, 开始清理早于该日期的目录", today);
int deleteCount = minioUtil.deleteDateFoldersBefore(today);
log.info("MinIO 清理任务执行完成,共删除 {} 张图片", deleteCount);
} catch (Exception e) {
// 防止定时任务因异常停止
log.error("MinIO 清理任务执行失败", e);
}
}
}
启动类需要能扫描到定时任务类,否则定时任务启动不起来(启动类位置位于定时任务类之上或者通过注解指定扫描包的位置)。

1.3 @Scheduled 参数详解
| 参数 |
作用 |
特点 |
示例 |
fixedRate |
上一次开始时间到下一次开始时间的间隔(毫秒) |
无视任务执行时长 |
@Scheduled(fixedRate = 3000) // 每3秒执行一次 |
fixedDelay |
上一次结束时间到下一次开始时间的间隔(毫秒) |
等待上次任务完成 |
@Scheduled(fixedDelay = 4000) // 任务结束后4秒再执行 |
initialDelay |
首次任务延迟时间(需配合 fixedRate/fixedDelay) |
仅首次生效 |
@Scheduled(initialDelay = 10000, fixedRate = 5000) // 首次延迟10秒,之后每5秒执行 |
cron |
通过表达式定义复杂时间规则 |
支持灵活的时间组合 |
"0 15 10 * * ?" // 每天10:15执行 |
1.4 Cron 表达式详解
Cron 表达式由 5-7 个字段组成,每个字段代表一个时间单位,字段之间用空格分隔。
1.4.1 常见格式
| 字段数 |
格式 |
使用场景 |
| 5 位 |
分 时 日 月 周 |
Linux Crontab |
| 6 位 |
秒 分 时 日 月 周 |
Spring/Quartz |
| 7 位 |
秒 分 时 日 月 周 年 |
AWS EventBridge |
1.4.2 字段说明(以 6 位格式为例)
| 位置 |
字段 |
取值范围 |
允许的特殊字符 |
| 1 |
秒 |
0-59 |
, - * / |
| 2 |
分 |
0-59 |
, - * / |
| 3 |
小时 |
0-23 |
, - * / |
| 4 |
日期 |
1-31 |
, - * / ? L W |
| 5 |
月份 |
1-12 或 JAN-DEC |
, - * / |
| 6 |
星期 |
0-7 或 SUN-SAT(0 和 7 均为星期日) |
, - * / ? L # |
| 7 |
年份(可选) |
1970-2099 |
, - * / |
1.4.3 特殊字符详解
| 字符 |
含义 |
示例说明 |
* |
任意值 |
在“小时”字段表示“每小时” |
? |
不指定值 |
用于“日期”和“星期”字段互斥使用 |
- |
范围 |
9-17 表示从 9 到 17 |
, |
枚举值 |
1,3,5 表示 1、3、5 |
/ |
增量 |
0/15 表示从 0 开始,每 15 秒一次 |
L |
Last(最后) |
L 在“日期”中表示“当月最后一天” |
W |
工作日 |
15W 表示“离 15 号最近的工作日” |
# |
第几个星期几 |
6#3 表示“当月第 3 个星期五” |
1.4.4 常用示例
基础定时任务
| 执行频率 |
Cron表达式 |
说明 |
| 每分钟执行一次 |
0 * * * * ? |
每分钟的第0秒触发 |
| 每5分钟执行一次 |
0 */5 * * * ? |
每隔5分钟的第0秒触发 |
| 每小时第30分钟执行 |
0 30 * * * ? |
每小时的30分0秒触发 |
| 每天凌晨1点执行 |
0 0 1 * * ? |
每天1:00:00触发 |
| 每月1号凌晨2点执行 |
0 0 2 1 * ? |
每月1日的2:00:00触发 |
| 每周六凌晨3点执行 |
0 0 3 * * 6 |
每周六的3:00:00触发(数字6表示周六) |
| 每周六凌晨3点执行 |
0 0 3 * * SAT |
每周六的3:00:00触发(SAT为英文缩写) |
高级用法
| 需求描述 |
Cron表达式 |
说明 |
| 每月最后一天23:59执行 |
0 59 23 L * ? |
L 表示月份的最后一天 |
| 每月15号或最后一天执行 |
0 0 0 15,L * ? |
逗号分隔多个日期 |
| 每月第2个星期一 |
0 0 0 ? * 2#2 |
2#2 表示第2周的周一 |
| 工作日(周一到周五)9点执行 |
0 0 9 * * MON-FRI |
MON-FRI 定义范围 |
| 每10秒执行一次 |
*/10 * * * * ? |
*/10 表示秒字段的步长 |
1.4.5 在线验证工具
推荐使用以下工具测试 Cron 表达式:
2. 配置线程池
当系统中有多个定时任务时,默认情况下它们共享同一个单线程。如果某个任务执行时间过长,会导致其他任务延迟执行。通过配置线程池可以:
- 实现任务隔离
- 提高任务并行性
- 避免任务阻塞
- 提供更好的资源控制
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@Configuration
@Slf4j
public class SchedulerConfiguration {
/**
* 配置定时任务线程池
*
* @return 任务调度器实例
*/
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
// 核心参数配置
scheduler.setPoolSize(3); // 线程池大小,建议设置为任务数+2
scheduler.setThreadNamePrefix("minio-scheduler-"); // 线程名前缀
scheduler.setAwaitTerminationSeconds(60); // 关闭时等待任务完成的时间(秒)
scheduler.setWaitForTasksToCompleteOnShutdown(true); // 关闭时是否等待任务完成
scheduler.setRemoveOnCancelPolicy(true); // 取消任务时是否立即移除
scheduler.setErrorHandler(throwable ->
log.error("定时任务执行异常", throwable)); // 异常处理器
// 任务拒绝策略配置
scheduler.setRejectedExecutionHandler((r, executor) -> {
log.warn("定时任务被拒绝,任务队列已满");
// 可添加自定义处理逻辑,如记录日志或发送告警
});
return scheduler;
}
}
2.1 线程池参数详解
| 参数 |
类型 |
默认值 |
说明 |
poolSize |
int |
1 |
线程池大小,决定同时执行的任务数量 |
threadNamePrefix |
String |
“scheduler-” |
线程名前缀,方便日志跟踪和调试 |
awaitTerminationSeconds |
int |
0 |
应用关闭时等待任务完成的秒数,0表示不等待 |
waitForTasksToCompleteOnShutdown |
boolean |
false |
是否等待计划任务完成再关闭线程池 |
removeOnCancelPolicy |
boolean |
false |
取消任务时是否立即从队列中移除该任务 |
errorHandler |
ErrorHandler |
null |
任务执行异常时的处理器,用于自定义异常处理逻辑 |
rejectedExecutionHandler |
RejectedExecutionHandler |
AbortPolicy |
任务被拒绝时的处理策略,默认直接抛出异常 |
2.2 线程池任务拒绝策略
| 策略名称 |
行为描述 |
适用场景 |
AbortPolicy (默认) |
直接抛出 RejectedExecutionException,中断任务提交流程 |
需要严格保证任务不丢失的场景,需显式处理异常 |
CallerRunsPolicy |
由提交任务的调用者线程直接执行被拒绝的任务(线程池未关闭时) |
需降低任务提交速度,避免完全阻塞的场景 |
DiscardPolicy |
静默丢弃被拒绝的任务,不触发任何异常或通知 |
允许丢弃部分非关键任务的场景 |
DiscardOldestPolicy |
丢弃任务队列中最早未处理的任务,并尝试重新提交当前任务(可能仍被拒绝) |
允许牺牲旧任务以优先处理新任务的场景 |
3. 异步执行
当定时任务包含阻塞操作(如网络IO、复杂计算)时,应使用异步模式避免阻塞调度线程。
3.1 异步线程池配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync // 开启异步支持
public class AsyncConfiguration {
/**
* 创建异步任务线程池
*/
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心参数
executor.setCorePoolSize(3);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-task-");
executor.setKeepAliveSeconds(60);
// 拒绝策略:由调用线程处理(避免任务丢失)
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 优雅停机配置
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
}
3.2 异步线程池配置建议
3.2.1 队列选择策略
| 队列类型 |
特点 |
适用场景 |
SynchronousQueue |
无容量 |
高吞吐、短任务 |
LinkedBlockingQueue |
无界队列 |
保证任务不丢失 |
ArrayBlockingQueue |
有界队列 |
资源受限环境 |
PriorityBlockingQueue |
优先级队列 |
任务优先级处理 |
3.2.2 拒绝策略选择
// 常用拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); // 抛出异常
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy()); // 静默丢弃
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 调用者执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy()); // 丢弃最旧任务
3.2.3 动态配置
// 运行时调整核心参数
executor.setCorePoolSize(10);
executor.setMaxPoolSize(30);
executor.setQueueCapacity(200);
3.3 应用
在定时任务上添加 @Async,标明该任务异步执行(所在类必须被 Spring 管理)。

/**
* 定时清理 MinIO 中早于当前日期的日期目录(格式:yyyy-MM-dd/)
* 执行时间:每月1号凌晨3点
*/
@Async(value = "taskExecutor") // 添加异步注解
@Scheduled(cron = "0 0 3 1 * ?")
public void minioClean() {
try {
log.info("MinIO 清理任务开始执行...");
// 明确语义:删除早于今天的所有日期目录(不含今天)
LocalDate today = LocalDate.now();
log.info("当前日期:{}, 开始清理早于该日期的目录", today);
int deleteCount = minioUtil.deleteDateFoldersBefore(today);
log.info("MinIO 清理任务执行完成,共删除 {} 张图片", deleteCount);
} catch (Exception e) {
// 防止定时任务因异常停止
log.error("MinIO 清理任务执行失败", e);
}
}
4. 扩展
4.1 配置文件优化
4.1.1 添加配置信息
minio:
clean:
enabled: true # 是否启用清理功能
retain-days: 1 # 保留最近多少天的文件
earliest-date: "2025/08/01" # 最早保留日期(避免误删重要文件)
cron: "0 0 2 * * ?" # 每天凌晨2点执行
4.1.2 配置类映射
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
@Component
@Data
@ConfigurationProperties(prefix = "vehicle.minio.clean")
public class MinioCleanProperties {
private boolean enabled;
private int retainDays;
private LocalDate earliestDate;
private String cron;
}
4.1.3 修改 MinioUtil
修改 MinioUtil,移除硬编码的 retainSince,改为从配置类动态获取。

核心修改点:
private final MinioProperties minioProperties;
private final MinioCleanProperties minioCleanProperties; // 添加映射配置类
private MinioClient minioClient;
private String bucketName;
private LocalDate retainSince;
@PostConstruct
public void init() {
try {
minioClient = MinioClient.builder()
.endpoint(minioProperties.getUrl())
.credentials(minioProperties.getUsername(), minioProperties.getPassword())
.build();
bucketName = minioProperties.getBucketName();
retainSince = minioCleanProperties.getEarliestDate(); // 获取动态参数
// 检查桶是否存在,不存在则创建
boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!bucketExists) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
} catch (Exception e) {
throw new RuntimeException("Minio 初始化失败", e);
}
}
其余方法与之前相同,此处不再重复。
4.1.4 修改 MinioCleanTask

import com.fc.properties.MinioCleanProperties;
import com.fc.utils.MinioUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
@Component // 注册Bean
@RequiredArgsConstructor
@Slf4j
public class MinioCleanTask {
private final MinioUtil minioUtil;
private final MinioCleanProperties minioCleanProperties;
/**
* 定时清理 MinIO 中早于当前日期的日期目录(格式:yyyy-MM-dd/)
*/
@Async(value = "taskExecutor") // 添加异步注解
@Scheduled(cron = "#{@minioCleanProperties.cron}")
public void minioClean() {
try {
if (!minioCleanProperties.isEnabled()) {
log.warn("清理【Minio】图片定时任务已关闭");
return;
}
log.info("MinIO 清理任务开始执行...");
LocalDate cutoff = LocalDate.now().minusDays(minioCleanProperties.getRetainDays()); // 获取保留自定义天数的时间日期
log.info("清理截止日期:{},最早保留日期:{}", cutoff, minioCleanProperties.getEarliestDate());
int deleteCount = minioUtil.deleteDateFoldersBefore(cutoff);
log.info("MinIO 清理任务执行完成,共删除 {} 张图片", deleteCount);
} catch (Exception e) {
// 防止定时任务因异常停止
log.error("MinIO 清理任务执行失败", e);
}
}
}
@Scheduled(cron = "#{@minioCleanProperties.cron}") 让 @Scheduled 注解的 cron 表达式从 Spring 容器里的某个 Bean 中动态取值,而不是写死在代码里。
| 片段 |
含义 |
@Scheduled(cron = ...) |
Spring 定时任务的注解,指定 cron 表达式 |
#{} |
SpEL(Spring 表达式语言),允许在注解里写动态表达式 |
@minioCleanProperties |
从 Spring 容器里按 Bean 名称取出对应的 Bean |
.cron |
取出该 Bean 的 getCron() 方法返回的字符串,即 cron 表达式 |
4.1.5 测试
修改依赖配置文件:
minio:
clean:
enabled: true # 是否启用清理功能
retain-days: 5 # 保留最近多少天的文件
earliest-date: "2025/08/01" # 最早保留日期(避免误删重要文件)
cron: "0/5 * * * * ?" # 每五秒执行一次

4.2 接口方法
4.2.1 建表和插入数据
CREATE TABLE corn (
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
`enabled` TINYINT NOT NULL COMMENT '是否开启定时任务,1-开启,0-关闭',
`retain_days` INT NOT NULL COMMENT '保留最近多少天的文件',
`earliest_date` DATE NOT NULL COMMENT '最早保留日期',
`corn` VARCHAR ( 20 ) NOT NULL COMMENT 'CORN表达式(触发时间)',
`create_time` DATETIME COMMENT '创建时间',
`update_time` DATETIME COMMENT '更新时间'
) ENGINE = INNODB DEFAULT CHARSET = UTF8MB4;
insert into corn values (1, true, 90, '2025/08/01', '0/5 * * * * ?', NOW(), NOW());

4.2.2 创建实体
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CornDTO {
private long id;
private boolean enabled;
private int retainDays;
private LocalDate earliestDate;
private String corn;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
4.2.3 Mapper 接口
import com.fc.dto.CornDTO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface CornMapper {
@Select("select * from corn where id=#{id}")
CornDTO selectCornById(long id);
}



关闭数据库的打印信息后,观察日志输出:

可以看到定时清理任务按照预期执行,非常顺畅。
参考来源:原文链接