java(springboot)实现闹钟功能(动态定时器)

前言

项目中遇到一个延迟闹钟功能,谨以此篇博客和大家分享下。

需求

有个日程功能需要添加个闹钟提醒功能,可以设置一次提醒和多次提醒,并且可以设置提醒时间范围。

总体流程

  1. 通过接口新增一个闹铃(选择提醒时间,设置范围)
  2. 解析参数生成corn表达式,并生成一条任务数据存入数据库
  3. 判断闹铃的下一次时间是否有今天,有的话需要马上新增一条任务
  4. 每天晚上定时去获取数据库的数据通过判断时间范围来区分,找到有效的闹铃加到任务中去,因为有些任务并不是当天执行的,可能设在几个月后
  5. 任务失效后去数据库把失效字段设为失效,扫描的时候不扫描失效数据
  6. 服务重启也要把所有有效任务加上

实现

新增任务关键逻辑:解析前端传的参数,动态生成对应的corn表达式,生成任务,并添加任务

String corn = "";
        if (appSchedule.getPushType() == DataConstant.ONE) {
            int day = localDateTime.getDayOfMonth();
            int monthValue = localDateTime.getMonthValue();
            int year = localDateTime.getYear();
            // 一次
            corn = second + " " + minute + " " + hour + " " + day + " " + monthValue + " ?";
        } else if (appSchedule.getPushType() == DataConstant.TWO) {
            // 每天
            corn = second + " " + minute + " " + hour + " * * ?";
        } else if (appSchedule.getPushType() == DataConstant.ZERO) {
            // 每月
            int day = localDateTime.getDayOfMonth();
            corn = second + " " + minute + " " + hour + " " + day + " * ?";
        } else  {
            // 每周几 pushtype-2就是周几
            int week = appSchedule.getPushType() - DataConstant.TWO;
            corn = second + " " + minute + " " + hour + " ? * " + week;
        }
        appSchedule.setCorn(corn);
        // 新增
        appScheduleService.save(appSchedule);
        // 如果是今天有执行的任务就注册定时器,不然就是晚上凌晨自动注册
        List<String> recentDataByCorn = getRecentDataByCorn(corn, 1, new Date());
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        LocalDateTime nextDay = LocalDateTime.parse(recentDataByCorn.get(DataConstant.ZERO), dateTimeFormatter);
        if (nextDay.toLocalDate().isEqual(LocalDate.now())) {
            // 注册定时器,定时器执行的时候会调用appScheduleService的pushOne方法
            SchedulingRunnable task = new SchedulingRunnable("appScheduleService", "pushOne", appSchedule);
            cronTaskRegistrar.addCronTask(appSchedule.getId(), task, corn);
        }

注意: appScheduleService是注册到spring里的beanName

解析corn

 /**
     * 解析corn获取最近数据
     * @param corn
     * @param size 获取条数
     * @param startDate 开始时间
     * @return
     */
    private List<String> getRecentDataByCorn(String corn, Integer size, Date startDate) {
        CronSequenceGenerator cronSequenceGenerator = new CronSequenceGenerator(corn);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        List<String> list = new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            // 计算下次时间点的开始时间
            startDate = cronSequenceGenerator.next(startDate);
            list.add(sdf.format(startDate));
        }
        return list;
    }

在这里插入图片描述
任务的管理通过CronTaskRegistrar类实现:
任务开启:
在这里插入图片描述
任务关闭:
在这里插入图片描述

ScheduledTask.java

public final class ScheduledTask {

    public volatile ScheduledFuture<?> future;
    /**
     * 取消定时任务
     */
    public void cancel() {
        ScheduledFuture<?> future = this.future;
        if (future != null) {
            future.cancel(true);
        }
    }
}

线程池配置类:SchedulingConfig.class

@Configuration
public class SchedulingConfig {
    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        // 定时任务执行线程池核心线程数
        taskScheduler.setPoolSize(4);
        taskScheduler.setRemoveOnCancelPolicy(true);
        taskScheduler.setThreadNamePrefix("TaskSchedulerThreadPool-");
        return taskScheduler;
    }
}

程序启动时加载数据库中的定时器
注意
如果用到@scheduled 定时执行的任务的话需要在服务启动类开启加上注解:
@EnableScheduling
在这里插入图片描述

@Component
@Transactional(rollbackFor = Exception.class)
public class SchedulingInitConfig {

    @Resource
    AppScheduleMapper appScheduleMapper;

    @Resource
    CronTaskRegistrar cronTaskRegistrar;

