定时任务schedule在微服务中的实际应用
需求模拟:
现有一张车辆的启动时间和停车时间统计表,需要将 某个时间段内,停车间隔超过 某个设定值 的数据在 指定时间 通过 邮件 发送给客户。简单一点就是用户需要9:00-13:00的车辆停车时间超过30min的数据邮件,并且在13:00发送。
需求分析:
1.停车超时时间最好不要在后台设定为一个固定值,建议另外添加一张表来保存数据,通过前台用户来设定,设定部分是常规单表的增删改,这里不做记录。
2.通过前台写入执行时间、邮件对象的参数,后台接收并执行的接口,可以使用cron表达式,具体用法可以去网上查。
3.数据分析:停车的数据可能出现四种情况
a.
b.
c.
d.
说明:c、d两种情况可以合并为一种情况,下一次开始的时间超出统计的结束时间且本次停车的时间小于13:00减去30min(超时参数)的数据
代码分析:
前台传入cron表达式、目标邮箱、目标id
后台接收数据后,发送超时统计数据邮件,返回执行结果码
(前台用户输入部分省略,下面是后端主要代码部分)
controller部分这里使用的是rest风格,SysJob类是任务数据类,包含cron等信息,可自行定义
/**
* 新增定时任务
*/
@PostMapping
public AjaxResult add(@RequestBody SysJob sysJob) throws SchedulerException, TaskException
{
if (!CronUtils.isValid(sysJob.getCronExpression()))
{
return AjaxResult.error("cron表达式不正确");
}
return toAjax(jobService.insertJob(sysJob));
}
/**
* 定时任务状态修改
*/
@PutMapping("/changeStatus")
public AjaxResult changeStatus(@RequestBody SysJob job) throws SchedulerException
{
SysJob newJob = jobService.selectJobById(job.getJobId());
newJob.setStatus(job.getStatus());
return toAjax(jobService.changeStatus(newJob));
}
/**
* 定时任务立即执行一次
*/
@PutMapping("/run")
public AjaxResult run(@RequestBody SysJob job) throws SchedulerException
{
jobService.run(job);
return AjaxResult.success();
}
CronUtils工具类
import java.text.ParseException;
import java.util.Date;
import org.quartz.CronExpression;
/**
* cron表达式工具类
*/
public class CronUtils
{
/**
* 返回一个布尔值代表一个给定的Cron表达式的有效性
*
* @param cronExpression Cron表达式
* @return boolean 表达式是否有效
*/
public static boolean isValid(String cronExpression)
{
return CronExpression.isValidExpression(cronExpression);
}
/**
* 返回一个字符串值,表示该消息无效Cron表达式给出有效性
*
* @param cronExpression Cron表达式
* @return String 无效时返回表达式错误描述,如果有效返回null
*/
public static String getInvalidMessage(String cronExpression)
{
try
{
new CronExpression(cronExpression);
return null;
}
catch (ParseException pe)
{
return pe.getMessage();
}
}
/**
* 返回下一个执行时间根据给定的Cron表达式
*
* @param cronExpression Cron表达式
* @return Date 下次Cron表达式执行时间
*/
public static Date getNextExecution(String cronExpression)
{
try
{
CronExpression cron = new CronExpression(cronExpression);
return cron.getNextValidTimeAfter(new Date(System.currentTimeMillis()));
}
catch (ParseException e)
{
throw new IllegalArgumentException(e.getMessage());
}
}
}
系统任务impl部分
@Service
public class SysJobServiceImpl implements ISysJobService
{
//Scheduler定时任务类,系统自带
@Autowired
private Scheduler scheduler;
//定时任务mapper这里不列写了
@Autowired
private SysJobMapper jobMapper;
/**
* 项目启动时,初始化定时器 主要是防止手动修改数据库导致未同步到定时任务处理(注:不能手动修改数据库ID和任务组名,否则会导致脏数据)
*/
@PostConstruct
public void init() throws SchedulerException, TaskException, ParseException {
scheduler.clear();
List<SysJob> jobList = jobMapper.selectJobAll();
for (SysJob job : jobList)
{
if(ScheduleUtils.isValidateCanDoExpression(job.getCronExpression())){
ScheduleUtils.createScheduleJob(scheduler, job);
}
}
}
/**
* 暂停任务
*
* @param job 调度信息
*/
@Override
@Transactional(rollbackFor = Exception.class)//事务回滚
public int pauseJob(SysJob job) throws SchedulerException
{
Long jobId = job.getJobId();
String jobGroup = job.getJobGroup();
job.setStatus(ScheduleConstants.Status.PAUSE.getValue());
int rows = jobMapper.updateJob(job);
if (rows > 0)
{
scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup));
}
return rows;
}
/**
* 恢复任务
*
* @param job 调度信息
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int resumeJob(SysJob job) throws SchedulerException
{
Long jobId = job.getJobId();
String jobGroup = job.getJobGroup();
job.setStatus(ScheduleConstants.Status.NORMAL.getValue());
int rows = jobMapper.updateJob(job);
if (rows > 0)
{
scheduler.resumeJob(ScheduleUtils.getJobKey(jobId, jobGroup));
}
return rows;
}
/**
* 删除任务后,所对应的trigger也将被删除
*
* @param job 调度信息
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteJob(SysJob job) throws SchedulerException
{
Long jobId = job.getJobId();
String jobGroup = job.getJobGroup();
int rows = jobMapper.deleteJobById(jobId);
if (rows > 0)
{
scheduler.deleteJob(ScheduleUtils.getJobKey(jobId, jobGroup));
}
return rows;
}
/**
* 任务调度状态修改
*
* @param job 调度信息
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int changeStatus(SysJob job) throws SchedulerException
{
int rows = 0;
String status = job.getStatus();
if (ScheduleConstants.Status.NORMAL.getValue().equals(status))
{
rows = resumeJob(job);
}
else if (ScheduleConstants.Status.PAUSE.getValue().equals(status))
{
rows = pauseJob(job);
}
return rows;
}
/**
* 立即运行任务
*
* @param job 调度信息
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void run(SysJob job) throws SchedulerException
{
Long jobId = job.getJobId();
String jobGroup = job.getJobGroup();
SysJob properties = selectJobById(job.getJobId());
// 参数
JobDataMap dataMap = new JobDataMap();
dataMap.put(ScheduleConstants.TASK_PROPERTIES, properties);
scheduler.triggerJob(ScheduleUtils.getJobKey(jobId, jobGroup), dataMap);
}
/**
* 新增任务
*
* @param job 调度信息 调度信息
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int insertJob(SysJob job) throws SchedulerException, TaskException
{
if(job.getUserId()==null){
job.setStatus(ScheduleConstants.Status.PAUSE.getValue());
}
int rows = jobMapper.insertJob(job);
if (rows > 0)
{
ScheduleUtils.createScheduleJob(scheduler, job);
}
return rows;
}
/**
* 更新任务的时间表达式
*
* @param job 调度信息
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int updateJob(SysJob job) throws SchedulerException, TaskException
{
SysJob properties = selectJobById(job.getJobId());
int rows = jobMapper.updateJob(job);
if (rows > 0)
{
updateSchedulerJob(job, properties.getJobGroup());
}
return rows;
}
/**
* 更新任务
*
* @param job 任务对象
* @param jobGroup 任务组名
*/
public void updateSchedulerJob(SysJob job, String jobGroup) throws SchedulerException, TaskException
{
Long jobId = job.getJobId();
// 判断是否存在
JobKey jobKey = ScheduleUtils.getJobKey(jobId, jobGroup);
if (scheduler.checkExists(jobKey))
{
// 防止创建时存在数据问题 先移除,然后在执行创建操作
scheduler.deleteJob(jobKey);
}
ScheduleUtils.createScheduleJob(scheduler, job);
}
/**
* 校验cron表达式是否有效
*
* @param cronExpression 表达式
* @return 结果
*/
@Override
public boolean checkCronExpressionIsValid(String cronExpression)
{
return CronUtils.isValid(cronExpression);
}
@Override
public List<Long> queryJobIdsByUserId(Integer userId) {
return jobMapper.queryJobIdsByUserId(userId);
}
}
/**
* 任务调度通用常量
*/
public class ScheduleConstants
{
public static final String TASK_CLASS_NAME = "TASK_CLASS_NAME";
/** 执行目标key */
public static final String TASK_PROPERTIES = "TASK_PROPERTIES";
/** 默认 */
public static final String MISFIRE_DEFAULT = "0";
/** 立即触发执行 */
public static final String MISFIRE_IGNORE_MISFIRES = "1";
/** 触发一次执行 */
public static final String MISFIRE_FIRE_AND_PROCEED = "2";
/** 不触发立即执行 */
public static final String MISFIRE_DO_NOTHING = "3";
public enum Status
{
/**
* 正常
*/
NORMAL("0"),
/**
* 暂停
*/
PAUSE("1");
private String value;
private Status(String value)
{
this.value = value;
}
public String getValue()
{
return value;
}
}
}
定时任务工具类:
/**
* 定时任务工具类
*
* @author foms
*
*/
public class ScheduleUtils
{
/**
* 得到quartz任务类
*
* @param sysJob 执行计划
* @return 具体执行任务类
*/
private static Class<? extends Job> getQuartzJobClass(SysJob sysJob)
{
boolean isConcurrent = "0".equals(sysJob.getConcurrent());
return isConcurrent ? QuartzJobExecution.class : QuartzDisallowConcurrentExecution.class;
}
/**
* 构建任务触发对象
*/
public static TriggerKey getTriggerKey(Long jobId, String jobGroup)
{
//通过task类名获取对应的任务类容
return TriggerKey.triggerKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
}
/**
* 构建任务键对象
*/
public static JobKey getJobKey(Long jobId, String jobGroup)
{
return JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
}
/**
* 创建定时任务
*/
public static void createScheduleJob(Scheduler scheduler, SysJob job) throws SchedulerException, TaskException
{
Class<? extends Job> jobClass = getQuartzJobClass(job);
// 构建job信息
Long jobId = job.getJobId();
String jobGroup = job.getJobGroup();
JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(getJobKey(jobId, jobGroup)).build();
// 表达式调度构建器
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder);
// 按新的cronExpression表达式构建一个新的trigger
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(jobId, jobGroup))
.withSchedule(cronScheduleBuilder).build();
// 放入参数,运行时的方法可以获取
jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job);
// 判断是否存在
if (scheduler.checkExists(getJobKey(jobId, jobGroup)))
{
// 防止创建时存在数据问题 先移除,然后在执行创建操作
scheduler.deleteJob(getJobKey(jobId, jobGroup));
}
scheduler.scheduleJob(jobDetail, trigger);
// 暂停任务
if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue()))
{
scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup));
}
}
/**
* 设置定时任务策略
*/
public static CronScheduleBuilder handleCronScheduleMisfirePolicy(SysJob job, CronScheduleBuilder cb)
throws TaskException
{
switch (job.getMisfirePolicy())
{
case ScheduleConstants.MISFIRE_DEFAULT:
return cb;
case ScheduleConstants.MISFIRE_IGNORE_MISFIRES:
return cb.withMisfireHandlingInstructionIgnoreMisfires();
case ScheduleConstants.MISFIRE_FIRE_AND_PROCEED:
return cb.withMisfireHandlingInstructionFireAndProceed();
case ScheduleConstants.MISFIRE_DO_NOTHING:
return cb.withMisfireHandlingInstructionDoNothing();
default:
throw new TaskException("The task misfire policy '" + job.getMisfirePolicy()
+ "' cannot be used in cron schedule tasks", Code.CONFIG_ERROR);
}
}
/**
* 校验cron表达式是否能执行
* @param cron
* @return
*/
public static boolean isValidateCanDoExpression(String cron) throws ParseException {
//先校验cron表达式格式是否正确
if(!isValidExpression(cron)) {
return false;
}
CronTriggerImpl triggerImpl = new CronTriggerImpl();
try {
triggerImpl.setCronExpression(cron);
} catch (ParseException e) {
return false;
}
Date date = triggerImpl.computeFirstFireTime(null);
return date != null && date.after(new Date());
}
/**
* 校验cron表达式格式
* @param cron
* @return
*/
public static boolean isValidExpression(String cron){
if(StringUtils.isEmpty(cron)){
return false;
}
return CronExpression.isValidExpression(cron);
}
}
task类(自定义调度方法,方法名与前台参数相同)
给定时任务添加任务、具体的数据处理
@Component("parkingTimeOutTask")
public class ParkingTimeOutTask {
@Autowired
private ParkingTimeOutService parkingTimeOutService;
// 9.00-13.00 停车超时数据
public void executeParkingTimeOutData(Long deptId, String emails){
Date date = new Date();
String msg = "9.00-13.00停车超时数据";
log.info("9.00-13.00 停车超时统计数据 start ... [{}]", DateUtil.now());
new Timer().schedule(new TimerTask() {
@Override
public void run() {
log.info("停车超时统计数据执行时间 [{}]", DateUtil.now());
//ParkingTimeOutForm为超时数据查询参数表单,具体省略
ParkingTimeOutForm form = new ParkingTimeOutForm();
form.setDeptId(deptId);
//任务设置为在13:00执行,所以开始时间为当前时间(任务执行时的时间)减去240min
form.setBeginTime(DateUtil.formatDateTime(DateUtil.offsetMinute(date, -240)));
//结束时间为当前时间
form.setEndTime(DateUtil.formatDateTime(date));
//ParkingTimeOutService的实现方法,传入查询表单form,邮箱地址emails,邮件标题msg
parkingTimeOutService.executeParkingTimeOutData(form, emails,msg);
}
}, TimeUnit.MINUTES.toMillis(0));
log.info("停车超时统计数据 end");
}
}
接下来是数据处理,以及发送邮件的impl类:
@Slf4j
@Service
public class ParkingTimeOutServiceImpl implements IParkingTimeOutService {
//邮件参数自己设置
@Value("${邮件服务器的SMTP地址,可选,默认为smtp.<发件人邮箱后缀>}")
private String host;
@Value("${邮件服务器的SMTP端口,可选,默认25}")
private Integer port;
@Value("${发件人(必须正确,否则发送失败)}")
private String from;
@Value("${账号}")
private String user;
@Value("${密码(注意,某些邮箱需要为SMTP服务单独设置密码,详情查看相关帮助)}")
private String pass;
@Value("${使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。}")
private boolean starttlsEnable;
private MailAccount account;
@Autowired
private RemoteTimeOutSettingService TimeOutSettingService;
//邮箱参数
@PostConstruct
public void init(){
account = new MailAccount();
account.setHost(host);
account.setPort(port);
account.setUser(user);
account.setPass(pass);
account.setFrom(from);
account.setStarttlsEnable(starttlsEnable);
}
@Override
public void executeParkingTimeoutData(ParkingTimeOutForm form, String emails,String msg) {
//VParkingTimeout是xml表格报表参数类,发送的邮件附带的是此xml格式的文件,可按实际需求设定。remoteSettingService通过feign调用停车时间数据服务
//说明:这里的停车时间数据位于另外一个模块,需要用feign调用,调用方法这里不做介绍。
List<VParkingTimeout> list = remoteSettingService.getParkingTimeoutList(form).getData();
log.info(" %s点停车超时统计 list ===> [{}]", JSON.toJSONString(list));
//判断内容是否为空
if (CollUtil.isNotEmpty(list)) {
timeoutSettingDataWriteToExcel(msg, emails, list);
}else {
log.info("停车超时数据为空");
}
}
//将数据变成xml格式,添加到邮件发送的方法
private void timeoutSettingDataWriteToExcel(String subject, String emails, List<VParkingTimeout> list){
String path = FileUtil.getTmpDirPath()
+ File.separator
+ DateUtil.format(new Date(), DatePattern.PURE_DATETIME_PATTERN)
+ RandomUtil.randomNumbers(4)
+ ".xlsx";
File file = FileUtil.file(path);
ExcelWriter writer = new ExcelWriter(true, subject);
writer.addHeaderAlias("id","序号");
writer.addHeaderAlias("Name","车辆编号");
writer.addHeaderAlias("Number","车牌号");
writer.addHeaderAlias("driverNameBefore","停车前驾驶员姓名");
writer.addHeaderAlias("driverIcBefore","停机前驾驶员工号");
writer.addHeaderAlias("startTime","停机开始时间");
writer.addHeaderAlias("shutdown","停机结束时间");
writer.addHeaderAlias("parkingTime","停机时长");
writer.addHeaderAlias("driverNameAfter","停机后驾驶员姓名");
writer.addHeaderAlias("driverIcAfter","停机后驾驶员工号");
writer.write(list);
writer.flush(file);
String content = String.format("尊敬的客户您好:为你统计的数据为%s,详情请查看附件", subject);
MailUtil.send(account, emails, subject, content, false, file);
log.info("邮件发送成功");
}
}
车辆数据模块的超时数据获取impl
@Override
public List<VParkingTimeout> getParkingTimeoutList(ParkingTimeOutForm form) {
long t = System.currentTimeMillis();
//获取设定超时时间
//这里也使用feign调用最开始设置的那张超时设置单表的模块的方法
Long timeoutSetting = (remoteSysReportParameterService.getParameter(form.getDeptId())).getData();
//FmsCarDeviceRunRecordForm为车辆停车开车时间统计数据表单
FmsCarDeviceRunRecordForm runRecordForm = new FmsCarDeviceRunRecordForm();
List<VParkingTimeout> timeoutList = new ArrayList<>();
//通过信息表单设置查询表单
runRecordForm.setDeptId(form.getDeptId());//此处根据实际需求设置
runRecordForm.setStartTime(form.getBeginTime());
runRecordForm.setEndTime(form.getEndTime());
//通过表单信息获取数据集合
//当前类的selectFmsCarDeviceRunRecordListIntact为获取数据库中停车间隔超过设定值的数据的方法
List<FmsCarDeviceRunRecord> runRecordList = this.selectFmsCarDeviceRunRecordListIntact(runRecordForm);
// 停车间隔为,下次的开始时间减去本次的结束时间
if (CollUtil.isNotEmpty(runRecordList)){
Long n = 0L;
for (int i = 0; i < runRecordList.size()-1; i++) {
//停车间隔时间(单位:分钟)
long pTime = (runRecordList.get(i).getEndDateTime().getTime()-runRecordList.get(i+1).getStartDateTime().getTime())/(1000*60);
//根据条件构建数据并添加到集合
if(pTime>timeoutSetting){
VParkingTimeout timeout = new VParkingTimeout();
//设置参数
n++;
timeout.setId(n);
timeout.setDriverIcBefore(runRecordList.get(i).getDriverIc());
timeout.setDriverIcAfter(runRecordList.get(i+1).getDriverIc());
timeout.setDriverNameBefore(runRecordList.get(i).getFmsDriver().getDriverName());
timeout.setDriverNameAfter(runRecordList.get(i+1).getFmsDriver().getDriverName());
timeout.setCarName(runRecordList.get(i).getFmsCar().getCarName());
timeout.setLicencePlateNumber(runRecordList.get(i).getFmsCar().getLicensePlateNumber());
timeout.setShutdown(runRecordList.get(i).getEndDateTime().toString());
timeout.setStartTime(runRecordList.get(i+1).getStartDateTime().toString());
timeout.setParkingTime(pTime);
//添加到集合
timeoutList.add(timeout);
}
}
System.out.println("excution: " + (System.currentTimeMillis() - t) );
return timeoutList;
}
System.out.println("excution2: " + (System.currentTimeMillis() - t) );
return null;
}
timeout.setLicencePlateNumber(runRecordList.get(i).getFmsCar().getLicensePlateNumber());
timeout.setShutdown(runRecordList.get(i).getEndDateTime().toString());
timeout.setStartTime(runRecordList.get(i+1).getStartDateTime().toString());
timeout.setParkingTime(pTime);
//添加到集合
timeoutList.add(timeout);
}
}
System.out.println("excution: " + (System.currentTimeMillis() - t) );
return timeoutList;
}
System.out.println("excution2: " + (System.currentTimeMillis() - t) );
return null;
}
至此,整个业务的核心代码与逻辑处理完毕。