本系列文章记录“智能提醒助理”wx公众号 建设历程,记录实践经验、巩固知识点、锻炼总结能力。
目录
本篇文章介绍 智能提醒助理的核心模块,调度器的技术选型和使用。
一、需求出发点
当用户在前端设置提醒的时候,会选择一个时间,比如:单次、每日、每月、每年、每周。
那么如何在用户设置的时间点给予提醒呢?
这个时候 就需要一个调度。
二、实现路径分析
1、Quartz
2、xxl-job
对比分析:
1、quartz 是 调度逻辑和执行逻辑通常并存于同一个项目中,会相互影响。默认不支持分布式需要自己扩展。
2、支持分布式、调度独立部署、有独立的界面维护管理、支持分片、负载均衡策略和故障转移。
三、最终方案
选择xxl-job。
四、使用方式
1、如何集成
使用源码包,自己增加API,调整下。官方地址
GitHub - xuxueli/xxl-job: A distributed task scheduling framework.(分布式任务调度平台XXL-JOB)
主要修改逻辑:将 addJob、remove 增加通过token认证即可API访问,我自己又增加了removes方法。
简单粗暴:增加、删除,批量删除,修改也是先删再加,已解决批量更新 还要触发任务的问题。
package com.xxl.job.admin.controller;
import com.xxl.job.admin.controller.annotation.TokenPermission;
import com.xxl.job.admin.core.exception.XxlJobException;
import com.xxl.job.admin.core.model.XxlJobGroup;
import com.xxl.job.admin.core.model.XxlJobInfo;
import com.xxl.job.admin.core.model.XxlJobUser;
import com.xxl.job.admin.core.route.ExecutorRouteStrategyEnum;
import com.xxl.job.admin.core.scheduler.MisfireStrategyEnum;
import com.xxl.job.admin.core.scheduler.ScheduleTypeEnum;
import com.xxl.job.admin.core.thread.JobScheduleHelper;
import com.xxl.job.admin.core.util.I18nUtil;
import com.xxl.job.admin.dao.XxlJobGroupDao;
import com.xxl.job.admin.service.LoginService;
import com.xxl.job.admin.service.XxlJobService;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.enums.ExecutorBlockStrategyEnum;
import com.xxl.job.core.glue.GlueTypeEnum;
import com.xxl.job.core.util.DateUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StopWatch;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
/**
* index controller
* @author xuxueli 2015-12-19 16:13:16
*/
@Controller
@RequestMapping("/jobinfo")
public class JobInfoController {
private static Logger logger = LoggerFactory.getLogger(JobInfoController.class);
@Resource
private XxlJobGroupDao xxlJobGroupDao;
@Resource
private XxlJobService xxlJobService;
@RequestMapping
public String index(HttpServletRequest request, Model model, @RequestParam(required = false, defaultValue = "-1") int jobGroup) {
// 枚举-字典
model.addAttribute("ExecutorRouteStrategyEnum", ExecutorRouteStrategyEnum.values()); // 路由策略-列表
model.addAttribute("GlueTypeEnum", GlueTypeEnum.values()); // Glue类型-字典
model.addAttribute("ExecutorBlockStrategyEnum", ExecutorBlockStrategyEnum.values()); // 阻塞处理策略-字典
model.addAttribute("ScheduleTypeEnum", ScheduleTypeEnum.values()); // 调度类型
model.addAttribute("MisfireStrategyEnum", MisfireStrategyEnum.values()); // 调度过期策略
// 执行器列表
List<XxlJobGroup> jobGroupList_all = xxlJobGroupDao.findAll();
// filter group
List<XxlJobGroup> jobGroupList = filterJobGroupByRole(request, jobGroupList_all);
if (jobGroupList==null || jobGroupList.size()==0) {
throw new XxlJobException(I18nUtil.getString("jobgroup_empty"));
}
model.addAttribute("JobGroupList", jobGroupList);
model.addAttribute("jobGroup", jobGroup);
return "jobinfo/jobinfo.index";
}
public static List<XxlJobGroup> filterJobGroupByRole(HttpServletRequest request, List<XxlJobGroup> jobGroupList_all){
List<XxlJobGroup> jobGroupList = new ArrayList<>();
if (jobGroupList_all!=null && jobGroupList_all.size()>0) {
XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY);
if (loginUser.getRole() == 1) {
jobGroupList = jobGroupList_all;
} else {
List<String> groupIdStrs = new ArrayList<>();
if (loginUser.getPermission()!=null && loginUser.getPermission().trim().length()>0) {
groupIdStrs = Arrays.asList(loginUser.getPermission().trim().split(","));
}
for (XxlJobGroup groupItem:jobGroupList_all) {
if (groupIdStrs.contains(String.valueOf(groupItem.getId()))) {
jobGroupList.add(groupItem);
}
}
}
}
return jobGroupList;
}
public static void validPermission(HttpServletRequest request, int jobGroup) {
XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY);
if (!loginUser.validPermission(jobGroup)) {
throw new RuntimeException(I18nUtil.getString("system_permission_limit") + "[username="+ loginUser.getUsername() +"]");
}
}
@RequestMapping("/pageList")
@ResponseBody
@TokenPermission
public Map<String, Object> pageList(@RequestParam(required = false, defaultValue = "0") int start,
@RequestParam(required = false, defaultValue = "10") int length,
int jobGroup, int triggerStatus, String jobDesc, String executorHandler, String author) {
return xxlJobService.pageList(start, length, jobGroup, triggerStatus, jobDesc, executorHandler, author);
}
@RequestMapping("/add")
@ResponseBody
public ReturnT<String> add(XxlJobInfo jobInfo) {
return xxlJobService.add(jobInfo);
}
@PostMapping("/addJob")
@ResponseBody
@TokenPermission
public ReturnT<String> addJob(@RequestBody XxlJobInfo jobInfo) {
return xxlJobService.add(jobInfo);
}
@RequestMapping("/update")
@ResponseBody
public ReturnT<String> update(XxlJobInfo jobInfo) {
return xxlJobService.update(jobInfo);
}
@PostMapping("/updateJob")
@ResponseBody
@TokenPermission
public ReturnT<String> updateJob(@RequestBody XxlJobInfo jobInfo) {
return xxlJobService.update(jobInfo);
}
@RequestMapping("/remove")
@ResponseBody
@TokenPermission
public ReturnT<String> remove(int id) {
return xxlJobService.remove(id);
}
@RequestMapping("/removes")
@ResponseBody
@TokenPermission
public ReturnT<String> removes(String jobIds) {
String[] split = jobIds.split(",");
for (String s : split) {
if ((StringUtils.isEmpty(s) || "null".equals(s))){
continue;
}
xxlJobService.remove(Integer.valueOf(s));
}
return ReturnT.SUCCESS;
}
@RequestMapping("/stop")
@ResponseBody
@TokenPermission
public ReturnT<String> pause(int id) {
return xxlJobService.stop(id);
}
@RequestMapping("/start")
@ResponseBody
@TokenPermission
public ReturnT<String> start(int id) {
return xxlJobService.start(id);
}
@RequestMapping("/trigger")
@ResponseBody
@TokenPermission
public ReturnT<String> triggerJob(HttpServletRequest request, int id, String executorParam, String addressList) {
// login user
XxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginService.LOGIN_IDENTITY_KEY);
// trigger
return xxlJobService.trigger(loginUser, id, executorParam, addressList);
}
@RequestMapping("/nextTriggerTime")
@ResponseBody
@TokenPermission
public ReturnT<List<String>> nextTriggerTime(String scheduleType, String scheduleConf) {
XxlJobInfo paramXxlJobInfo = new XxlJobInfo();
paramXxlJobInfo.setScheduleType(scheduleType);
paramXxlJobInfo.setScheduleConf(scheduleConf);
List<String> result = new ArrayList<>();
try {
Date lastTime = new Date();
for (int i = 0; i < 5; i++) {
lastTime = JobScheduleHelper.generateNextValidTime(paramXxlJobInfo, lastTime);
if (lastTime != null) {
result.add(DateUtil.formatDateTime(lastTime));
} else {
break;
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
return new ReturnT<List<String>>(ReturnT.FAIL_CODE, (I18nUtil.getString("schedule_type")+I18nUtil.getString("system_unvalid")) + e.getMessage());
}
return new ReturnT<List<String>>(result);
}
}
2、如何通过API动态创建任务
/**
* 添加任务到xxljob
* @param jobTime
* @return
*/
private Integer addJob(JobTime jobTime) {
JobInfoVo jobInfoVo = new JobInfoVo();
jobInfoVo.setJobDesc(jobTime.getTitle()+" "+jobTime.getRemark());java
jobInfoVo.setScheduleConf(jobTime.getCron());
jobInfoVo.setExecutorParam(JSONObject.toJSONString(jobTime));//参数
jobInfoVo.setExecutorHandler("baseTipsJob");//执行器任务handler
jobInfoVo.setAuthor("user-" + jobTime.getUserId());
jobInfoVo.setJobGroup(2);
jobInfoVo.setScheduleType("CRON");
jobInfoVo.setExecutorRouteStrategy("FIRST");// 执行器路由策略 FIRST 第一个,ROUND 轮询,RANDOM 随机,SHARDING_BROADCAST 分片广播
jobInfoVo.setExecutorBlockStrategy("DISCARD_LATER");//阻塞处理策略 DISCARD_LATER,丢弃后续调度
jobInfoVo.setTriggerStatus(1);//调度状态:0-停止,1-运行
jobInfoVo.setGlueType("BEAN");
jobInfoVo.setMisfireStrategy("DO_NOTHING");
jobInfoVo.setAddTime(new Date());
Integer jobId = xxlJobApi.addJob(jobInfoVo);
log.info("添加jobId成功:{}", jobId);
return jobId;
}
3、通过API更新和删除任务
/**
* 修改提醒任务
*
* @param aiTips
* @return
*/
@Transactional(rollbackFor = Exception.class)
public boolean updateTips(AiTips aiTips) {
AiTips byId = super.getById(aiTips.getId());
//先删除任务
delJobs(byId.getJobId());
//创建xxjob
List<JobTime> jobTimes = aiTips.getJobTimes();
List<String> jobIds = new ArrayList<>();
for (JobTime jobTime : jobTimes) {
jobTime.setAiTipsId(aiTips.getId());
Integer jobId = addJob(jobTime);
if (jobId!=null){
jobIds.add(String.valueOf(jobId));
}
}
// 3、更新jobId
String join = String.join(",", jobIds);
aiTips.setJobId(join);
this.updateById(aiTips);
return true;
}
4、问题:时间如何转换为cron表达式
package com.jdy.common.util;
import com.jdy.common.enums.TipsCycleEnum;
import com.jdy.common.utils.DateUtils;
import com.nlf.calendar.Lunar;
import lombok.extern.slf4j.Slf4j;
import java.text.ParseException;
/**
* cron 工具类
*/
@Slf4j
public class CronUtil {
/**
* 输入的日期转为cron
*/
public static void date2Cron() throws ParseException {
/* log.info("单次提醒corn:{}",createCronByType("2024-07-30 10:07","单次"));
// 阴历转阳历
String[] str = "2024-07-30 10:07".split(" ");
String yangli = yinli2yangli(str[0]);
log.info("单次提醒corn:{}",createCronByType(yangli+" "+str[1],"阴历单次"));
log.info("每天提醒corn:{}",createCronByType("09:03","每天"));
log.info("每工作日corn:{}",createCronByType("10:07","每工作日"));
log.info("每周corn:{}",createCronByType("10:08","每周",1));
log.info("每周六日corn:{}",createCronByType("10:07","每周六日"));
log.info("每月corn:{}",createCronByType("30 10:08","每月"));
log.info("每年corn:{}",createCronByType("07-30 10:10","每年"));
log.info("每年corn:{}",createCronByType("07-30 10:10","阴历每年"));
*/
}
/**
* 根据类型创建cron表达式
* @param str
* @param tipsCycle
* @return
*/
public static String createCronByType(String str,TipsCycleEnum tipsCycle){
return createCronByType(str, tipsCycle,0);
}
public static String createCronByType(String str,TipsCycleEnum tipsCycle,int week){
String[] ary = str.split("-| |:");
StringBuffer sb = new StringBuffer("0 ");
int flag = -1;
//单次 阴历单次
if(TipsCycleEnum.ONE.equals(tipsCycle) || TipsCycleEnum.LUNAR_ONE.equals(tipsCycle)){
flag = 0;
}
for (int i = ary.length -1; i >flag ; i--) {
sb.append(ary[i]+" ");
}
switch (tipsCycle){
case ONE: sb.append("? ").append(ary[0]); break;
case LUNAR_ONE: sb.append("? ").append(ary[0]); break;
case DAY: sb.append("* * ?");break;
// cron的周 对应关系
// 日 一 二 三 四 五 六
// 1 2 3 4 5 6 7
case WORKDAY: sb.append("? * 2-6");break;
case WEEKEND: sb.append("? * 1,7");break;
case WEEK: sb.append("? * "+(week+1>7?0:week+1));break; // week cron的 1 是周日, 7 是周六 需要修正下
case MONTH: sb.append("* ?");break;
case YEAR: sb.append("? *");break;
}
return sb.toString();
}
/**
* 阴历转阳历
*/
public static String yinli2yangli(String date) {
String[] ary = date.split("-");
Lunar lunar ;
// 场景1:单次 "2024-12-23"
if(ary.length == 3){
lunar = Lunar.fromYmd(Integer.parseInt(ary[0]),Integer.parseInt(ary[1]),Integer.parseInt(ary[2]));
}else{
// 场景2:每年 "12-23"的情况,当没有年的时候 获取当前年份
lunar = Lunar.fromYmd(DateUtils.getYear(),Integer.parseInt(ary[0]),Integer.parseInt(ary[1]));
}
return lunar.getSolar().toString();
}
}
5、问题:阴历如何转阳历
使用 Lunar 每年阳历的时候 需要注意,需要 在每年正月初一的时候 把第二年的 阳历再修正一遍。
想要了解更多关于lunar
的信息,请访问其官方文档:lunar。