    /**
     * 在服务启动时查询数据库数据启动相关任务
     */
    @PostConstruct
    public void initFileSuffix() {
        //在服务启动时查询数据库数据启动相关任务
        LocalDate now = LocalDate.now();
        List<AppSchedule> appSchedules = appScheduleMapper.selectInitList(now);
        if (DataUtil.isEmpty(appSchedules)) {
            return;
        }
        appSchedules.forEach(v -> {
            SchedulingRunnable task = new SchedulingRunnable("appScheduleService", "pushOne", v);
            cronTaskRegistrar.addCronTask(v.getId(), task, v.getCorn());
        });
    }

    /**
     * 每天凌晨启动有效的定时任务,同时去掉过时的定时任务
     */
    @Async("DefaultExecutor")
    @Scheduled(cron = "0 0 0 * * ?")
    public void updateRechargeRecord() {
        // 找过时且有效的数据, 改成失效
        LocalDate now = LocalDate.now();
        List<AppSchedule> appSchedules = appScheduleMapper.selectNotValidList(now);
        if (DataUtil.isNotEmpty(appSchedules)) {
            // 批量失效
            appScheduleMapper.batchUpdateIfValid(appSchedules.stream().map(v -> {return v.getId().toString();}).collect(Collectors.joining(",")));
            // 关掉对应的定时器
            cronTaskRegistrar.removeBatchCronTask(appSchedules.stream().map(AppSchedule::getId).collect(Collectors.toList()));
        }
        // 启动有效的定时任务
        List<AppSchedule> validList = appScheduleMapper.selectInitList(now);
        if (DataUtil.isNotEmpty(validList)) {
            validList.forEach(v -> {
                SchedulingRunnable task = new SchedulingRunnable("appScheduleService", "pushOne", v);
                cronTaskRegistrar.addCronTask(v.getId(), task, v.getCorn());
            });
        }
    }
}

定时器执行类:

public class SchedulingRunnable implements Runnable {

    private static final Logger logger = LoggerFactory.getLogger(SchedulingRunnable.class);

    private String beanName;

    private String methodName;

    private Object[] params;

    public SchedulingRunnable(String beanName, String methodName) {
        this(beanName, methodName, null);
    }

    public SchedulingRunnable(String beanName, String methodName, Object...params ) {
        this.beanName = beanName;
        this.methodName = methodName;
        this.params = params;
    }

    @Override
    public void run() {
        logger.info("定时任务开始执行 - bean:{},方法:{},参数:{}", beanName, methodName, params);
        long startTime = System.currentTimeMillis();

        try {
            Object target = SpringContextTaskUtils.getBean(beanName);

            Method method = null;
            if (null != params && params.length > 0) {
                Class<?>[] paramCls = new Class[params.length];
                for (int i = 0; i < params.length; i++) {
                    paramCls[i] = params[i].getClass();
                }
                method = target.getClass().getDeclaredMethod(methodName, paramCls);
            } else {
                method = target.getClass().getDeclaredMethod(methodName);
            }

            ReflectionUtils.makeAccessible(method);
            if (null != params && params.length > 0) {
                method.invoke(target, params);
            } else {
                method.invoke(target);
            }
        } catch (Exception ex) {
            logger.error(String.format("定时任务执行异常 - bean:%s,方法:%s,参数:%s ", beanName, methodName, params), ex);
        }

        long times = System.currentTimeMillis() - startTime;
        logger.info("定时任务执行结束 - bean:{},方法:{},参数:{},耗时:{} 毫秒", beanName, methodName, params, times);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        SchedulingRunnable that = (SchedulingRunnable) o;
        if (params == null) {
            return beanName.equals(that.beanName) &&
                    methodName.equals(that.methodName) &&
                    that.params == null;
        }

        return beanName.equals(that.beanName) &&
                methodName.equals(that.methodName) &&
                params.equals(that.params);
    }

    @Override
    public int hashCode() {
        if (params == null) {
            return Objects.hash(beanName, methodName);
        }

        return Objects.hash(beanName, methodName, params);
    }
}

获取上下文类:

@Component
public class SpringContextTaskUtils implements ApplicationContextAware {
    private static ApplicationContext applicationContext = null;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if (SpringContextTaskUtils.applicationContext == null) {
            SpringContextTaskUtils.applicationContext = applicationContext;
        }
    }

    //获取applicationContext
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    //通过name获取 Bean.
    public static Object getBean(String name) {
        return getApplicationContext().getBean(name);
    }

    //通过class获取Bean.
    public static <T> T getBean(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
    }

    //通过name,以及Clazz返回指定的Bean
    public static <T> T getBean(String name, Class<T> clazz) {
        return getApplicationContext().getBean(name, clazz);
    }
}

