定时任务与异步
约 1356 字大约 5 分钟
布欧-Lewyon
2026-05-15
首页 › Spring Boot › 任务管理 › 定时任务与异步
Spring Boot 应用内即可实现轻量级的定时调度和异步方法调用,无需额外引入 Quartz 等任务框架。
开启调度
@SpringBootApplication
@EnableScheduling // 开启定时任务
@EnableAsync // 开启异步方法支持
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}@Scheduled 定时任务
@Component
public class ScheduledTasks {
private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);
// 固定延迟:上次执行完成后等 5 秒再执行下一次
@Scheduled(fixedDelay = 5000)
public void reportCurrentTime() {
log.info("当前时间: {}", LocalDateTime.now());
}
// 固定速率:每 5 秒执行一次(不管上次是否完成)
@Scheduled(fixedRate = 5000)
public void periodicTask() {
log.info("固定速率任务");
}
// 首次延迟 10 秒后开始
@Scheduled(fixedRate = 5000, initialDelay = 10000)
public void delayedTask() { ... }
// Cron 表达式:每天凌晨 2 点执行
@Scheduled(cron = "0 0 2 * * ?")
public void midnightCleanup() {
log.info("执行每日清理任务");
}
}Cron 表达式示例
格式: 秒 分 时 日 月 星期
0 0 2 * * ? 每天 02:00
0 0/5 * * * ? 每 5 分钟
0 0 9-18 * * MON-FRI 工作日 9:00-18:00 每小时
0 0 0 1 * ? 每月 1 号凌晨Spring Cron 表达式与 Unix/Linux Cron 最主要的区别:Spring 的 cron 有 6 位(第一位是秒),标准 Unix 只有 5 位。
@Async 异步方法
将耗时操作放到独立线程执行,不阻塞调用方:
@Component
public class EmailService {
@Async
public Future<Boolean> sendWelcomeEmail(Long userId) {
// 模拟耗时操作
try {
Thread.sleep(2000);
log.info("欢迎邮件发送成功, userId={}", userId);
return new AsyncResult<>(true);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return new AsyncResult<>(false);
}
}
}调用方:
@Service
public class UserService {
@Autowired
private EmailService emailService;
public User createUser(UserCreateRequest request) {
User user = userRepository.save(new User(request));
// 异步发邮件,不阻塞用户创建流程
Future<Boolean> future = emailService.sendWelcomeEmail(user.getId());
// 如果需要等待结果:
// Boolean result = future.get(5, TimeUnit.SECONDS);
return user;
}
}线程池配置
Spring Boot 默认的 @Async 使用 SimpleAsyncTaskExecutor,不重用线程。建议配置自定义线程池:
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) ->
log.error("异步方法执行失败: {}#{}", method.getDeclaringClass(), method.getName(), ex);
}
}定时任务的线程池配置:
@Configuration
public class SchedulingConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5);
scheduler.setThreadNamePrefix("scheduled-");
return scheduler;
}
}源码与架构:@EnableAsync 与 @EnableScheduling 的内部机制
@Async 代理创建
@EnableAsync 通过 @Import(AsyncConfigurationSelector.class) 导入 ProxyAsyncConfiguration,后者注册 AsyncAnnotationBeanPostProcessor——这是一个 BeanPostProcessor,在 Bean 初始化后检查其方法上是否有 @Async:
AsyncAnnotationBeanPostProcessor.postProcessAfterInitialization(bean, beanName)
│
└── AbstractAdvisingBeanPostProcessor.postProcessAfterInitialization()
├── ① 获取该 Bean 的所有 Method + Class 级别的 @Async 元数据
├── ② 存在 @Async 且未被代理?→ 创建 AOP 代理
│ └── ProxyFactory.getProxy(classLoader, advisor)
│ └── 添加 AsyncExecutionInterceptor 作为增强
│
└── AsyncExecutionInterceptor.invoke(MethodInvocation)
├── ① 确定 AsyncTaskExecutor
│ └── 从 @Async("beanName") 获取指定线程池
│ └── 未指定 → 使用默认 executor
├── ② 包装为 Callable<Object>
├── ③ 提交到线程池
│ ├── 返回 void → 直接返回 null
│ ├── 返回 Future → 返回 asyncResult
│ └── 返回 CompletableFuture → 通过 Delegate 包装
└── ④ 异常处理(AsyncUncaughtExceptionHandler)@Scheduled 注册流程
@EnableScheduling 通过 @Import(SchedulingConfiguration.class) 注册 ScheduledAnnotationBeanPostProcessor。它不是基于 AOP 代理,而是通过后处理器扫描:
ScheduledAnnotationBeanPostProcessor
│
├── postProcessAfterInitialization(bean, beanName) ← Bean 实例化后
│ └── processScheduled(scheduledMethod, bean)
│ ├── 解析 @Scheduled(fixedDelay / fixedRate / cron / initialDelay)
│ ├── 构建 Runnable 包装(Callable 方式调用目标方法)
│ └── 注册到待调度任务列表
│
├── afterSingletonsInstantiated() ← 所有单例 Bean 创建完后
│ └── finishRegistration()
│ └── 遍历所有已注册的 ScheduledTaskRegistrar
│ └── 将任务注册到 TaskScheduler(默认 ThreadPoolTaskScheduler)
│ ├── TriggerTask → scheduler.schedule(runnable, trigger)
│ └── CronTask → scheduler.schedule(runnable, new CronTrigger(cron))
│
└── 返回 ScheduledTask 注册表(可后续取消)生产意识:
@Async和@Scheduled都不能直接在同一个类内部调用生效(AOP 代理限制),但机制不同:@Async需要 AOP 代理(方法必须通过代理调用),@Scheduled通过ScheduledAnnotationBeanPostProcessor在后处理器阶段就已捕获到方法引用——它不依赖代理,而是直接包装原始 Bean 的方法引用为 Runnable。这也是为什么同类内this.someAsyncMethod()不生效,但this.someScheduledMethod()实际上会被触发——因为@Scheduled在afterSingletonsInstantiated阶段就已注册了执行器,后续无论通过代理还是原始 Bean 调用,执行器都已在调度表中。
@EnableScheduling+@Scheduled实现轻量定时任务(fixedDelay / fixedRate / cron)。@EnableAsync+@Async异步执行方法,不阻塞调用方。- 自定义
AsyncConfigurer配置线程池,避免默认的SimpleAsyncTaskExecutor。 - 定时任务线程池通过
TaskSchedulerBean 配置。 - 易错:
@Async与@Transactional一样,同一个类内部调用不生效(AOP 代理限制)。@Async方法的返回值必须是void或Future类型(AsyncResult或CompletableFuture)。定时任务默认单线程,若一个任务阻塞会影响其他定时任务,务必配置线程池。 - 生产意识:定时任务在分布式部署下会产生多实例重复执行问题——如果有多台机器同时运行定时任务,每个实例都会触发同一段逻辑(如清理、对账)。解决方案:①使用分布式锁(如 Redis SETNX)做任务争抢;②使用
@SchedulerLock(配合 ShedLock 库)做去重。对于 @Async 方法,异常处理非常关键——如果异步方法抛出了未捕获的异常,默认的SimpleAsyncTaskExecutor不会打印堆栈(@Async没有设置AsyncUncaughtExceptionHandler),会导致异常静默消失。务必实现AsyncConfigurer.getAsyncUncaughtExceptionHandler()记录异常。 - 思考任务:编写一个定时任务每天凌晨清理过期数据,编写一个异步方法发送通知短信,配置合适的线程池参数验证异步执行。
上一节:缓存抽象与 Redis 集成
下一节:日志
