原文路徑:https://zhuanlan.zhihu.com/p/79644891
在日常的項目開發中,往往會涉及到一些需要做到定時執行的代碼,例如自動將超過24小時的未付款的單改為取消狀態,自動將超過14天客戶未簽收的訂單改為已簽收狀態等等,那么為了在Spring Boot中實現此類需求,我們要怎么做呢?
Spring Boot早已考慮到了這類情況,先來看看要怎么做。第一種方式是比較簡單的,先搭建好Spring Boot微服務,加上這個注解 @EnableScheduling :
/** * @author yudong * @date 2019/8/24 */ @EnableScheduling // 開啟定時任務功能 @ComponentScan(basePackages = "org.javamaster.b2c") @EnableTransactionManagement @SpringBootApplication public class ScheduledApplication { static Logger logger = LoggerFactory.getLogger(ScheduledApplication.class); public static void main(String[] args) { SpringApplication.run(ScheduledApplication.class, args); logger.info("定時任務頁面管理地址:{}", "http://localhost:8089/scheduled/task/taskList"); } }
然后編寫定時任務類:
/** * @author yudong * @date 2019/8/24 */ @Component public class FixedPrintTask { Logger logger = LoggerFactory.getLogger(getClass()); private int i; @Scheduled(cron = "*/15 * * * * ?") public void execute() { logger.info("FixedPrintTask execute times:{}", ++i); } }
@Scheduled(cron ="*/15 * * * * ?")注解表明這是一個需要定時執行的方法,里面的cron屬性接收的是一個cron表達式,這里我給的是 */15 * * * * ? ,這個的意思是每隔15秒執行一次方法,對cron表達式不熟悉的同學可以百度一下用法。項目跑起來后可以看到方法被定時執行了:
這種方式有個缺點,那就是執行周期寫死在代碼里了,沒有辦法動態改變,要想改變只能修改代碼在重新部署啟動微服務。其實Spring也考慮到了這個,所以給出了另外的解決方案,就是我下面說的第二種方式。
第二種方式需要用到數據庫,先來建立一個定時任務表並插入三條定時任務記錄:
drop table if exists `spring_scheduled_cron`; create table `spring_scheduled_cron` ( `cron_id` int primary key auto_increment comment '主鍵id', `cron_key` varchar(128) not null unique comment '定時任務完整類名', `cron_expression` varchar(20) not null comment 'cron表達式', `task_explain` varchar(50) not null default '' comment '任務描述', `status` tinyint not null default 1 comment '狀態,1:正常;2:停用', unique index cron_key_unique_idx(`cron_key`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '定時任務表'; insert into `spring_scheduled_cron` values (1, 'org.javamaster.b2c.scheduled.task.DynamicPrintTask', '*/5 * * * * ?', '定時任務描述', 1); insert into `spring_scheduled_cron` values (2, 'org.javamaster.b2c.scheduled.task.DynamicPrintTask1', '*/5 * * * * ?', '定時任務描述1', 1); insert into `spring_scheduled_cron` values (3, 'org.javamaster.b2c.scheduled.task.DynamicPrintTask2', '*/5 * * * * ?', '定時任務描述2', 1);
編寫一個配置類:
@Configuration public class ScheduledConfig implements SchedulingConfigurer { @Autowired private ApplicationContext context; @Autowired private SpringScheduledCronRepository cronRepository; @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { for (SpringScheduledCron springScheduledCron : cronRepository.findAll()) { Class<?> clazz; Object task; try { clazz = Class.forName(springScheduledCron.getCronKey()); task = context.getBean(clazz); } catch (ClassNotFoundException e) { throw new IllegalArgumentException("spring_scheduled_cron表數據" + springScheduledCron.getCronKey() + "有誤", e); } catch (BeansException e) { throw new IllegalArgumentException(springScheduledCron.getCronKey() + "未納入到spring管理", e); } Assert.isAssignable(ScheduledOfTask.class, task.getClass(), "定時任務類必須實現ScheduledOfTask接口"); // 可以通過改變數據庫數據進而實現動態改變執行周期 taskRegistrar.addTriggerTask(((Runnable) task), triggerContext -> { String cronExpression = cronRepository.findByCronId(springScheduledCron.getCronId()).getCronExpression(); return new CronTrigger(cronExpression).nextExecutionTime(triggerContext); } ); } } @Bean public Executor taskExecutor() { return Executors.newScheduledThreadPool(10); } }
這里我為了做到可以靈活處理,自定義了一個接口ScheduledOfTask:
/** * @author yudong * @date 2019/5/11 */ public interface ScheduledOfTask extends Runnable { /** * 定時任務方法 */ void execute(); /** * 實現控制定時任務啟用或禁用的功能 */ @Override default void run() { SpringScheduledCronRepository repository = SpringUtils.getBean(SpringScheduledCronRepository.class); SpringScheduledCron scheduledCron = repository.findByCronKey(this.getClass().getName()); if (StatusEnum.DISABLED.getCode().equals(scheduledCron.getStatus())) { return; } execute(); } }
所有定時任務類只需要實現這個接口並相應的在數據庫插入一條記錄,那么在微服務啟動的時候,就會被自動注冊到Spring的定時任務里,也就是這行代碼所起的作用:
// 可以通過改變數據庫數據進而實現動態改變執行周期 taskRegistrar.addTriggerTask(((Runnable) task), triggerContext -> { String cronExpression = cronRepository.findByCronId(springScheduledCron.getCronId()).getCronExpression(); return new CronTrigger(cronExpression).nextExecutionTime(triggerContext); } );
具體的定時任務類(一共有三個,這里我只列出一個):
/** * @author yudong * @date 2019/5/10 */ @Component public class DynamicPrintTask implements ScheduledOfTask { Logger logger = LoggerFactory.getLogger(getClass()); private int i; @Override public void execute() { logger.info("DynamicPrintTask execute times:{}", ++i); } }
項目跑起來后,可以看到類被定時執行了:
那么,要如何動態改變執行周期呢,沒有理由去手工改動數據庫吧?開發測試環境可以這么搞,生產環境就不可以了,所以為了做到動態改變數據庫數據,很簡單,提供一個Controller類供調用:
/** * 管理定時任務(需要做權限控制),具體的業務邏輯應 * 該寫在Service里,良好的設計是Controller本身 * 只處理很少甚至不處理工作,業務邏輯均委托給 * Service進行處理,這里我偷一下懶,都寫在Controller * @author yudong * @date 2019/5/10 */ @Controller @RequestMapping("/scheduled/task") public class TaskController { @Autowired private ApplicationContext context; @Autowired private SpringScheduledCronRepository cronRepository; /** * 查看任務列表 */ @RequestMapping("/taskList") public String taskList(Model model) { model.addAttribute("cronList", cronRepository.findAll()); return "task-list"; } /** * 編輯任務cron表達式 */ @ResponseBody @RequestMapping("/editTaskCron") public Integer editTaskCron(String cronKey, String newCron) { if (!CronUtils.isValidExpression(newCron)) { throw new IllegalArgumentException("失敗,非法表達式:" + newCron); } cronRepository.updateCronExpressionByCronKey(newCron, cronKey); return AppConsts.SUCCESS; } /** * 執行定時任務 */ @ResponseBody @RequestMapping("/runTaskCron") public Integer runTaskCron(String cronKey) throws Exception { ((ScheduledOfTask) context.getBean(Class.forName(cronKey))).execute(); return AppConsts.SUCCESS; } /** * 啟用或禁用定時任務 */ @ResponseBody @RequestMapping("/changeStatusTaskCron") public Integer changeStatusTaskCron(Integer status, String cronKey) { cronRepository.updateStatusByCronKey(status, cronKey); return AppConsts.SUCCESS; } }
這里我為了方便調用Controller接口,使用thymeleaf技術寫了一個簡易的html管理頁面:
網頁效果是這樣的:
可以做到查看任務列表,修改任務cron表達式(也就實現了動態改變定時任務執行周期),暫停定時任務,以及直接執行定時任務。
最后如果對定時任務有更多其它要求,可以考慮使用xxljob這個開源的分布式任務調度平台,有興趣的同學可以去了解,這里我就不展開了。
源碼github地址:https://github.com/jufeng98/java-master