CronTaskRegistrar.class

/**
 * 添加定时任务注册类,用来增加、删除定时任务。
 * @author wuge
 * @date 2021-11-27 10:44
 */
@Component
public class CronTaskRegistrar implements DisposableBean {

    private final Map<Long, ScheduledTask> scheduledTasks = new ConcurrentHashMap<>();

    @Autowired
    private TaskScheduler taskScheduler;

    public TaskScheduler getScheduler() {
        return this.taskScheduler;
    }

    /**
     * 新增定时任务
     * @param id 任务id也就是数据id
     * @param task
     * @param cronExpression
     */
    public void addCronTask(Long id, Runnable task, String cronExpression) {
        addCronTask(id, new CronTask(task, cronExpression));
    }

    public void addCronTask(Long id, CronTask cronTask) {
        if (cronTask != null) {
            Runnable task = cronTask.getRunnable();
            if (this.scheduledTasks.containsKey(id)) {
                removeCronTask(id);
            }

            this.scheduledTasks.put(id, scheduleCronTask(cronTask));
        }
    }

    /**
     * 移除定时任务
     * @param id
     */
    public void removeCronTask(Long id) {
        ScheduledTask scheduledTask = this.scheduledTasks.remove(id);
        if (scheduledTask != null)
            scheduledTask.cancel();
    }

    /**
     * 批量移除定时任务
     * @param idList 业务id列表
     */
    public void removeBatchCronTask(List<Long> idList) {
        if (DataUtil.isEmpty(idList)) {
            return;
        }
        idList.forEach(id -> {
            ScheduledTask scheduledTask = this.scheduledTasks.remove(id);
            if (scheduledTask != null) {
                scheduledTask.cancel();
            }
        });
    }

    public ScheduledTask scheduleCronTask(CronTask cronTask) {
        ScheduledTask scheduledTask = new ScheduledTask();
        scheduledTask.future = this.taskScheduler.schedule(cronTask.getRunnable(), cronTask.getTrigger());

        return scheduledTask;
    }


    @Override
    public void destroy() {
        for (ScheduledTask task : this.scheduledTasks.values()) {
            task.cancel();
        }
        this.scheduledTasks.clear();
    }
}

这样就实现了灵活配置定时器了

写在最后

非常感谢大家的认真阅读,如果有其他好用的技巧或者其他代码技巧都可以和我交流哦,如有不足,还望各位看官多批评指正=_=
技术交流秋秋群:719023986

微x关注:干饭必备外卖神券,每天领大额卷
微x关注:正好想买,自助查桃宝京d卷

  • 6
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
实现闹钟功能可以使用DS1302的外部中断功能,不需要使用定时器。下面是基于STM32F1系列芯片和DS1302的闹钟功能实现代码示例: 1. 首先进行DS1302的初始化,包括时钟、日期、控制寄存器等的设置。 2. 设置外部中断,将DS1302的CLK引脚接到STM32的外部中断引脚上,例如PA0。 3. 在中断服务函数中,判断是否到达设定的闹钟时间。如果到达,则触发闹钟事件,可以通过LED或蜂鸣器等方式进行提示。 以下是一个简单的示例代码: ```c #include "stm32f10x.h" #include "ds1302.h" void EXTI0_IRQHandler(void) { if(EXTI_GetITStatus(EXTI_Line0) != RESET) // 判断是否为PA0引脚触发的中断 { EXTI_ClearITPendingBit(EXTI_Line0); // 清除中断标志位 // 获取当前时间 RTC_Time time; DS1302_GetTime(&time); // 判断是否到达设定的闹钟时间 if(time.Hour == 6 && time.Minute == 30) { // 触发闹钟事件,例如点亮一个LED GPIO_SetBits(GPIOA, GPIO_Pin_1); } } } int main(void) { // 初始化GPIO和外部中断 GPIO_InitTypeDef GPIO_InitStructure; EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // PA0引脚 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入 GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; // PA1引脚 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); // 将PA0引脚连接到中断线0 EXTI_InitStructure.EXTI_Line = EXTI_Line0; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发 EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x00; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x00; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); // 初始化DS1302 DS1302_Init(); while(1) { // 主循环中可以进行其他操作,例如显示当前时间等 } } ``` 在以上代码中,当PA0引脚触发下降沿时,会进入中断服务函数`EXTI0_IRQHandler`中进行闹钟判断。如果当前时间是6:30,则会点亮PA1引脚,触发闹钟事件。主循环中可以进行其他操作,例如显示当前时间等。 需要注意的是,以上代码仅作为示例,实际使用时需要根据具体需求进行修改和完善。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值