Quartz 定时任务「新增功能」完整踩坑记:从 Cron 格式到事务回滚
一、背景介绍
在开发定时任务管理系统时,前端需要支持用户新增定时任务。功能包括:任务名称、调用方法、Cron 表达式、并发策略、状态等。本文将从零到一记录完整的开发过程,包括前端校验和后端逻辑的所有踩坑经历,希望能帮助遇到类似问题的开发者。
技术栈:
-
前端:Vue 2 + Element UI
-
后端:Spring Boot + MyBatis + Quartz
-
数据库:SQL Server
二、最终效果预览
text
┌─────────────────────────────────────────────────────────────┐ │ Add Job │ ├─────────────────────────────────────────────────────────────┤ │ Job Name: [测试任务 ] │ │ Job Group: [System ▼] │ │ Invoke Method: [taskService.sync() ] │ │ Format: beanName.methodName() │ │ Cron Expression: [0 0 0 * * ? ] [Generate] │ │ ✓ Valid cron expression │ │ Concurrent: ● Allow Concurrent ○ Disallow │ │ Status: ● Enabled ○ Disabled │ ├─────────────────────────────────────────────────────────────┤ │ [Cancel] [OK] │ └─────────────────────────────────────────────────────────────┘
三、踩坑历程
坑1:Cron 表达式格式错误
现象:用户输入 0 0 * * ?(5位),后端 Quartz 需要 6 位(秒 分 时 日 月 星期)
text
CronExpression '0 0 * * ?' is invalid
原因:前端没有校验 Cron 格式
解决方案:添加前端格式校验
javascript
checkCron() {
const cron = this.formData.cronExpression
if (!cron) return
const parts = cron.trim().split(/\s+/)
if (parts.length !== 6) {
this.cronValid = false
this.cronErrorMessage = 'Cron must have 6 fields: second minute hour day month week'
return
}
// 秒、分、时范围校验
const second = parseInt(parts[0])
const minute = parseInt(parts[1])
const hour = parseInt(parts[2])
if (second < 0 || second > 59) {
this.cronValid = false
this.cronErrorMessage = 'Second must be between 0 and 59'
return
}
// ... 分钟、小时类似校验
}
坑2:用户不知道 Cron 怎么写
现象:用户面对空输入框,不知道填什么格式
原因:缺少提示和示例
解决方案:添加 placeholder 和提示文字
html
<el-input v-model="formData.cronExpression" placeholder="e.g., 0 0 0 * * ?" /> <div class="cron-hint">Format: second minute hour day month week</div> <div class="cron-examples"> <span>Examples: 0 0 0 * * ? (daily 00:00)</span> <span>0 0 2 ? * SUN (weekly Sunday 02:00)</span> </div>
坑3:手写 Cron 容易出错
现象:用户写 0 0 0 * * ?(中文问号)或 0 0 0 * *(少字段)
原因:纯手工输入,没有辅助工具
解决方案:添加 Cron 生成器弹窗
html
<el-popover trigger="click" v-model="cronPopoverVisible" placement="bottom" width="400">
<div class="cron-generator">
<div class="cron-row">
<span class="cron-label">Second (0-59):</span>
<el-select v-model="cronSecond" size="mini" clearable>
<el-option v-for="i in 59" :key="i" :label="i" :value="i" />
</el-select>
</div>
<div class="cron-row">
<span class="cron-label">Minute (0-59):</span>
<el-select v-model="cronMinute" size="mini" clearable>
<el-option v-for="i in 59" :key="i" :label="i" :value="i" />
</el-select>
</div>
<!-- 小时、日期、月份、星期选择器... -->
<div class="cron-preview">
<strong>Cron:</strong> {{ generatedCron }}
</div>
<div style="text-align: right;">
<el-button size="mini" @click="cronPopoverVisible = false">Cancel</el-button>
<el-button type="primary" size="mini" @click="applyCron">Apply</el-button>
</div>
</div>
<el-button slot="reference" type="text" icon="el-icon-time">Generate</el-button>
</el-popover>
javascript
computed: {
generatedCron() {
return `${this.cronSecond} ${this.cronMinute} ${this.cronHour} ${this.cronDay} ${this.cronMonth} ${this.cronWeek}`
}
},
methods: {
applyCron() {
this.formData.cronExpression = this.generatedCron
this.cronPopoverVisible = false
this.checkCron()
}
}
坑4:日期和星期冲突
现象:用户同时选择了日期(如 15 号)和星期(如 Monday),Quartz 报错
text
'?' can only be specified for Day-of-Month or Day-of-Week
原因:Cron 规则中,日期和星期不能同时指定
解决方案:添加互斥逻辑
javascript
data() {
return {
cronDay: '*',
cronWeek: '?'
}
},
methods: {
onDayChange() {
if (this.cronDay !== '?' && this.cronDay !== '*') {
this.cronWeek = '?'
}
},
onWeekChange() {
if (this.cronWeek !== '?' && this.cronWeek !== '*') {
this.cronDay = '?'
}
}
}
html
<div class="cron-row">
<span class="cron-label">Day of Month (1-31):</span>
<el-select v-model="cronDay" @change="onDayChange">
<el-option label="Every day" value="*" />
<el-option label="?" value="?" />
<el-option v-for="i in 31" :key="i" :label="i" :value="i" />
</el-select>
<span class="cron-hint" v-if="cronDay !== '?' && cronDay !== '*'">(Week will be set to ?)</span>
</div>
坑5:Bean 名大小写问题
现象:用户输入 TaskService.sync()(首字母大写),后端找不到 Bean
text
Bean not found: TaskService
原因:Spring Bean 默认首字母小写,TaskService 应该是 taskService
解决方案:前端添加格式提示,后端自动转换
html
<div class="form-tip"> Format: beanName.methodName() (lowercase first letter) <br>Example: taskService.syncMonthLeepaOrder() </div>
java
// 后端自动将首字母转小写 String className = invokeTarget.substring(0, dotIndex); String beanName = Character.toLowerCase(className.charAt(0)) + className.substring(1); Object bean = SpringUtils.getBean(beanName);
坑6:方法名错误
现象:用户输入 taskService.syncOrder(),但实际方法名是 syncLeepaOrder
text
Method not found: syncOrder()
原因:用户不知道有哪些可用方法,也没有实时校验
解决方案:添加实时校验,输入框失焦时校验
html
<el-input
v-model="formData.invokeTarget"
placeholder="e.g., taskService.syncMonthLeepaOrder()"
@blur="checkInvokeTarget"
/>
<div v-if="invokeTargetValid === false" class="error-tip">
{{ invokeTargetError }}
</div>
javascript
async checkInvokeTarget() {
const invokeTarget = this.formData.invokeTarget
if (!invokeTarget) {
this.invokeTargetValid = null
return
}
// 格式校验
const dotIndex = invokeTarget.indexOf('.')
const paramStart = invokeTarget.indexOf('(')
const paramEnd = invokeTarget.lastIndexOf(')')
if (dotIndex === -1 || paramStart === -1 || paramEnd === -1) {
this.invokeTargetValid = false
this.invokeTargetError = 'Invalid format. Expected: beanName.methodName()'
return
}
// 调用后端校验接口
const res = await this.axios.post('/scheduler/job/validate', {
invokeTarget: invokeTarget
})
if (res.data && res.data.valid) {
this.invokeTargetValid = true
this.invokeTargetError = ''
} else {
this.invokeTargetValid = false
this.invokeTargetError = res.data.message
}
}
坑7:id 为 null 导致 Quartz 创建失败(重点!)
现象:新增任务时,Quartz 创建失败
text
java.lang.NullPointerException at SchedulerJobService.createScheduleJob(SchedulerJobService.java:62)
原因:代码顺序错误,先创建 Quartz 任务,后插入数据库。此时 job.id 还是 null
错误代码:
java
// ❌ 错误顺序 jobService.createScheduleJob(job); // job.id 还是 null! jobMapper.insertJob(job);
正确顺序:
java
// ✅ 正确顺序 jobMapper.insertJob(job); // 先插入数据库,获取自增 ID jobService.createScheduleJob(job); // 此时 job.id 已有值
关键点:MyBatis 的 @Options(useGeneratedKeys = true) 会在插入后自动回填 ID
java
@Insert("INSERT INTO ELAB_SCHEDULER_JOB (jobName, jobGroup, invokeTarget, cronExpression, concurrent, status, createBy, createTime) VALUES " +
"(#{jobName}, #{jobGroup}, #{invokeTarget}, #{cronExpression}, #{concurrent}, #{status}, #{createBy}, GETDATE())")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insertJob(SchedulerJob job);
坑8:Quartz 创建失败时数据库记录没有回滚
现象:Quartz 创建失败(如方法名错误),但数据库已经有记录了
原因:没有事务控制,数据库插入成功,但 Quartz 任务创建失败
解决方案:添加事务,失败时自动回滚
java
@PostMapping("/add")
@Transactional(transactionManager = "eLab_TransactionManager", rollbackFor = Exception.class)
public WebResult add(@RequestBody SchedulerJob job) {
WebResult result = new WebResult();
job.setCreateBy(getCurrentUser());
// 1. 插入数据库
int insertResult = jobMapper.insertJob(job);
if (insertResult <= 0) {
result.error("Failed to insert job");
return result;
}
// 2. 创建 Quartz 任务(失败时事务自动回滚数据库)
boolean success = jobService.createScheduleJob(job);
if (success) {
result.setMessage("Job added successfully");
} else {
result.error("Failed to schedule job");
}
return result;
}
坑9:重复提交
现象:用户快速连续点击 OK 按钮,发送了多个请求
原因:没有防重复提交机制
解决方案:添加 submitting 状态
javascript
data() {
return {
submitting: false
}
},
methods: {
async submitJob() {
if (this.submitting) return
this.$refs.jobForm.validate(async (valid) => {
if (!valid) return
if (this.cronValid !== true) {
this.$message.error('Please enter a valid cron expression')
return
}
this.submitting = true
try {
const res = await this.axios.post('/scheduler/job/add', this.formData)
if (res.data && res.data.status === 200) {
this.$message.success('Job added')
this.dialogVisible = false
this.loadJobList()
} else {
this.$message.error(res.data.message || 'Operation failed')
}
} catch (err) {
this.$message.error('Operation failed')
} finally {
this.submitting = false
}
})
}
}
html
<el-button type="primary" @click="submitJob" :loading="submitting">OK</el-button>
坑10:编辑时传了不该传的字段
现象:编辑任务时,后端报错
text
Failed to convert property value of type 'java.lang.String' to required type 'java.util.Date' for property 'createTime'
原因:{ ...row } 把 createTime、updateTime 也复制了
错误代码:
javascript
// ❌ 错误写法
this.formData = { ...row }
正确写法:
javascript
// ✅ 正确写法
this.formData = {
id: row.id,
jobName: row.jobName,
jobGroup: row.jobGroup,
invokeTarget: row.invokeTarget,
cronExpression: row.cronExpression,
concurrent: row.concurrent,
status: row.status
}
四、最终校验流程图
text
用户点击 OK
│
▼
submitting 检查 ───► 如果正在提交,忽略
│
▼
表单验证 ───► 失败 ───► 显示错误提示
│
▼ 通过
Cron 格式校验 ───► 失败 ───► 显示红色错误
│
▼ 通过
显示绿色 ✓ Valid cron expression
│
▼
Invoke Method 校验 ───► 失败 ───► 显示错误
│
▼ 通过
提交请求
│
├──► 成功 ───► 关闭弹窗,刷新列表
│
└──► 失败 ───► 显示错误信息
│
▼
submitting = false(恢复按钮)
五、完整代码
前端 TaskSchedule.vue(核心部分)
vue
<template>
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="560px" @close="resetForm">
<el-form ref="jobForm" :model="formData" :rules="formRules" label-width="120px" size="small">
<el-form-item label="Job Name" prop="jobName">
<el-input v-model="formData.jobName" placeholder="Enter job name" />
</el-form-item>
<el-form-item label="Job Group" prop="jobGroup">
<el-select v-model="formData.jobGroup" placeholder="Select job group">
<el-option label="System" value="DEFAULT" />
<el-option label="Custom" value="CUSTOM" />
</el-select>
</el-form-item>
<el-form-item label="Invoke Method" prop="invokeTarget">
<el-input
v-model="formData.invokeTarget"
placeholder="e.g., taskService.syncMonthLeepaOrder()"
@blur="checkInvokeTarget"
/>
<div class="form-tip">
Format: beanName.methodName() (lowercase first letter)
</div>
<div v-if="invokeTargetValid === false" class="error-tip">
{{ invokeTargetError }}
</div>
</el-form-item>
<el-form-item label="Cron Expression" prop="cronExpression">
<el-input
v-model="formData.cronExpression"
placeholder="e.g., 0 0 0 * * ?"
@blur="checkCron"
style="width: 100%"
/>
<el-button type="text" size="mini" @click="cronPopoverVisible = true" icon="el-icon-time">
Generate
</el-button>
<div v-if="cronValid === false" class="error-tip">
{{ cronErrorMessage }}
</div>
<div v-if="cronValid === true" class="success-tip">
✓ Valid cron expression
</div>
<!-- Cron 生成器弹窗 -->
<el-popover
placement="bottom"
width="400"
trigger="click"
v-model="cronPopoverVisible"
>
<div class="cron-generator">
<div class="cron-row">
<span class="cron-label">Second (0-59):</span>
<el-select v-model="cronSecond" size="mini" clearable>
<el-option label="0" value="0" />
<el-option v-for="i in 59" :key="i" :label="i" :value="i" />
</el-select>
</div>
<div class="cron-row">
<span class="cron-label">Minute (0-59):</span>
<el-select v-model="cronMinute" size="mini" clearable>
<el-option label="Every minute" value="*" />
<el-option v-for="i in 59" :key="i" :label="i" :value="i" />
</el-select>
</div>
<div class="cron-row">
<span class="cron-label">Hour (0-23):</span>
<el-select v-model="cronHour" size="mini" clearable>
<el-option label="Every hour" value="*" />
<el-option v-for="i in 23" :key="i" :label="i" :value="i" />
</el-select>
</div>
<div class="cron-row">
<span class="cron-label">Day of Month (1-31):</span>
<el-select
v-model="cronDay"
size="mini"
clearable
@change="onDayChange"
>
<el-option label="Every day" value="*" />
<el-option label="?" value="?" />
<el-option v-for="i in 31" :key="i" :label="i" :value="i" />
</el-select>
<span class="cron-hint" v-if="cronDay !== '?' && cronDay !== '*'">(Week will be ?)</span>
</div>
<div class="cron-row">
<span class="cron-label">Month (1-12):</span>
<el-select v-model="cronMonth" size="mini" clearable>
<el-option label="Every month" value="*" />
<el-option v-for="i in 12" :key="i" :label="i" :value="i" />
</el-select>
</div>
<div class="cron-row">
<span class="cron-label">Day of Week (0-7):</span>
<el-select
v-model="cronWeek"
size="mini"
clearable
@change="onWeekChange"
>
<el-option label="Every day" value="*" />
<el-option label="?" value="?" />
<el-option label="Sunday" value="0" />
<el-option label="Monday" value="1" />
<el-option label="Tuesday" value="2" />
<el-option label="Wednesday" value="3" />
<el-option label="Thursday" value="4" />
<el-option label="Friday" value="5" />
<el-option label="Saturday" value="6" />
</el-select>
<span class="cron-hint" v-if="cronWeek !== '?' && cronWeek !== '*'">(Day will be ?)</span>
</div>
<div class="cron-preview">
<strong>Cron:</strong> {{ generatedCron }}
</div>
<div style="text-align: right; margin-top: 10px;">
<el-button size="mini" @click="cronPopoverVisible = false">Cancel</el-button>
<el-button type="primary" size="mini" @click="applyCron">Apply</el-button>
</div>
</div>
<el-button slot="reference" type="text" size="mini" icon="el-icon-time">Generate</el-button>
</el-popover>
</el-form-item>
<el-form-item label="Concurrent" prop="concurrent">
<el-radio-group v-model="formData.concurrent">
<el-radio label="1">Allow Concurrent</el-radio>
<el-radio label="0">Disallow Concurrent</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="Status" prop="status">
<el-radio-group v-model="formData.status">
<el-radio label="0">Enabled</el-radio>
<el-radio label="1">Disabled</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="submitJob" :loading="submitting">OK</el-button>
</span>
</el-dialog>
</template>
<script>
import cronParser from 'cron-parser'
export default {
name: 'TaskSchedule',
data() {
return {
submitting: false,
dialogVisible: false,
dialogTitle: '',
cronValid: null,
cronErrorMessage: '',
invokeTargetValid: null,
invokeTargetError: '',
cronPopoverVisible: false,
cronSecond: '0',
cronMinute: '0',
cronHour: '0',
cronDay: '*',
cronMonth: '*',
cronWeek: '?',
formData: {
id: null,
jobName: '',
jobGroup: 'DEFAULT',
invokeTarget: '',
cronExpression: '',
concurrent: '1',
status: '0'
},
formRules: {
jobName: [{ required: true, message: 'Job name is required', trigger: 'blur' }],
jobGroup: [{ required: true, message: 'Job group is required', trigger: 'change' }],
invokeTarget: [{ required: true, message: 'Invoke method is required', trigger: 'blur' }],
cronExpression: [{ required: true, message: 'Cron expression is required', trigger: 'blur' }]
}
}
},
computed: {
generatedCron() {
return `${this.cronSecond} ${this.cronMinute} ${this.cronHour} ${this.cronDay} ${this.cronMonth} ${this.cronWeek}`
}
},
methods: {
checkCron() {
const cron = this.formData.cronExpression
if (!cron) {
this.cronValid = null
this.cronErrorMessage = ''
return
}
try {
cronParser.parseExpression(cron)
this.cronValid = true
this.cronErrorMessage = ''
} catch (err) {
this.cronValid = false
this.cronErrorMessage = err.message
}
},
checkInvokeTarget() {
const invokeTarget = this.formData.invokeTarget
if (!invokeTarget) {
this.invokeTargetValid = null
this.invokeTargetError = ''
return
}
const dotIndex = invokeTarget.indexOf('.')
const paramStart = invokeTarget.indexOf('(')
const paramEnd = invokeTarget.lastIndexOf(')')
if (dotIndex === -1 || paramStart === -1 || paramEnd === -1) {
this.invokeTargetValid = false
this.invokeTargetError = 'Invalid format. Expected: beanName.methodName()'
return
}
const className = invokeTarget.substring(0, dotIndex)
const methodName = invokeTarget.substring(dotIndex + 1, paramStart)
if (!className || !methodName) {
this.invokeTargetValid = false
this.invokeTargetError = 'Class name and method name cannot be empty'
return
}
this.invokeTargetValid = true
this.invokeTargetError = ''
},
applyCron() {
this.formData.cronExpression = this.generatedCron
this.cronPopoverVisible = false
this.checkCron()
},
onDayChange() {
if (this.cronDay !== '?' && this.cronDay !== '*') {
this.cronWeek = '?'
}
},
onWeekChange() {
if (this.cronWeek !== '?' && this.cronWeek !== '*') {
this.cronDay = '?'
}
},
handleAdd() {
this.dialogTitle = 'Add Job'
this.formData = {
id: null,
jobName: '',
jobGroup: 'DEFAULT',
invokeTarget: '',
cronExpression: '',
concurrent: '1',
status: '0'
}
this.cronValid = null
this.cronErrorMessage = ''
this.invokeTargetValid = null
this.invokeTargetError = ''
this.dialogVisible = true
},
async submitJob() {
if (this.submitting) return
this.$refs.jobForm.validate(async (valid) => {
if (!valid) return
if (!this.formData.cronExpression) {
this.$message.error('Please enter cron expression')
return
}
if (this.cronValid !== true) {
this.$message.error('Please enter a valid cron expression')
return
}
this.submitting = true
const isEdit = !!this.formData.id
const url = isEdit ? '/scheduler/job/update' : '/scheduler/job/add'
const submitData = {
jobName: this.formData.jobName,
jobGroup: this.formData.jobGroup,
invokeTarget: this.formData.invokeTarget,
cronExpression: this.formData.cronExpression,
concurrent: this.formData.concurrent,
status: this.formData.status
}
if (isEdit) {
submitData.id = this.formData.id
}
try {
const res = await this.axios.post(url, submitData)
if (res.data && res.data.status === 200) {
this.$message.success(isEdit ? 'Job updated' : 'Job added')
this.dialogVisible = false
this.loadJobList()
} else {
this.$message.error(res.data.message || 'Operation failed')
}
} catch (err) {
console.error(err)
this.$message.error('Operation failed')
} finally {
this.submitting = false
}
})
},
resetForm() {
if (this.$refs.jobForm) {
this.$refs.jobForm.resetFields()
}
this.cronValid = null
this.cronErrorMessage = ''
this.invokeTargetValid = null
this.invokeTargetError = ''
}
}
}
</script>
<style lang="scss" scoped>
.form-tip {
color: #909399;
font-size: 12px;
margin-top: 4px;
}
.error-tip {
color: #f56c6c;
font-size: 12px;
margin-top: 4px;
}
.success-tip {
color: #67c23a;
font-size: 12px;
margin-top: 4px;
}
.cron-generator {
.cron-row {
display: flex;
align-items: center;
margin-bottom: 10px;
.cron-label {
width: 130px;
font-size: 12px;
}
.el-select {
width: 120px;
}
.cron-hint {
margin-left: 10px;
font-size: 11px;
color: #909399;
}
}
.cron-preview {
margin-top: 10px;
padding: 8px;
background-color: #f5f7fa;
border-radius: 4px;
font-size: 12px;
word-break: break-all;
}
}
</style>
后端 Controller(核心部分)
java
@RestController
@RequestMapping("/scheduler/job")
public class SchedulerJobController {
@Autowired
private SchedulerJobService jobService;
@Autowired
private SchedulerJobMapper jobMapper;
/**
* 新增任务
*/
@PostMapping("/add")
@Transactional(transactionManager = "eLab_TransactionManager", rollbackFor = Exception.class)
public WebResult add(@RequestBody SchedulerJob job) {
WebResult result = new WebResult();
// 1. 设置创建人
job.setCreateBy(getCurrentUser());
// 2. 先插入数据库(获取自增 ID)
int insertResult = jobMapper.insertJob(job);
if (insertResult <= 0) {
result.error("Failed to insert job");
return result;
}
// 3. 创建 Quartz 任务(如果失败,事务自动回滚数据库)
boolean success = jobService.createScheduleJob(job);
if (success) {
result.setMessage("Job added successfully");
} else {
result.error("Failed to schedule job");
}
return result;
}
private String getCurrentUser() {
// 从 token 获取当前登录用户
return "system";
}
}
Service(Quartz 任务创建)
java
@Service
public class SchedulerJobService {
@Autowired
private Scheduler scheduler;
public boolean createScheduleJob(SchedulerJob job) throws SchedulerException {
// 1. 校验 invokeTarget
if (!isValidateInvokeTarget(job.getInvokeTarget())) {
return false;
}
// 2. 创建 JobDetail
JobDetail jobDetail = JobBuilder.newJob(QuartzJobExecution.class)
.withIdentity(job.getId().toString(), job.getJobGroup())
.build();
jobDetail.getJobDataMap().put("JOB_PARAM_KEY", job);
// 3. 创建 Trigger
CronScheduleBuilder cronBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity(job.getId().toString(), job.getJobGroup())
.withSchedule(cronBuilder)
.build();
// 4. 调度任务
scheduler.scheduleJob(jobDetail, trigger);
// 5. 如果任务是暂停状态
if ("1".equals(job.getStatus())) {
scheduler.pauseJob(jobDetail.getKey());
}
return true;
}
private boolean isValidateInvokeTarget(String invokeTarget) {
int dotIndex = invokeTarget.indexOf(".");
int paramStart = invokeTarget.indexOf("(");
if (dotIndex == -1 || paramStart == -1) {
log.info("Invalid format. Expected: beanName.methodName()");
return false;
}
String className = invokeTarget.substring(0, dotIndex);
String methodName = invokeTarget.substring(dotIndex + 1, paramStart);
String beanName = Character.toLowerCase(className.charAt(0)) + className.substring(1);
// 校验 Bean 是否存在
Object bean;
try {
bean = SpringUtils.getBean(beanName);
} catch (Exception e) {
log.info("Bean not found: " + beanName);
return false;
}
// 校验方法是否存在
try {
bean.getClass().getDeclaredMethod(methodName);
return true;
} catch (NoSuchMethodException e) {
log.info("Method not found: " + methodName + "()");
return false;
}
}
}
六、总结
| 坑 | 问题 | 解决方案 |
|---|---|---|
| 1 | Cron 格式错误 | 添加格式校验 |
| 2 | 用户不会写 Cron | 添加 placeholder 提示 |
| 3 | 手写容易出错 | 添加 Cron 生成器 |
| 4 | 日期星期冲突 | 添加互斥逻辑 |
| 5 | Bean 名大小写 | 后端自动转换首字母小写 |
| 6 | 方法名错误 | 添加实时校验 |
| 7 | id 为 null | 先插入数据库,再创建 Quartz 任务 |
| 8 | 事务回滚 | 添加 @Transactional |
| 9 | 重复提交 | 添加 submitting 状态 |
| 10 | 编辑传多余字段 | 只复制需要的字段 |
核心原则:提前校验,实时反馈,先存数据再建任务,事务保证一致性!
希望这篇文章能帮助到遇到类似问题的开发者。如有疑问,欢迎评论区交流!🎯
更多推荐


所有评论(0)