如何在Spring Boot中实现方法于指定日期(Ad实体activeUntil字段)执行?
当然有啦!Spring生态里有好几种成熟的方案能搞定这个「在广告过期时间点发送通知」的需求,我给你拆解几个常用的思路,你可以根据自己的业务场景来选:
方案一:使用Spring Scheduled定时任务(轻量简单)
这是最容易上手的方案,适合时效性要求不是特别高、广告数量适中的场景。核心逻辑是定时扫描数据库,找出已过期且未发送通知的广告,执行发送逻辑。
步骤示例:
- 先开启Spring的定时任务支持:
@SpringBootApplication @EnableScheduling public class AdApplication { public static void main(String[] args) { SpringApplication.run(AdApplication.class, args); } }
- 编写定时任务类,实现扫描和通知逻辑:
@Component public class AdExpirationTask { @Autowired private AdRepository adRepository; @Autowired private NotificationService notificationService; // 每小时执行一次扫描(可根据业务调整cron表达式或fixedRate) @Scheduled(cron = "0 0 * * * ?") public void checkExpiredAds() { LocalDateTime now = LocalDateTime.now(); // 查询已过期且未发送通知的广告 List<Ad> expiredAds = adRepository.findByActiveUntilBeforeAndNotificationSentFalse(now); for (Ad ad : expiredAds) { // 给广告发布用户发送通知(这里可以是邮件、短信、APP推送等) notificationService.sendExpirationNotification(ad.getPublisher()); // 标记为已发送,避免重复执行 ad.setNotificationSent(true); adRepository.save(ad); } } }
方案二:使用Quartz框架(灵活的动态任务调度)
如果你的广告过期时间完全不固定,且需要精确到每个广告的过期点触发,Quartz会是更合适的选择。它支持动态创建、修改、删除定时任务,每个广告可以对应一个独立的过期任务。
步骤示例:
- 引入Quartz依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency>
- 定义执行通知逻辑的Job类:
public class AdExpirationJob implements Job { @Autowired private NotificationService notificationService; @Autowired private AdRepository adRepository; @Override public void execute(JobExecutionContext context) throws JobExecutionException { Long adId = (Long) context.getJobDetail().getJobDataMap().get("adId"); Ad ad = adRepository.findById(adId).orElse(null); if (ad != null && !ad.isNotificationSent()) { notificationService.sendExpirationNotification(ad.getPublisher()); ad.setNotificationSent(true); adRepository.save(ad); } } }
- 编写任务调度服务,用于创建/取消广告过期任务:
@Service public class QuartzJobService { @Autowired private Scheduler scheduler; // 为单个广告创建过期任务 public void scheduleAdExpirationJob(Ad ad) throws SchedulerException { // 构建JobDetail,传入广告ID作为参数 JobDetail jobDetail = JobBuilder.newJob(AdExpirationJob.class) .withIdentity("adExpirationJob_" + ad.getId(), "adGroup") .usingJobData("adId", ad.getId()) .build(); // 构建Trigger,设置触发时间为activeUntil Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("adExpirationTrigger_" + ad.getId(), "adGroup") .startAt(Date.from(ad.getActiveUntil().atZone(ZoneId.systemDefault()).toInstant())) .build(); // 注册任务到调度器 scheduler.scheduleJob(jobDetail, trigger); } // 广告更新/删除时,取消对应的过期任务 public void cancelAdExpirationJob(Long adId) throws SchedulerException { JobKey jobKey = new JobKey("adExpirationJob_" + adId, "adGroup"); scheduler.deleteJob(jobKey); } }
- 在广告创建/更新时调用调度服务:
@Service public class AdService { @Autowired private AdRepository adRepository; @Autowired private QuartzJobService quartzJobService; public Ad createAd(Ad ad) throws SchedulerException { Ad savedAd = adRepository.save(ad); // 为新广告调度过期任务 quartzJobService.scheduleAdExpirationJob(savedAd); return savedAd; } public Ad updateAd(Long adId, Ad updatedAd) throws SchedulerException { Ad existingAd = adRepository.findById(adId).orElseThrow(); // 先取消旧的过期任务 quartzJobService.cancelAdExpirationJob(adId); // 更新广告字段 existingAd.setActiveUntil(updatedAd.getActiveUntil()); existingAd.setNotificationSent(false); Ad savedAd = adRepository.save(existingAd); // 调度新的过期任务 quartzJobService.scheduleAdExpirationJob(savedAd); return savedAd; } }
方案三:使用延迟消息队列(高并发场景推荐)
如果你的系统广告数量大、并发高,用延迟消息队列会更高效,避免定时扫描带来的数据库压力。核心逻辑是广告创建时发送一条延迟消息,延迟时间等于「过期时间 - 当前时间」,消息到期后自动触发通知逻辑。
以RabbitMQ延迟队列为例:
- 引入RabbitMQ依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
- 配置延迟队列:
@Configuration public class RabbitMQConfig { public static final String DELAY_EXCHANGE = "ad.delay.exchange"; public static final String DELAY_QUEUE = "ad.delay.queue"; public static final String DELAY_ROUTING_KEY = "ad.expiration"; @Bean public CustomExchange delayExchange() { Map<String, Object> args = new HashMap<>(); args.put("x-delayed-type", "direct"); return new CustomExchange(DELAY_EXCHANGE, "x-delayed-message", true, false, args); } @Bean public Queue delayQueue() { return QueueBuilder.durable(DELAY_QUEUE).build(); } @Bean public Binding delayBinding() { return BindingBuilder.bind(delayQueue()) .to(delayExchange()) .with(DELAY_ROUTING_KEY) .noargs(); } }
- 发送延迟消息:
@Service public class AdMessageService { @Autowired private RabbitTemplate rabbitTemplate; public void sendAdExpirationDelayMessage(Ad ad) { LocalDateTime activeUntil = ad.getActiveUntil(); long delayMillis = Duration.between(LocalDateTime.now(), activeUntil).toMillis(); if (delayMillis > 0) { rabbitTemplate.convertAndSend(RabbitMQConfig.DELAY_EXCHANGE, RabbitMQConfig.DELAY_ROUTING_KEY, ad.getId(), message -> { message.getMessageProperties().setHeader("x-delay", delayMillis); return message; }); } } }
- 消费延迟消息并发送通知:
@Component public class AdExpirationConsumer { @Autowired private AdRepository adRepository; @Autowired private NotificationService notificationService; @RabbitListener(queues = RabbitMQConfig.DELAY_QUEUE) public void handleAdExpiration(Long adId) { Ad ad = adRepository.findById(adId).orElse(null); if (ad != null && !ad.isNotificationSent()) { notificationService.sendExpirationNotification(ad.getPublisher()); ad.setNotificationSent(true); adRepository.save(ad); } } }
通用注意事项:
- 幂等性保障:一定要给
Ad实体加个notificationSent(布尔类型)字段,每次执行通知逻辑前先判断这个字段,避免重复发送通知给用户 - 异常处理:给发送通知的逻辑加异常捕获(比如邮件发送失败、短信接口报错),避免任务因为异常中断
- 任务清理:如果用Quartz或延迟消息,当广告被删除或者
activeUntil被修改时,一定要及时取消旧的任务/消息,避免无效执行
内容的提问来源于stack exchange,提问作者Stefan Radonjic
相关产品推荐
相关产品推荐

