MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java编程中的Spring Boot定时任务

2023-07-213.5k 阅读

Spring Boot 定时任务简介

在企业级应用开发中,常常需要执行一些周期性的任务,比如每天凌晨备份数据库、定时清理过期数据等。Spring Boot 提供了非常便捷的方式来实现定时任务,使得开发者可以专注于业务逻辑,而不必过多关注任务调度的底层实现细节。

Spring Boot 对定时任务的支持基于 Spring 框架的 @Scheduled 注解,它允许我们轻松地将方法标记为定时执行的任务。这种方式简单直观,大大降低了定时任务开发的难度。

引入依赖

要在 Spring Boot 项目中使用定时任务功能,首先需要确保项目的 pom.xml 文件中引入了 Spring Boot Starter 相关依赖。如果是基于 Maven 构建的项目,在 pom.xml 中添加以下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter - scheduling</artifactId>
</dependency>

spring-boot-starter - scheduling 依赖提供了定时任务相关的功能。当引入这些依赖后,Spring Boot 会自动配置相关的定时任务基础设施。

启用定时任务

在 Spring Boot 应用中启用定时任务非常简单,只需要在主应用类上添加 @EnableScheduling 注解即可。例如:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@EnableScheduling 注解告诉 Spring Boot 要开启定时任务功能,扫描带有 @Scheduled 注解的方法,并将其注册为定时任务。

使用 @Scheduled 注解

@Scheduled 注解是 Spring Boot 定时任务的核心,它可以应用在方法上,定义该方法的执行时间规则。@Scheduled 注解有几个常用的属性:

fixedRate 属性

fixedRate 属性定义了任务执行的频率,单位是毫秒。它表示上一次任务开始执行到下一次任务开始执行的时间间隔。例如:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class FixedRateTask {
    private static final Logger logger = LoggerFactory.getLogger(FixedRateTask.class);

    @Scheduled(fixedRate = 5000)
    public void runTask() {
        logger.info("FixedRateTask is running...");
    }
}

在上述代码中,runTask 方法每隔 5000 毫秒(即 5 秒)就会执行一次。无论上一次任务执行时间长短,都会按照固定的时间间隔启动下一次任务。如果上一次任务执行时间超过了 fixedRate 设置的时间,下一次任务会在上一次任务执行完后立即启动。

fixedDelay 属性

fixedDelay 属性定义了任务执行的延迟时间,单位也是毫秒。它表示上一次任务执行结束到下一次任务开始执行的时间间隔。例如:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class FixedDelayTask {
    private static final Logger logger = LoggerFactory.getLogger(FixedDelayTask.class);

    @Scheduled(fixedDelay = 5000)
    public void runTask() {
        logger.info("FixedDelayTask is running...");
    }
}

在这个例子中,runTask 方法在上一次执行结束后,会等待 5000 毫秒(5 秒)才开始下一次执行。如果任务执行时间较短,那么两次任务执行之间的间隔就是 fixedDelay 设置的时间;如果任务执行时间较长,那么下一次任务启动时间会相应推迟。

initialDelay 属性

initialDelay 属性用于指定任务首次执行的延迟时间,单位同样是毫秒。它通常与 fixedRatefixedDelay 一起使用。例如:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class InitialDelayTask {
    private static final Logger logger = LoggerFactory.getLogger(InitialDelayTask.class);

    @Scheduled(initialDelay = 3000, fixedRate = 5000)
    public void runTask() {
        logger.info("InitialDelayTask is running...");
    }
}

在上述代码中,runTask 方法在应用启动后会延迟 3000 毫秒(3 秒)执行第一次,之后每隔 5000 毫秒(5 秒)执行一次。

cron 属性

cron 属性使用 Cron 表达式来定义任务的执行时间,Cron 表达式是一个字符串,由 6 或 7 个空格分隔的时间字段组成,每个字段代表不同的时间单位。Cron 表达式的格式为:秒 分 时 日 月 周 年(可选)。例如,要定义一个每天凌晨 2 点执行的任务,可以这样写:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class CronTask {
    private static final Logger logger = LoggerFactory.getLogger(CronTask.class);

    @Scheduled(cron = "0 0 2 * * *")
    public void runTask() {
        logger.info("CronTask is running...");
    }
}

