今天我们来讲一下编码规则的功能设计与开发。
1. 概述
在MES(制造执行系统)中,定时任务承担着数据同步、报表生成、设备状态检查等关键功能。本文档详细阐述基于Quartz的定时任务模块设计与实现,重点解决线程安全、任务调度策略等核心问题。
2. 定时任务框架选型
2.1 主流框架对比
| 特性 | Quartz | Spring @Scheduled | XXL-JOB | Timer |
|---|---|---|---|---|
| 分布式支持 | 需要额外配置 | 不支持 | 原生支持 | 不支持 |
| 动态任务管理 | 支持 | 不支持 | 支持 | 有限支持 |
| 失败处理策略 | 完善 | 简单 | 完善 | 有限 |
| 任务持久化 | 支持 | 不支持 | 支持 | 不支持 |
| Cron表达式支持 | 完整支持 | 支持 | 支持 | 不支持 |
| 线程池管理 | 可配置 | 简单 | 可配置 | 固定单线程 |
| 学习曲线 | 中等 | 简单 | 中等 | 简单 |
2.2 选择Quartz的原因
-
非分布式需求:当前MES系统为单实例部署,无需分布式调度能力
-
动态任务管理:支持运行时添加、修改、删除任务,适应MES需求变化
-
健壮的错误处理:提供多种misfire策略应对任务执行失败场景
-
企业级特性:作业持久化、事务支持、集群能力(为未来扩展预留)
-
成熟稳定:经过多年生产验证,社区支持完善
3. 系统架构设计
4. 核心功能实现
4.1 表结构
定时任务调度表(tool_job)
| 字段名 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | bigint | 主键,非空 | 任务ID |
| job_name | varchar(64) | 非空 | 任务名称 |
| job_group | varchar(64) | 非空 | 任务组名 |
| invoke_target | varchar(500) | 非空 | 调用目标字符串 |
| cron_expression | varchar(255) | 可空 | cron执行表达式 |
| misfire_policy | varchar(20) | 可空 | 计划执行错误策略(1立即执行 2执行一次 3放弃执行) |
| concurrent | char | 可空 | 是否并发执行(0允许 1禁止) |
| status | char | 可空 | 状态(0正常 1暂停) |
| create_by | bigint | 可空 | 创建者 |
| create_time | datetime | 可空 | 创建时间 |
| update_by | bigint | 可空 | 更新者 |
| update_time | datetime | 可空 | 更新时间 |
| remark | varchar(500) 默认'' | 可空 | 备注信息 |
| 定时任务调度表(tool_job) |
| 字段名 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | bigint | 主键,非空 | 任务ID |
| job_name | varchar(64) | 非空 | 任务名称 |
| job_group | varchar(64) | 非空 | 任务组名 |
| invoke_target | varchar(500) | 非空 | 调用目标字符串 |
| cron_expression | varchar(255) | 可空 | cron执行表达式 |
| misfire_policy | varchar(20) | 可空 | 计划执行错误策略(1立即执行 2执行一次 3放弃执行) |
| concurrent | char | 可空 | 是否并发执行(0允许 1禁止) |
| status | char | 可空 | 状态(0正常 1暂停) |
| create_by | bigint | 可空 | 创建者 |
| create_time | datetime | 可空 | 创建时间 |
| update_by | bigint | 可空 | 更新者 |
| update_time | datetime | 可空 | 更新时间 |
| remark | varchar(500) 默认'' | 可空 | 备注信息 |
4.2 定时任务执行类
/**
* 抽象quartz调用
*
* @author fwj
**/
public abstract class AbstractQuartzJob implements Job
{
private static final Logger log = LoggerFactory.getLogger(AbstractQuartzJob.class);
/**
* 线程本地变量
*/
private static ThreadLocal<LocalDateTime> threadLocal = new ThreadLocal<>();
@Override
public void execute(JobExecutionContext context) throws JobExecutionException
{
com.hgyc.mom.tool.entity.Job toolJob = new com.hgyc.mom.tool.entity.Job();
BeanUtils.copyBeanProp(toolJob, context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES));
try
{
before(context, toolJob);
if (toolJob != null)
{
doExecute(context, toolJob);
}
after(context, toolJob, null);
}
catch (Exception e)
{
log.error("任务执行异常 - :", e);
after(context, toolJob, e);
}
}
/**
* 执行前
*
* @param context 工作执行上下文对象
* @param toolJob 系统计划任务
*/
protected void before(JobExecutionContext context, com.hgyc.mom.tool.entity.Job toolJob)
{
threadLocal.set(LocalDateTime.now());
}
/**
* 执行后
*
* @param context 工作执行上下文对象
* @param toolJob 系统计划任务
*/
protected void after(JobExecutionContext context, com.hgyc.mom.tool.entity.Job toolJob, Exception e)
{
LocalDateTime startTime = threadLocal.get();
threadLocal.remove();
final JobLog jobLog = new JobLog();
jobLog.setJobName(toolJob.getJobName());
jobLog.setJobGroup(toolJob.getJobGroup());
jobLog.setInvokeTarget(toolJob.getInvokeTarget());
jobLog.setStartTime(startTime);
jobLog.setStopTime(LocalDateTime.now());
// 计算持续时间
Duration duration = Duration.between(startTime, jobLog.getStopTime());
long runMs = duration.toMillis();
jobLog.setJobMessage(jobLog.getJobName() + " 总共耗时:" + runMs + "毫秒");
if (e != null)
{
jobLog.setStatus(Constants.FAIL);
String errorMsg = StringUtils.substring(ExceptionUtils.getExceptionMessage(e), 0, 2000);
jobLog.setExceptionInfo(errorMsg);
}
else
{
jobLog.setStatus(Constants.SUCCESS);
}
// 写入数据库当中
SpringUtils.getBean(JobLogService.class).save(jobLog);
}
/**
* 执行方法,由子类重载
*
* @param context 工作执行上下文对象
* @param job 系统计划任务
* @throws Exception 执行过程中的异常
*/
protected abstract void doExecute(JobExecutionContext context, com.hgyc.mom.tool.entity.Job job) throws Exception;
}
备注: 定义了任务执行前后的方法,
before和after,可以自定义逻辑进行扩充,这里我定义了任务执行后,记录任务执行的日志。
禁止并发执行类
/**
* 定时任务处理(禁止并发执行)
*
* @author fwj
* */@DisallowConcurrentExecution
public class QuartzDisallowConcurrentExecution extends AbstractQuartzJob
{
@Override
protected void doExecute(JobExecutionContext context, Job sysJob) throws Exception
{
JobInvokeUtil.invokeMethod(sysJob);
}
}
允许并发执行类
/**
* 定时任务处理(允许并发执行)
*
* @author fwj
* */public class QuartzJobExecution extends AbstractQuartzJob
{
@Override
protected void doExecute(JobExecutionContext context, Job sysJob) throws Exception
{
JobInvokeUtil.invokeMethod(sysJob);
}
}
通过配置任务信息,执行定时任务
/**
* 任务执行工具
*
* @author fwj */public class JobInvokeUtil
{
/**
* 执行方法
*
* @param job 系统任务
*/
public static void invokeMethod(Job job) throws Exception
{
String invokeTarget = job.getInvokeTarget();
String beanName = getBeanName(invokeTarget);
String methodName = getMethodName(invokeTarget);
List<Object[]> methodParams = getMethodParams(invokeTarget);
if (!isValidClassName(beanName))
{
Object bean = SpringUtils.getBean(beanName);
invokeMethod(bean, methodName, methodParams);
}
else
{
Object bean = Class.forName(beanName).newInstance();
invokeMethod(bean, methodName, methodParams);
}
}
/**
* 调用任务方法
*
* @param bean 目标对象
* @param methodName 方法名称
* @param methodParams 方法参数
*/
private static void invokeMethod(Object bean, String methodName, List<Object[]> methodParams)
throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (StringUtils.isNotNull(methodParams) && methodParams.size() > 0)
{
Method method = bean.getClass().getDeclaredMethod(methodName, getMethodParamsType(methodParams));
method.invoke(bean, getMethodParamsValue(methodParams));
}
else
{
Method method = bean.getClass().getDeclaredMethod(methodName);
method.invoke(bean);
}
}
/**
* 校验是否为为class包名
*
* @param invokeTarget 名称
* @return true是 false否
*/
public static boolean isValidClassName(String invokeTarget)
{
return StringUtils.countMatches(invokeTarget, ".") > 1;
}
/**
* 获取bean名称
*
* @param invokeTarget 目标字符串
* @return bean名称
*/
public static String getBeanName(String invokeTarget)
{
String beanName = StringUtils.substringBefore(invokeTarget, "(");
return StringUtils.substringBeforeLast(beanName, ".");
}
/**
* 获取bean方法
*
* @param invokeTarget 目标字符串
* @return method方法
*/
public static String getMethodName(String invokeTarget)
{
String methodName = StringUtils.substringBefore(invokeTarget, "(");
return StringUtils.substringAfterLast(methodName, ".");
}
/**
* 获取method方法参数相关列表
*
* @param invokeTarget 目标字符串
* @return method方法相关参数列表
*/
public static List<Object[]> getMethodParams(String invokeTarget)
{
String methodStr = StringUtils.substringBetween(invokeTarget, "(", ")");
if (StringUtils.isEmpty(methodStr))
{
return null;
}
String[] methodParams = methodStr.split(",(?=([^\"']*[\"'][^\"']*[\"'])*[^\"']*$)");
List<Object[]> classs = new LinkedList<>();
for (int i = 0; i < methodParams.length; i++)
{
String str = StringUtils.trimToEmpty(methodParams[i]);
// String字符串类型,以'或"开头
if (StringUtils.startsWithAny(str, "'", "\""))
{
classs.add(new Object[] { StringUtils.substring(str, 1, str.length() - 1), String.class });
}
// boolean布尔类型,等于true或者false
else if ("true".equalsIgnoreCase(str) || "false".equalsIgnoreCase(str))
{
classs.add(new Object[] { Boolean.valueOf(str), Boolean.class });
}
// long长整形,以L结尾
else if (StringUtils.endsWith(str, "L"))
{
classs.add(new Object[] { Long.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Long.class });
}
// double浮点类型,以D结尾
else if (StringUtils.endsWith(str, "D"))
{
classs.add(new Object[] { Double.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Double.class });
}
// 其他类型归类为整形
else
{
classs.add(new Object[] { Integer.valueOf(str), Integer.class });
}
}
return classs;
}
/**
* 获取参数类型
*
* @param methodParams 参数相关列表
* @return 参数类型列表
*/
public static Class<?>[] getMethodParamsType(List<Object[]> methodParams)
{
Class<?>[] classs = new Class<?>[methodParams.size()];
int index = 0;
for (Object[] os : methodParams)
{
classs[index] = (Class<?>) os[1];
index++;
}
return classs;
}
/**
* 获取参数值
*
* @param methodParams 参数相关列表
* @return 参数值列表
*/
public static Object[] getMethodParamsValue(List<Object[]> methodParams)
{
Object[] classs = new Object[methodParams.size()];
int index = 0;
for (Object[] os : methodParams)
{
classs[index] = (Object) os[0];
index++;
}
return classs;
}
}
备注: 这里是使用了Java的反射机制,通过定义的执行类和执行方法,具体实现请查看
invokeMethod方法
5 功能说明
5.1 配置定时任务
系统工具 -- 定时任务 -- 新增
- 这里我添加了一个测试的任务
jobService.test(),jobService可以通过spring 注解@Service("jobService")定义,也可以使用完成的类路径定义,test()为要定时执行的方法。 - cron执行表达,我在前端封装了一个生成表达式的组件,点击【生产表达式】按钮
这里有快速模板,可以直接点击就可以生成对应的cron表达式,也可以自己手动配置,配置完之后会自动生成表达式的说明和是否合法。
5.2 执行定时任务
有两种方法执行,第一种是通过cron表达式执行,第二种是通过手动触发执行。这里说明手动触发如何实行。
首先任务状态是【开启】状态,选择要执行的任务,点击操作列中的【执行一次】按钮。
5.3 任务执行日志
日志可以通过定时任务操作按钮中的【日志】查看所有任务的执行日志,如果只需要查看对应任务的执行日志,可以在对应任务的操作列中点击【日志】查看。
本文源码已上传Gitee 开源项目地址:
欢迎在评论区分享你的技术选型经验,或对本文方案的改进建议!
关注公众号「慧工云创」