从0到1构建MES系统12-定时任务

85 阅读7分钟

今天我们来讲一下编码规则的功能设计与开发。

1. 概述

在MES(制造执行系统)中,定时任务承担着数据同步、报表生成、设备状态检查等关键功能。本文档详细阐述基于Quartz的定时任务模块设计与实现,重点解决线程安全、任务调度策略等核心问题。

2. 定时任务框架选型

2.1 主流框架对比

特性QuartzSpring @ScheduledXXL-JOBTimer
分布式支持需要额外配置不支持原生支持不支持
动态任务管理支持不支持支持有限支持
失败处理策略完善简单完善有限
任务持久化支持不支持支持不支持
Cron表达式支持完整支持支持支持不支持
线程池管理可配置简单可配置固定单线程
学习曲线中等简单中等简单

2.2 选择Quartz的原因

  1. 非分布式需求:当前MES系统为单实例部署,无需分布式调度能力

  2. 动态任务管理:支持运行时添加、修改、删除任务,适应MES需求变化

  3. 健壮的错误处理:提供多种misfire策略应对任务执行失败场景

  4. 企业级特性:作业持久化、事务支持、集群能力(为未来扩展预留)

  5. 成熟稳定:经过多年生产验证,社区支持完善

3. 系统架构设计

deepseek_mermaid_20250630_44870d.png

4. 核心功能实现

4.1 表结构

定时任务调度表(tool_job)

字段名类型约束说明
idbigint主键,非空任务ID
job_namevarchar(64)非空任务名称
job_groupvarchar(64)非空任务组名
invoke_targetvarchar(500)非空调用目标字符串
cron_expressionvarchar(255)可空cron执行表达式
misfire_policyvarchar(20)可空计划执行错误策略(1立即执行 2执行一次 3放弃执行)
concurrentchar可空是否并发执行(0允许 1禁止)
statuschar可空状态(0正常 1暂停)
create_bybigint可空创建者
create_timedatetime可空创建时间
update_bybigint可空更新者
update_timedatetime可空更新时间
remarkvarchar(500) 默认''可空备注信息
定时任务调度表(tool_job)
字段名类型约束说明
idbigint主键,非空任务ID
job_namevarchar(64)非空任务名称
job_groupvarchar(64)非空任务组名
invoke_targetvarchar(500)非空调用目标字符串
cron_expressionvarchar(255)可空cron执行表达式
misfire_policyvarchar(20)可空计划执行错误策略(1立即执行 2执行一次 3放弃执行)
concurrentchar可空是否并发执行(0允许 1禁止)
statuschar可空状态(0正常 1暂停)
create_bybigint可空创建者
create_timedatetime可空创建时间
update_bybigint可空更新者
update_timedatetime可空更新时间
remarkvarchar(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;  
}

备注: 定义了任务执行前后的方法,beforeafter,可以自定义逻辑进行扩充,这里我定义了任务执行后,记录任务执行的日志。

禁止并发执行类

/**  
 * 定时任务处理(禁止并发执行)  
 *   
 * @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 功能说明

Pasted image 20250630220705.png

5.1 配置定时任务

系统工具 -- 定时任务 -- 新增

Pasted image 20250630220800.png

  1. 这里我添加了一个测试的任务 jobService.test(), jobService 可以通过spring 注解 @Service("jobService")定义,也可以使用完成的类路径定义, test()为要定时执行的方法。
  2. cron执行表达,我在前端封装了一个生成表达式的组件,点击【生产表达式】按钮

Pasted image 20250630221241.png 这里有快速模板,可以直接点击就可以生成对应的cron表达式,也可以自己手动配置,配置完之后会自动生成表达式的说明和是否合法。

5.2 执行定时任务

有两种方法执行,第一种是通过cron表达式执行,第二种是通过手动触发执行。这里说明手动触发如何实行。

Pasted image 20250630221612.png 首先任务状态是【开启】状态,选择要执行的任务,点击操作列中的【执行一次】按钮。

5.3 任务执行日志

Pasted image 20250630221815.png 日志可以通过定时任务操作按钮中的【日志】查看所有任务的执行日志,如果只需要查看对应任务的执行日志,可以在对应任务的操作列中点击【日志】查看。

本文源码已上传Gitee 开源项目地址

欢迎在评论区分享你的技术选型经验,或对本文方案的改进建议!

关注公众号「慧工云创」 扫码_搜索联合传播样式-标准色版.png