在这个例子中,cron 表达式 0 0 2 * * * 表示在每天的 2 点 0 分 0 秒执行任务。Cron 表达式非常灵活,可以满足各种复杂的时间调度需求。例如,0 0/5 * * * * 表示每隔 5 分钟执行一次任务;0 15 10 * * MON-FRI 表示在周一到周五的 10 点 15 分执行任务。

多线程定时任务

默认情况下,Spring Boot 定时任务是单线程执行的。如果有多个定时任务,并且某些任务执行时间较长,可能会导致其他任务延迟执行。为了避免这种情况,可以启用多线程定时任务。

首先,需要创建一个配置类来配置线程池。例如:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

@Configuration
@EnableScheduling
public class SchedulingConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(10);
        scheduler.setThreadNamePrefix("MyScheduled-");
        scheduler.initialize();
        taskRegistrar.setTaskScheduler(scheduler);
    }

    @Bean
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(10);
        scheduler.setThreadNamePrefix("MyScheduled-");
        scheduler.initialize();
        return scheduler;
    }
}

在上述配置类中,通过 ThreadPoolTaskScheduler 创建了一个线程池,设置线程池大小为 10,并将其注册到 ScheduledTaskRegistrar 中。这样,Spring Boot 的定时任务就会使用这个线程池来并行执行任务,提高任务执行的效率。

异常处理

在定时任务执行过程中,可能会发生各种异常。如果不进行适当的处理,异常可能会导致任务中断,影响系统的稳定性。Spring Boot 提供了几种方式来处理定时任务中的异常。

一种简单的方式是在任务方法中使用 try-catch 块来捕获异常,并进行相应的处理。例如:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class ExceptionHandlingTask {
    private static final Logger logger = LoggerFactory.getLogger(ExceptionHandlingTask.class);

    @Scheduled(fixedRate = 5000)
    public void runTask() {
        try {
            // 任务逻辑
            int result = 10 / 0; // 模拟异常
            logger.info("Task executed successfully: {}", result);
        } catch (Exception e) {
            logger.error("Task execution failed", e);
        }
    }
}

在上述代码中,通过 try-catch 块捕获了任务执行过程中可能出现的异常,并记录了错误日志,这样任务不会因为异常而中断。

另一种方式是通过自定义异常处理器来处理定时任务的异常。首先,创建一个自定义的异常处理器类,实现 SchedulingConfigurer 接口,并覆盖 configureTasks 方法,在其中设置异常处理器。例如:

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@Configuration
@EnableScheduling
public class CustomExceptionHandlingConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(taskExecutor());
        taskRegistrar.setErrorHandler(task -> {
            System.err.println("Task execution failed: " + task.getThrowable());
        });
    }

    private ScheduledExecutorService taskExecutor() {
        return Executors.newScheduledThreadPool(10);
    }
}

在上述代码中,通过 taskRegistrar.setErrorHandler 设置了一个全局的异常处理器,当定时任务发生异常时,会调用这个异常处理器进行处理。

动态定时任务

在实际应用中,有时需要根据运行时的条件动态调整定时任务的执行时间。Spring Boot 也支持动态定时任务的实现。

一种常见的方式是通过配置文件来动态修改定时任务的 Cron 表达式。首先,在 application.properties 文件中添加一个属性来存储 Cron 表达式,例如:

task.cron=0 0/5 * * * *

然后,创建一个定时任务类,并使用 @Value 注解注入这个属性值,同时通过 CronTrigger 来动态更新任务的执行时间。例如:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class DynamicTask implements SchedulingConfigurer {
    private static final Logger logger = LoggerFactory.getLogger(DynamicTask.class);

    @Value("${task.cron}")
    private String cronExpression;

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.addTriggerTask(
                () -> {
                    logger.info("DynamicTask is running...");
                },
                triggerContext -> {
                    CronTrigger trigger = new CronTrigger(cronExpression);
                    return trigger.nextExecutionTime(triggerContext);
                }
        );
    }
}

在上述代码中,通过 CronTrigger 根据注入的 cronExpression 动态生成任务的执行时间。当 application.properties 文件中的 task.cron 属性值发生变化时,任务的执行时间也会相应改变。

与数据库结合的定时任务

在企业级应用中,定时任务常常需要与数据库进行交互,比如定时从数据库中读取数据进行处理,或者定时将处理结果写入数据库。下面以定时备份数据库为例,介绍如何实现与数据库结合的定时任务。

假设我们使用 MySQL 数据库,并且项目中已经配置好了数据库连接和相关的持久化层框架(如 Spring Data JPA)。首先,创建一个备份数据库的方法,例如:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.persistence.Query;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

