Commit 1274f925449e12f4fff9105d52cfc698ff5e3592

Authored by YunaiV
1 parent 62c6c4b9

优化 job 调度:

1. 新增或修改 job 配置时,校验 handlerName 对应的 Spring Bean 存在
2. 删除 job 时,额外暂停 Trigger、取消调度,更完善
yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/core/scheduler/SchedulerManager.java
@@ -48,7 +48,7 @@ public class SchedulerManager { @@ -48,7 +48,7 @@ public class SchedulerManager {
48 .withIdentity(jobHandlerName).build(); 48 .withIdentity(jobHandlerName).build();
49 // 创建 Trigger 对象 49 // 创建 Trigger 对象
50 Trigger trigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval); 50 Trigger trigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval);
51 - // 新增调度 51 + // 新增 Job 调度
52 scheduler.scheduleJob(jobDetail, trigger); 52 scheduler.scheduleJob(jobDetail, trigger);
53 } 53 }
54 54
@@ -80,6 +80,10 @@ public class SchedulerManager { @@ -80,6 +80,10 @@ public class SchedulerManager {
80 */ 80 */
81 public void deleteJob(String jobHandlerName) throws SchedulerException { 81 public void deleteJob(String jobHandlerName) throws SchedulerException {
82 validateScheduler(); 82 validateScheduler();
  83 + // 暂停 Trigger 对象
  84 + scheduler.pauseTrigger(new TriggerKey(jobHandlerName));
  85 + // 取消并删除 Job 调度
  86 + scheduler.unscheduleJob(new TriggerKey(jobHandlerName));
83 scheduler.deleteJob(new JobKey(jobHandlerName)); 87 scheduler.deleteJob(new JobKey(jobHandlerName));
84 } 88 }
85 89
yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java
@@ -22,6 +22,8 @@ public interface ErrorCodeConstants { @@ -22,6 +22,8 @@ public interface ErrorCodeConstants {
22 ErrorCode JOB_CHANGE_STATUS_EQUALS = new ErrorCode(1_001_001_003, "定时任务已经处于该状态,无需修改"); 22 ErrorCode JOB_CHANGE_STATUS_EQUALS = new ErrorCode(1_001_001_003, "定时任务已经处于该状态,无需修改");
23 ErrorCode JOB_UPDATE_ONLY_NORMAL_STATUS = new ErrorCode(1_001_001_004, "只有开启状态的任务,才可以修改"); 23 ErrorCode JOB_UPDATE_ONLY_NORMAL_STATUS = new ErrorCode(1_001_001_004, "只有开启状态的任务,才可以修改");
24 ErrorCode JOB_CRON_EXPRESSION_VALID = new ErrorCode(1_001_001_005, "CRON 表达式不正确"); 24 ErrorCode JOB_CRON_EXPRESSION_VALID = new ErrorCode(1_001_001_005, "CRON 表达式不正确");
  25 + ErrorCode JOB_HANDLER_BEAN_NOT_EXISTS = new ErrorCode(1_001_001_006, "定时任务的处理器 Bean 不存在");
  26 + ErrorCode JOB_HANDLER_BEAN_TYPE_ERROR = new ErrorCode(1_001_001_007, "定时任务的处理器 Bean 类型不正确,未实现 JobHandler 接口");
25 27
26 // ========== API 错误日志 1-001-002-000 ========== 28 // ========== API 错误日志 1-001-002-000 ==========
27 ErrorCode API_ERROR_LOG_NOT_FOUND = new ErrorCode(1_001_002_000, "API 错误日志不存在"); 29 ErrorCode API_ERROR_LOG_NOT_FOUND = new ErrorCode(1_001_002_000, "API 错误日志不存在");
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/job/JobServiceImpl.java
1 package cn.iocoder.yudao.module.infra.service.job; 1 package cn.iocoder.yudao.module.infra.service.job;
2 2
  3 +import cn.hutool.extra.spring.SpringUtil;
3 import cn.iocoder.yudao.framework.common.pojo.PageResult; 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  6 +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
5 import cn.iocoder.yudao.framework.quartz.core.scheduler.SchedulerManager; 7 import cn.iocoder.yudao.framework.quartz.core.scheduler.SchedulerManager;
6 import cn.iocoder.yudao.framework.quartz.core.util.CronUtils; 8 import cn.iocoder.yudao.framework.quartz.core.util.CronUtils;
7 import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobPageReqVO; 9 import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobPageReqVO;
@@ -9,13 +11,12 @@ import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobSaveReqVO; @@ -9,13 +11,12 @@ import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobSaveReqVO;
9 import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobDO; 11 import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobDO;
10 import cn.iocoder.yudao.module.infra.dal.mysql.job.JobMapper; 12 import cn.iocoder.yudao.module.infra.dal.mysql.job.JobMapper;
11 import cn.iocoder.yudao.module.infra.enums.job.JobStatusEnum; 13 import cn.iocoder.yudao.module.infra.enums.job.JobStatusEnum;
  14 +import jakarta.annotation.Resource;
12 import org.quartz.SchedulerException; 15 import org.quartz.SchedulerException;
13 import org.springframework.stereotype.Service; 16 import org.springframework.stereotype.Service;
14 import org.springframework.transaction.annotation.Transactional; 17 import org.springframework.transaction.annotation.Transactional;
15 import org.springframework.validation.annotation.Validated; 18 import org.springframework.validation.annotation.Validated;
16 19
17 -import jakarta.annotation.Resource;  
18 -  
19 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 20 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
20 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.containsAny; 21 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.containsAny;
21 import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; 22 import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
@@ -39,24 +40,25 @@ public class JobServiceImpl implements JobService { @@ -39,24 +40,25 @@ public class JobServiceImpl implements JobService {
39 @Transactional(rollbackFor = Exception.class) 40 @Transactional(rollbackFor = Exception.class)
40 public Long createJob(JobSaveReqVO createReqVO) throws SchedulerException { 41 public Long createJob(JobSaveReqVO createReqVO) throws SchedulerException {
41 validateCronExpression(createReqVO.getCronExpression()); 42 validateCronExpression(createReqVO.getCronExpression());
42 - // 校验唯一性 43 + // 1.1 校验唯一性
43 if (jobMapper.selectByHandlerName(createReqVO.getHandlerName()) != null) { 44 if (jobMapper.selectByHandlerName(createReqVO.getHandlerName()) != null) {
44 throw exception(JOB_HANDLER_EXISTS); 45 throw exception(JOB_HANDLER_EXISTS);
45 } 46 }
46 - // 插入 47 + // 1.2 校验 JobHandler 是否存在
  48 + validateJobHandlerExists(createReqVO.getHandlerName());
  49 +
  50 + // 2. 插入 JobDO
47 JobDO job = BeanUtils.toBean(createReqVO, JobDO.class); 51 JobDO job = BeanUtils.toBean(createReqVO, JobDO.class);
48 job.setStatus(JobStatusEnum.INIT.getStatus()); 52 job.setStatus(JobStatusEnum.INIT.getStatus());
49 fillJobMonitorTimeoutEmpty(job); 53 fillJobMonitorTimeoutEmpty(job);
50 jobMapper.insert(job); 54 jobMapper.insert(job);
51 55
52 - // 添加 Job 到 Quartz 中 56 + // 3.1 添加 Job 到 Quartz 中
53 schedulerManager.addJob(job.getId(), job.getHandlerName(), job.getHandlerParam(), job.getCronExpression(), 57 schedulerManager.addJob(job.getId(), job.getHandlerName(), job.getHandlerParam(), job.getCronExpression(),
54 createReqVO.getRetryCount(), createReqVO.getRetryInterval()); 58 createReqVO.getRetryCount(), createReqVO.getRetryInterval());
55 - // 更新 59 + // 3.2 更新 JobDO
56 JobDO updateObj = JobDO.builder().id(job.getId()).status(JobStatusEnum.NORMAL.getStatus()).build(); 60 JobDO updateObj = JobDO.builder().id(job.getId()).status(JobStatusEnum.NORMAL.getStatus()).build();
57 jobMapper.updateById(updateObj); 61 jobMapper.updateById(updateObj);
58 -  
59 - // 返回  
60 return job.getId(); 62 return job.getId();
61 } 63 }
62 64
@@ -64,22 +66,35 @@ public class JobServiceImpl implements JobService { @@ -64,22 +66,35 @@ public class JobServiceImpl implements JobService {
64 @Transactional(rollbackFor = Exception.class) 66 @Transactional(rollbackFor = Exception.class)
65 public void updateJob(JobSaveReqVO updateReqVO) throws SchedulerException { 67 public void updateJob(JobSaveReqVO updateReqVO) throws SchedulerException {
66 validateCronExpression(updateReqVO.getCronExpression()); 68 validateCronExpression(updateReqVO.getCronExpression());
67 - // 校验存在 69 + // 1.1 校验存在
68 JobDO job = validateJobExists(updateReqVO.getId()); 70 JobDO job = validateJobExists(updateReqVO.getId());
69 - // 只有开启状态,才可以修改.原因是,如果出暂停状态,修改 Quartz Job 时,会导致任务又开始执行 71 + // 1.2 只有开启状态,才可以修改.原因是,如果出暂停状态,修改 Quartz Job 时,会导致任务又开始执行
70 if (!job.getStatus().equals(JobStatusEnum.NORMAL.getStatus())) { 72 if (!job.getStatus().equals(JobStatusEnum.NORMAL.getStatus())) {
71 throw exception(JOB_UPDATE_ONLY_NORMAL_STATUS); 73 throw exception(JOB_UPDATE_ONLY_NORMAL_STATUS);
72 } 74 }
73 - // 更新 75 + // 1.3 校验 JobHandler 是否存在
  76 + validateJobHandlerExists(updateReqVO.getHandlerName());
  77 +
  78 + // 2. 更新 JobDO
74 JobDO updateObj = BeanUtils.toBean(updateReqVO, JobDO.class); 79 JobDO updateObj = BeanUtils.toBean(updateReqVO, JobDO.class);
75 fillJobMonitorTimeoutEmpty(updateObj); 80 fillJobMonitorTimeoutEmpty(updateObj);
76 jobMapper.updateById(updateObj); 81 jobMapper.updateById(updateObj);
77 82
78 - // 更新 Job 到 Quartz 中 83 + // 3. 更新 Job 到 Quartz 中
79 schedulerManager.updateJob(job.getHandlerName(), updateReqVO.getHandlerParam(), updateReqVO.getCronExpression(), 84 schedulerManager.updateJob(job.getHandlerName(), updateReqVO.getHandlerParam(), updateReqVO.getCronExpression(),
80 updateReqVO.getRetryCount(), updateReqVO.getRetryInterval()); 85 updateReqVO.getRetryCount(), updateReqVO.getRetryInterval());
81 } 86 }
82 87
  88 + private void validateJobHandlerExists(String handlerName) {
  89 + Object handler = SpringUtil.getBean(handlerName);
  90 + if (handler == null) {
  91 + throw exception(JOB_HANDLER_BEAN_NOT_EXISTS);
  92 + }
  93 + if (!(handler instanceof JobHandler)) {
  94 + throw exception(JOB_HANDLER_BEAN_TYPE_ERROR);
  95 + }
  96 + }
  97 +
83 @Override 98 @Override
84 @Transactional(rollbackFor = Exception.class) 99 @Transactional(rollbackFor = Exception.class)
85 public void updateJobStatus(Long id, Integer status) throws SchedulerException { 100 public void updateJobStatus(Long id, Integer status) throws SchedulerException {
yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/job/JobServiceImplTest.java
1 package cn.iocoder.yudao.module.infra.service.job; 1 package cn.iocoder.yudao.module.infra.service.job;
2 2
  3 +import cn.hutool.extra.spring.SpringUtil;
3 import cn.iocoder.yudao.framework.common.pojo.PageResult; 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 import cn.iocoder.yudao.framework.quartz.core.scheduler.SchedulerManager; 5 import cn.iocoder.yudao.framework.quartz.core.scheduler.SchedulerManager;
5 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; 6 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
@@ -8,7 +9,9 @@ import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobSaveReqVO; @@ -8,7 +9,9 @@ import cn.iocoder.yudao.module.infra.controller.admin.job.vo.job.JobSaveReqVO;
8 import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobDO; 9 import cn.iocoder.yudao.module.infra.dal.dataobject.job.JobDO;
9 import cn.iocoder.yudao.module.infra.dal.mysql.job.JobMapper; 10 import cn.iocoder.yudao.module.infra.dal.mysql.job.JobMapper;
10 import cn.iocoder.yudao.module.infra.enums.job.JobStatusEnum; 11 import cn.iocoder.yudao.module.infra.enums.job.JobStatusEnum;
  12 +import cn.iocoder.yudao.module.infra.job.job.JobLogCleanJob;
11 import org.junit.jupiter.api.Test; 13 import org.junit.jupiter.api.Test;
  14 +import org.mockito.MockedStatic;
12 import org.quartz.SchedulerException; 15 import org.quartz.SchedulerException;
13 import org.springframework.boot.test.mock.mockito.MockBean; 16 import org.springframework.boot.test.mock.mockito.MockBean;
14 import org.springframework.context.annotation.Import; 17 import org.springframework.context.annotation.Import;
@@ -23,6 +26,7 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString @@ -23,6 +26,7 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
23 import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; 26 import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
24 import static org.junit.jupiter.api.Assertions.*; 27 import static org.junit.jupiter.api.Assertions.*;
25 import static org.mockito.ArgumentMatchers.eq; 28 import static org.mockito.ArgumentMatchers.eq;
  29 +import static org.mockito.Mockito.mockStatic;
26 import static org.mockito.Mockito.verify; 30 import static org.mockito.Mockito.verify;
27 31
28 @Import(JobServiceImpl.class) 32 @Import(JobServiceImpl.class)
@@ -35,6 +39,9 @@ public class JobServiceImplTest extends BaseDbUnitTest { @@ -35,6 +39,9 @@ public class JobServiceImplTest extends BaseDbUnitTest {
35 @MockBean 39 @MockBean
36 private SchedulerManager schedulerManager; 40 private SchedulerManager schedulerManager;
37 41
  42 + @MockBean
  43 + private JobLogCleanJob jobLogCleanJob;
  44 +
38 @Test 45 @Test
39 public void testCreateJob_cronExpressionValid() { 46 public void testCreateJob_cronExpressionValid() {
40 // 准备参数。Cron 表达式为 String 类型,默认随机字符串。 47 // 准备参数。Cron 表达式为 String 类型,默认随机字符串。
@@ -48,11 +55,15 @@ public class JobServiceImplTest extends BaseDbUnitTest { @@ -48,11 +55,15 @@ public class JobServiceImplTest extends BaseDbUnitTest {
48 public void testCreateJob_jobHandlerExists() throws SchedulerException { 55 public void testCreateJob_jobHandlerExists() throws SchedulerException {
49 // 准备参数 指定 Cron 表达式 56 // 准备参数 指定 Cron 表达式
50 JobSaveReqVO reqVO = randomPojo(JobSaveReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *")); 57 JobSaveReqVO reqVO = randomPojo(JobSaveReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *"));
51 -  
52 - // 调用  
53 - jobService.createJob(reqVO);  
54 - // 调用,并断言异常  
55 - assertServiceException(() -> jobService.createJob(reqVO), JOB_HANDLER_EXISTS); 58 + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
  59 + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(reqVO.getHandlerName())))
  60 + .thenReturn(jobLogCleanJob);
  61 +
  62 + // 调用
  63 + jobService.createJob(reqVO);
  64 + // 调用,并断言异常
  65 + assertServiceException(() -> jobService.createJob(reqVO), JOB_HANDLER_EXISTS);
  66 + }
56 } 67 }
57 68
58 @Test 69 @Test
@@ -60,18 +71,22 @@ public class JobServiceImplTest extends BaseDbUnitTest { @@ -60,18 +71,22 @@ public class JobServiceImplTest extends BaseDbUnitTest {
60 // 准备参数 指定 Cron 表达式 71 // 准备参数 指定 Cron 表达式
61 JobSaveReqVO reqVO = randomPojo(JobSaveReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *")) 72 JobSaveReqVO reqVO = randomPojo(JobSaveReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *"))
62 .setId(null); 73 .setId(null);
63 -  
64 - // 调用  
65 - Long jobId = jobService.createJob(reqVO);  
66 - // 断言  
67 - assertNotNull(jobId);  
68 - // 校验记录的属性是否正确  
69 - JobDO job = jobMapper.selectById(jobId);  
70 - assertPojoEquals(reqVO, job, "id");  
71 - assertEquals(JobStatusEnum.NORMAL.getStatus(), job.getStatus());  
72 - // 校验调用  
73 - verify(schedulerManager).addJob(eq(job.getId()), eq(job.getHandlerName()), eq(job.getHandlerParam()),  
74 - eq(job.getCronExpression()), eq(reqVO.getRetryCount()), eq(reqVO.getRetryInterval())); 74 + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
  75 + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(reqVO.getHandlerName())))
  76 + .thenReturn(jobLogCleanJob);
  77 +
  78 + // 调用
  79 + Long jobId = jobService.createJob(reqVO);
  80 + // 断言
  81 + assertNotNull(jobId);
  82 + // 校验记录的属性是否正确
  83 + JobDO job = jobMapper.selectById(jobId);
  84 + assertPojoEquals(reqVO, job, "id");
  85 + assertEquals(JobStatusEnum.NORMAL.getStatus(), job.getStatus());
  86 + // 校验调用
  87 + verify(schedulerManager).addJob(eq(job.getId()), eq(job.getHandlerName()), eq(job.getHandlerParam()),
  88 + eq(job.getCronExpression()), eq(reqVO.getRetryCount()), eq(reqVO.getRetryInterval()));
  89 + }
75 } 90 }
76 91
77 @Test 92 @Test
@@ -109,15 +124,19 @@ public class JobServiceImplTest extends BaseDbUnitTest { @@ -109,15 +124,19 @@ public class JobServiceImplTest extends BaseDbUnitTest {
109 o.setId(job.getId()); 124 o.setId(job.getId());
110 o.setCronExpression("0 0/1 * * * ? *"); 125 o.setCronExpression("0 0/1 * * * ? *");
111 }); 126 });
112 -  
113 - // 调用  
114 - jobService.updateJob(updateReqVO);  
115 - // 校验记录的属性是否正确  
116 - JobDO updateJob = jobMapper.selectById(updateReqVO.getId());  
117 - assertPojoEquals(updateReqVO, updateJob);  
118 - // 校验调用  
119 - verify(schedulerManager).updateJob(eq(job.getHandlerName()), eq(updateReqVO.getHandlerParam()),  
120 - eq(updateReqVO.getCronExpression()), eq(updateReqVO.getRetryCount()), eq(updateReqVO.getRetryInterval())); 127 + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
  128 + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(updateReqVO.getHandlerName())))
  129 + .thenReturn(jobLogCleanJob);
  130 +
  131 + // 调用
  132 + jobService.updateJob(updateReqVO);
  133 + // 校验记录的属性是否正确
  134 + JobDO updateJob = jobMapper.selectById(updateReqVO.getId());
  135 + assertPojoEquals(updateReqVO, updateJob);
  136 + // 校验调用
  137 + verify(schedulerManager).updateJob(eq(job.getHandlerName()), eq(updateReqVO.getHandlerParam()),
  138 + eq(updateReqVO.getCronExpression()), eq(updateReqVO.getRetryCount()), eq(updateReqVO.getRetryInterval()));
  139 + }
121 } 140 }
122 141
123 @Test 142 @Test