基于Spring的SchedulingConfigurer实现动态定时任务

我们在日常工作中避免不了使用定时任务做一些事情。除了可以使用类似XXL-job之类的中间件添加。我们还可以利用Spring的SchedulingConfigurer来实现。下面来从代码的角度介绍SchedulingConfigurer的用法。
要实现Spring的定时任务,我们都知道,可以直接使用注解来实现的@Scheduled(cron = "* * * * * ?")。但我们不能因为修改个定时时间,再上线一次吧。我们也不能动态的来添加定时任务。

使用SchedulingConfigurer 方式来实现定时任务,代码如下

@Configuration
public class ScheduledConfig implements SchedulingConfigurer {
    //笔者这里配合数据库来实现动态添加。方便查询数据库这里使用JdbcTemplate 
    @Autowired
    JdbcTemplate jdbcTemplate;

    private ScheduledTaskRegistrar taskRegistrar;

    private Set<ScheduledFuture<?>> scheduledFutures = null;
    private Map<String, ScheduledFuture<?>> taskFutures = new ConcurrentHashMap<>();
    
    /**
     * 这个方法在Spring初始化的时候会帮我们执行,这里也会拉取数据库内需要执行的任务,进行添加到定时器里。
     * @param scheduledTaskRegistrar
     */
    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        List<TriggerTask> list= new ArrayList<>();
        //查询出来当前数据库中存储的所有有效的任务
        List<Map<String, Object>> maps = jdbcTemplate.queryForList("select * from corn  where status=1");
        //循环添加任务
        maps.forEach(t->{
            TriggerTask triggerTask = new TriggerTask(()->{
                System.out.println("执行定时任务:"+ LocalDateTime.now().toLocalTime());
            },triggerContext -> {
                System.out.println("执行Cron:"+t.get("corn").toString()+",id="+t.get("id").toString());
                //如果需要动态的指定当前定时任务的执行corn。这里可以增加一步,查询数据库操作。如果任务corn不需要精确修改,corn可进行缓存。到期在去查询数据库。这里根据读者的需求自行取舍。
                return new CronTrigger(t.get("corn").toString()).nextExecutionTime(triggerContext);
            });
            list.add(triggerTask);
        });
        //将任务列表注册到定时器
        scheduledTaskRegistrar.setTriggerTasksList(list);
        this.taskRegistrar = scheduledTaskRegistrar;

    }


    /**
     * 添加任务
     * @param taskId
     * @param triggerTask
     */
    public void addTask(String taskId, TriggerTask triggerTask) {
        //如果定时任务id已存在,则取消原定时器,从新创建新定时器,这里也是个更新定时任务的过程。
        if (taskFutures.containsKey(taskId)) {
            System.out.println("the taskId[" + taskId + "]  取消,重新添加");
            cancelTriggerTask(taskId);
        }
        TaskScheduler scheduler = taskRegistrar.getScheduler();
        ScheduledFuture<?> future = scheduler.schedule(triggerTask.getRunnable(), triggerTask.getTrigger());
        getScheduledFutures().add(future);
        taskFutures.put(taskId, future);
    }

    /**
     * 获取任务列表
     */
    private Set<ScheduledFuture<?>> getScheduledFutures() {
        if (scheduledFutures == null) {
            try {
                scheduledFutures = (Set<ScheduledFuture<?>>) Utils.getProperty(taskRegistrar, "scheduledTasks");
            } catch (NoSuchFieldException e) {
                throw new SchedulingException("not found scheduledFutures field.");
            }
        }
        return scheduledFutures;
    }
    /**
     * 取消任务
     */
    public void cancelTriggerTask(String taskId) {
        ScheduledFuture<?> future = taskFutures.get(taskId);
        if (future != null) {
            future.cancel(true);
        }
        taskFutures.remove(taskId);
        getScheduledFutures().remove(future);
    }
}

config代码中使用到的反射工具类

public static Object getProperty(Object obj, String name) throws NoSuchFieldException {
    Object value = null;
    Field field = findField(obj.getClass(), name);
    if (field == null) {
        throw new NoSuchFieldException("no such field [" + name + "]");
    }
    boolean accessible = field.isAccessible();
    field.setAccessible(true);
    try {
        value = field.get(obj);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
    field.setAccessible(accessible);
    return value;
}

public static Field findField(Class<?> clazz, String name) {
    try {
        return clazz.getField(name);
    } catch (NoSuchFieldException ex) {
        return findDeclaredField(clazz, name);
    }
}
public static Field findDeclaredField(Class<?> clazz, String name) {
    try {
        return clazz.getDeclaredField(name);
    } catch (NoSuchFieldException ex) {
        if (clazz.getSuperclass() != null) {
            return findDeclaredField(clazz.getSuperclass(), name);
        }
        return null;
    }
}

添加测试类

@RestController
public class test {

    @Autowired
    ScheduledConfig scheduledConfig;

    @Autowired
    JdbcTemplate jdbcTemplate;


    @GetMapping("/test")
    public void test(String id) throws NoSuchFieldException {
        Map<String, ScheduledFuture<?>> taskRegistrar = (Map<String, ScheduledFuture<?>>) Utils.getProperty(scheduledConfig, "taskFutures");
        System.out.println(taskRegistrar.size());
        scheduledConfig.addTask(id,triggerTask(id,"* * * * * ?"));
        //这里可根据需要自行存库jdbcTemplate.insert()。
    }

    private TriggerTask triggerTask(String taskId,String corn){
        return new TriggerTask(()->{
            System.out.println("执行定时任务:"+ LocalDateTime.now().toLocalTime());
        },triggerContext -> {
            System.out.println("执行corn:"+"* * * * * ?"+",id="+taskId);
            //这里与config类的代码类似。根据需要自行处理
            return new CronTrigger(corn).nextExecutionTime(triggerContext);
        });
    }
}

测试一下我们的定时任务:

数据库中的定时任务
image.png

项目启动时,定时任务注册。就会按照corn去执行。

修改corn的值。定时任务在下次执行的时候会加载新的时间。

image.png

访问接口添加一个定时任务http://127.0.0.1:8080/test?id=10

任务生效,定时执行,如下图。

image.png

值得注意的是,我们并不能直接在数据库中修改status的值,使其生效。需要在定时器中移除这个任务,可以使任务失效,并且修改数据库status的值,使其下次加载的时候不生效即可。

这里仅供学习,真正生产环境。还是使用xxl-job之类的中间件比较好。毕竟我们这里需要考虑分布式环境下。任务多次并发执行的情况,需要加分布式锁。这样会使功能的实现复杂度更进一步。实现起来复杂,后期不易维护。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容