@Service
public class DatabaseBackupService {
    private static final Logger logger = LoggerFactory.getLogger(DatabaseBackupService.class);

    @Autowired
    private EntityManager entityManager;

    @Transactional
    public void backupDatabase() {
        try {
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
            String backupFileName = "database_backup_" + dateFormat.format(new Date()) + ".sql";

            // 获取数据库连接
            java.sql.Connection connection = entityManager.unwrap(java.sql.Connection.class);

            // 执行数据库备份命令
            String command = "mysqldump -u root -p[password] [database_name] > " + backupFileName;
            Process process = Runtime.getRuntime().exec(command);

            // 处理备份结果
            int exitCode = process.waitFor();
            if (exitCode == 0) {
                logger.info("Database backup successful: {}", backupFileName);
            } else {
                logger.error("Database backup failed with exit code: {}", exitCode);
            }
        } catch (IOException | InterruptedException e) {
            logger.error("Database backup error", e);
        }
    }
}

在上述代码中,backupDatabase 方法通过执行 mysqldump 命令将数据库备份到一个 SQL 文件中。然后,创建一个定时任务类来调用这个备份方法,例如:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class DatabaseBackupTask {
    private static final Logger logger = LoggerFactory.getLogger(DatabaseBackupTask.class);

    @Autowired
    private DatabaseBackupService databaseBackupService;

    @Scheduled(cron = "0 0 2 * * *")
    public void runTask() {
        logger.info("Database backup task is starting...");
        databaseBackupService.backupDatabase();
        logger.info("Database backup task completed.");
    }
}

在这个定时任务类中,runTask 方法在每天凌晨 2 点调用 DatabaseBackupServicebackupDatabase 方法来执行数据库备份任务。

分布式定时任务

在分布式系统中,多个节点可能都部署了相同的 Spring Boot 应用,默认情况下,每个节点上的定时任务都会独立执行,这可能会导致任务重复执行。为了避免这种情况,需要实现分布式定时任务。

一种常见的实现方式是使用 Redis 来实现分布式锁。首先,引入 Redis 相关依赖,例如:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

然后,创建一个 Redis 分布式锁工具类,例如:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
public class RedisLockUtil {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    public boolean tryLock(String lockKey, String requestId, long expireTime) {
        return redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expireTime, TimeUnit.SECONDS);
    }

    public void unlock(String lockKey, String requestId) {
        if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) {
            redisTemplate.delete(lockKey);
        }
    }
}

在定时任务类中使用这个分布式锁工具类来确保只有一个节点执行任务,例如:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.UUID;

@Component
public class DistributedTask {
    private static final Logger logger = LoggerFactory.getLogger(DistributedTask.class);

    @Autowired
    private RedisLockUtil redisLockUtil;

    private static final String LOCK_KEY = "distributed_task_lock";
    private static final long LOCK_EXPIRE_TIME = 30; // 锁过期时间,单位秒

    @Scheduled(cron = "0 0/5 * * * *")
    public void runTask() {
        String requestId = UUID.randomUUID().toString();
        boolean locked = redisLockUtil.tryLock(LOCK_KEY, requestId, LOCK_EXPIRE_TIME);
        if (locked) {
            try {
                logger.info("DistributedTask is running...");
                // 任务逻辑
            } finally {
                redisLockUtil.unlock(LOCK_KEY, requestId);
            }
        } else {
            logger.info("DistributedTask is skipped, another instance is running.");
        }
    }
}

在上述代码中,runTask 方法在执行任务前先尝试获取 Redis 分布式锁,如果获取成功则执行任务,执行完后释放锁;如果获取锁失败,则说明其他节点正在执行任务,当前节点跳过任务执行。

总结

Spring Boot 的定时任务功能为企业级应用开发提供了强大且便捷的任务调度能力。通过简单的注解和配置,开发者可以轻松实现各种定时任务需求,包括固定频率执行、基于 Cron 表达式的复杂调度、多线程执行、异常处理、动态任务调整、与数据库结合以及分布式任务等。在实际项目中,根据具体的业务场景和需求,合理选择和运用这些功能,可以提高系统的稳定性和效率,实现更加智能化和自动化的业务流程。希望通过本文的介绍,读者对 Spring Boot 定时任务有更深入的理解和掌握,能够在实际开发中灵活运用。