博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
初步实现 Job 插件
阅读量:6258 次
发布时间:2019-06-22

本文共 14148 字,大约阅读时间需要 47 分钟。

hot3.png

我们在做项目的时候,可能会遇到这样的问题:如何定时或周期性地调用某个类的方法呢?

您丰富的经验或许会告诉自己,定时器(Timer)或调度器(Scheduler)可以实现该功能,当然 JDK 自带的 java.util.Timer 也是一个轻量级选择,但功能比较欠缺,它不能实现一些较复杂的任务调度,比如:在周一至周五,每天 8 点到 20 点,每隔 5 分钟调用一次。

正因为 可以做以上这件看似简单而又复杂的事情,所以它在业界才会流行起来。此外它也能保持着苗条的身材,为我们展现它的骄姿,所以它往往是开发人员实现任务调度的首选。

我们先来看看 Quartz 是怎样使用的吧!

1 使用 Quartz 实现任务调度

Quartz 告诉我们,所有的 Job 类必须实现 org.quartz.Job 接口,该接口仅提供了一个 execute 方法,该方法会被 Quartz 框架自动调度。

我们先来一个简单的 Quartz Job 吧!

public class QuartzHelloJob implements Job {    private static final SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");    @Override    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {        System.out.println(format.format(new Date()) + " Hello Quartz!");    }}

我们需要每隔一段时间输出一个 Hello Quartz 的文本信息,需要自定义一个 Job 类。在 Job 类中必须实现 Job 接口,填充它的 execute 方法。

需要说明的是,该方法中有个 JobExecutionContext 参数,它表示 Job 执行上下文,它是一个很牛逼对象,在一些复杂的场景下会使用该参数,比如实现数据传递功能,现在暂时忽略它吧。这个方法还要求我们,必须在 execute 方法的声明处,定义可抛出 JobExecutionException 异常,否则 Job 的调用者无法捕获到 Job 类中产生的任何异常。

下面,我们需要借助 Quartz 提供的几个核心组件来完成任务调度功能,不妨编写一个单元测试来实现着一切吧!

public class QuartzJobTest {    @Test    public void test() {        try {            JobDetail jobDetail = JobBuilder.newJob(QuartzHelloJob.class).build();            ScheduleBuilder builder = CronScheduleBuilder.cronSchedule("0/1 * * * * ?");            Trigger trigger = TriggerBuilder.newTrigger().withSchedule(builder).build();            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();            scheduler.scheduleJob(jobDetail, trigger);            scheduler.start();            sleep(3000);            scheduler.shutdown(true);            sleep(3000);        } catch (SchedulerException se) {            se.printStackTrace();        }    }    private void sleep(long ms) {        try {            Thread.sleep(ms);        } catch (InterruptedException e) {            e.printStackTrace();        }    }}

  1. 我们需要根据刚才编写的 QuartzHelloJob 类来创建一个 JobDetail 对象。

  2. 需要指定一个  表达式“0/1 * * * * ?”,它表示每隔 1 秒钟的意思,进而创建一个 ScheduleBuilder 对象。

  3. 根据 ScheduleBuilder 对象我们来创建一个 Trigger 对象。

  4. 创建一个 Scheduler 对象,并将 JobDetail 对象与 Trigger 对象加入其中,这样才能开启这个调度。

  5. 不妨延迟 3 秒,看看控制台的输出。

  6. 关闭当前的调度,允许在结束调度之前等待最后一个 Job 运行结束(防止该 Job 没有执行完就被扼杀了)。

  7. 最后再延迟 3 秒,看看控制台还会不会输出相关信息。

您没有看错,要使用 Quartz,你就需要知道这些核心对象(组件)到底是干嘛的,它们主要包括:JobDetail、ScheduleBuilder、Trigger、Scheduler 等。

这一切似乎简单,而又非常繁琐,能否让 Job 更加简化呢?

牛逼的 Spring 提供了一个极简的抽象,我们再来看看如何通过 Spring 来完成任务调度。

2 使用 Spring 简化任务调度

首先需要告诉 Spring:我们想使用您的任务调度功能。此时必须通过一个简单的配置才行:

    

需要说明的是,我们采用的是 Spring 3 提供的最简单的任务调度解决方案,以前或许您会做大量的 XML 配置,但从 Spring 3 以后,推荐我们使用基于注解的方式,而不是 XML 配置方式。

我们可以像这样来定义一个 Job:

@Componentpublic class SpringHelloJob {    private static final SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");    @Scheduled(corn = "0/1 * * * * ?")    public void execute() {        System.out.println(format.format(new Date()) + " Hello Spring!");    }}

这里就可以看到明显的简化,无需实现所谓的 Job 接口,但必须提供一个可以被调度的方法,方法名叫什么无所谓,为了理解上保持一致,不妨命名为 execute 吧。

更有特色的是,Spring 使用了一个名为 @Scheduled 的注解,可定义一个 cron 表达式,从而指定方法的调用方式。

需要补充说明的是,Spring 提供了一个 @Component 注解,来声明该对象是由 Spring IOC 容器来管理的,也就是说,这样声明后,我们就可以使用“依赖注入”功能了。

其实,在上面这个 Job 类中可定义多个 execute 方法,但都需要使用各自的 @Scheduled 来定义调度策略。但我个人认为,一个 Job 类只提供一个 execute 方法比较合适,这就是传说中的“单一指责原则”了!

看来 Spring 确实够强悍的,轻松几下,就能实现 Quartz 较为复杂的代码结构。

机灵的 Smart 也不甘示弱,也想提供一个与 Spring 相似的任务调度框架。那么,我们应该如何实现呢?

3 开发 Smart Job 插件

3.1 编写一个 Job 类

目前,Smart 的插件已经很多了,缺少了 Job 插件似乎有些遗憾,所以我们无论如何都要提供一个任务调度框架,才对得起 Smart 的精神:Smart your dev,Smart your life!

既然 cron 表达式已经这么强大了,我们不妨就使用它来作为 Smart Job 插件的调度公式吧,我们可以这样来写一个 Smart Job:

@Bean@Job("0/1 * * * * ?")public class SmartHelloJob extends BaseJob {    private static final SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");    @Override    public void execute() {        System.out.println(format.format(new Date()) + " - Hello Smart!");    }}

上面定义了一个 Job 类,根据我们一贯的作风,该 Job 类继承一个 BaseJob 抽象类,必须完成该抽象父类的 execute 方法才行,可以肯定的是,该方法就是需要调度执行的方法。

当初设想,其实也可以不继承任何的类,就像 Spring 一样,但对于一个 Job 而言,继承一个 BaseJob,将来或许能够提供一些通用的功能,也方便我们扩展。至少现在我们得满足 Quartz 的要求:每个 Job 类必须实现 org.quartz.Job 接口。所以 BaseJob 应该这样写:

public abstract class BaseJob implements Job {    private static final Logger logger = Logger.getLogger(BaseJob.class);    @Override    public final void execute(JobExecutionContext context) throws JobExecutionException {        try {            execute();        } catch (Exception e) {            logger.error("执行 Job 出错!", e);        }    }    public abstract void execute();}

在 BaseJob 中,我们屏蔽了 JobExecutionContext 与 JobExecutionException。以上代码可以看出,这里使用了模板方法模式,BaseJob 的子类必须实现自己定义的 execute 方法。

此外,需要注意的是,与 Spring 不同,Smart 提供了一个 @Job 注解,该注解类似于 Spring 的 @Scheduled 注解,但它是标注在 Job 类上的,而不是 execute 方法上。这样也保证了一个 Job 类对应一个业务逻辑,不会将多个业务逻辑混入到同一个 Job 类中,这是为了“单一指责原则”而故意这样设计的。

需要补充的是,Smart 的 @Bean 注解就相当于 Spring 的 @Component 注解。

需要像 Spring 那样再做一个 XML 配置吗?——不需要了。

3.2 提供 JobHelper 类

为了封装 Quartz 繁琐的 API,我们需要编写一个 JobHelper,它是这样写的:

public class JobHelper {    private static final Logger logger = Logger.getLogger(JobHelper.class);    private static final Map
, Scheduler> jobMap = new HashMap
, Scheduler>();    private static final JobFactory jobFactory = new SmartJobFactory();    public static void startJob(Class
 jobClass, String cron) {        try {            Scheduler scheduler = createScheduler(jobClass, cron);            scheduler.start();            jobMap.put(jobClass, scheduler);            if (logger.isDebugEnabled()) {                logger.debug("[Smart] start job: " + jobClass.getName());            }        } catch (SchedulerException e) {            logger.error("启动 Job 出错!", e);        }    }    public static void startJobAll() {        List
> jobClassList = ClassHelper.getClassListBySuper(BaseJob.class);        if (CollectionUtil.isNotEmpty(jobClassList)) {            for (Class
 jobClass : jobClassList) {                if (jobClass.isAnnotationPresent(Job.class)) {                    String cron = jobClass.getAnnotation(Job.class).value();                    startJob(jobClass, cron);                }            }        }    }    public static void stopJob(Class
 jobClass) {        try {            Scheduler scheduler = getScheduler(jobClass);            scheduler.shutdown(true);            jobMap.remove(jobClass); // 从 jobMap 中移除该 Job            if (logger.isDebugEnabled()) {                logger.debug("[Smart] stop job: " + jobClass.getName());            }        } catch (SchedulerException e) {            logger.error("停止 Job 出错!", e);        }    }    public static void stopJobAll() {        for (Class
 jobClass : jobMap.keySet()) {            stopJob(jobClass);        }    }    public static void pauseJob(Class
 jobClass) {        try {            Scheduler scheduler = getScheduler(jobClass);            scheduler.pauseJob(new JobKey(jobClass.getName()));            if (logger.isDebugEnabled()) {                logger.debug("[Smart] pause job: " + jobClass.getName());            }        } catch (SchedulerException e) {            logger.error("暂停 Job 出错!", e);        }    }    public static void resumeJob(Class
 jobClass) {        try {            Scheduler scheduler = getScheduler(jobClass);            scheduler.resumeJob(new JobKey(jobClass.getName()));            if (logger.isDebugEnabled()) {                logger.debug("[Smart] resume job: " + jobClass.getName());            }        } catch (SchedulerException e) {            logger.error("恢复 Job 出错!", e);        }    }    private static Scheduler createScheduler(Class
 jobClass, String cron) {        Scheduler scheduler = null;        try {            @SuppressWarnings("unchecked")            JobDetail jobDetail = JobBuilder.newJob((Class
) jobClass)                .withIdentity(jobClass.getName())                .build();            Trigger trigger = TriggerBuilder.newTrigger()                .withIdentity(jobClass.getName())                .withSchedule(CronScheduleBuilder.cronSchedule(cron))                .build();            scheduler = StdSchedulerFactory.getDefaultScheduler();            scheduler.setJobFactory(jobFactory); // 从 Smart IOC 容器中获取 Job 实例            scheduler.scheduleJob(jobDetail, trigger);        } catch (SchedulerException e) {            logger.error("创建 Scheduler 出错!", e);        }        return scheduler;    }    private static Scheduler getScheduler(Class
 jobClass) {        Scheduler scheduler = null;        if (jobMap.containsKey(jobClass)) {            scheduler = jobMap.get(jobClass);        }        return scheduler;    }}

代码稍微有点长,但是首先需要明确的是,JobHelper 是封装 Quartz 常用 API 的。此外,在里面还有一个重要的 Map 对象:

Map<Class<?>, Scheduler> jobMap

它的 key 就是 Job 类的 Class 对象,value 是 Quartz 的 Scheduler 对象。也就是说,一个 Job 对象对应一个 Scheduler 对象,每个 Job 都有各自的 Scheduler,而并非所有的 Job 都公用同一个 Scheduler。

下面的几个方法无非就是:启动 Job、启动所有 Job、关闭 Job、关闭所有 Job、暂停 Job、恢复 Job,还有几个私有方法。

在 startJob 方法中,我们需要从 Smart IOC 容器中获取 Job 实例,所以要自定义一个 JobFactory,这样的扩展机制也是 Quartz 给我们的礼物。

下面就是我们自定义的 SmartJobFactory:

public class SmartJobFactory implements JobFactory {    @Override    public Job newJob(TriggerFiredBundle bundle, Scheduler scheduler) throws SchedulerException {        JobDetail jobDetail = bundle.getJobDetail();        Class
 jobClass = jobDetail.getJobClass();        return BeanHelper.getBean(jobClass);    }}

似乎一切都是那么自然,那么简单,好像 Quartz 就是为 Smart 准备的一样。我们直接从 TriggerFiredBundle 中获取 JobDetail,根据 JobDetail 获取 Job 类,根据 Job 类从 BeanHelper 中获取 Bean 的对象实例。

为什么要从 Smart IOC 容器中获取 Bean 实例?因为如果不这样做的话,我们的 Job 类就由 Quartz 来管理了(它是通过反射来创建的实例的),那么我们也无法在 Job 类中使用 @Inject 注解来注入我们所需要的对象了。

OK,到这里我想已经大致说清楚了,JobHelper 的实现原理,但是如何让 Job 类中的 @Job 注解生效呢?这个就要用到 Smart 的插件机制了。

3.3 实现 JobPlugin 类

正如我们所知,Job 不但需要启动,还需要停止。实现 Job 的启动应该比较简单,就是 Smart Plugin 接口的 init 方法了。但 Job 的停止应该如何实现呢?

在回答这个问题之前,我们先回答有些朋友可能会提出的质疑:为什么要停止?Web 服务器(如 Tomcat)停止了,Job 不会自动停止吗?

实际情况或许会超乎您的想象,即使 Tomcat 停止了,Job 还仍然活着!它就像幽灵一样,匪夷所思。从技术的角度上来解释,其实 Job 就是一个 daemon 方式的 Thread 而已。我想您已经知道了缘由了。

那么,我们就需要提供一个插件的销毁机制,此时就需要考虑到“开闭原则”了,我们稍微扩展一下即可实现。

以下是改进后的 Plugin 接口:

public interface Plugin {    void init();    void destroy();}

可见,该接口提供了一个 destroy 方法而已,那么它的实现类都必须实现这两个方法。或许对于某些插件而言,destroy 方法是多余的,但空实现或许也是一种不错的选择。

对于 Job 插件而言,destroy 方法就非常重要了,因为我们需要在其中停止所有的 Job 调度。

其实 JobPlugin 真的很简单:

public class JobPlugin implements Plugin {    @Override    public void init() {        JobHelper.startJobAll();    }    @Override    public void destroy() {        JobHelper.stopJobAll();    }}

应该无需做任何解释了,因为它真的很简单。

细心的您肯定会注意到:init 方法可以在 Smart 框架加载的时候被调用,但 destroy 方法又在哪里调用呢?

我们不妨回头去看看 Plugin 的 init 方法是如何被调用的吧。

在 Smart 框架中,有一个 ContainerListener。没错!它就是一个 Listener,更确切地说,它应该是一个 ServletContextListener,它可以监听 Web 容器的初始化与销毁实现,也就是说,当 Tomcat 启动时与停止时,它都可以察觉到这些事件,这不就是“观察者模式”的最佳实践吗?

@WebListenerpublic class ContainerListener implements ServletContextListener {    @Override    public void contextInitialized(ServletContextEvent sce) {        // 初始化相关 Helper 类        Smart.init();        // 添加 Servlet 映射        addServletMapping(sce.getServletContext());    }    @Override    public void contextDestroyed(ServletContextEvent sce) {    }...

当 Tomcat 初始化时会调用 contextInitialized 方法;当 Tomcat 停止时会调用 contextDestroyed 方法。此时,我们应该找到了停止 Job 的最佳地点了。

需要补充说明的是,我们目前是在 Smart.init() 方法内部来调用 Plugin 的 init 方法的,这是一个多态调用方式。有兴趣的朋友,可以阅读一下 Smart 框架的源码。

为了将 PluginHelper 打造成一款强大的武器,我们需要从它那里获取所有的 Plugin,这样才能遍历这些 Plugin,从而通过多态的方式调用每个 Plugin 实现类的 destroy 方法。

现在的 ContainerListener 看起来应该更加的丰满了!

@WebListenerpublic class ContainerListener implements ServletContextListener {    @Override    public void contextInitialized(ServletContextEvent sce) {        // 初始化相关 Helper 类        Smart.init();        // 添加 Servlet 映射        addServletMapping(sce.getServletContext());    }    @Override    public void contextDestroyed(ServletContextEvent sce) {        // 销毁插件        destroyPlugin();    }...    public static void destroyPlugin() {        List
 pluginList = PluginHelper.getPluginList();        for (Plugin plugin : pluginList) {            plugin.destroy();        }    }...

下面是扩展后的 PluginHelper:

public class PluginHelper {    private static final Logger logger = Logger.getLogger(PluginHelper.class);    // 创建一个 Plugin 列表(用于存放 Plugin 实例)    private static final List
 pluginList = new ArrayList
();    static {        try {            // 获取并遍历所有的 Plugin 类(实现了 Plugin 接口的类)            List
> pluginClassList = ClassUtil.getClassListBySuper(FrameworkConstant.PLUGIN_PACKAGE, Plugin.class);            for (Class
 pluginClass : pluginClassList) {                // 创建 Plugin 实例                Plugin plugin = (Plugin) pluginClass.newInstance();                // 调用初始化方法                plugin.init();                // 将 Plugin 实例添加到 Plugin 列表                pluginList.add(plugin);            }        } catch (Exception e) {            logger.error("初始化 PluginHelper 出错!", e);        }    }    public static List
 getPluginList() {        return pluginList;    }}

直到今天,Smart 的插件机制看起来终于有那么一点样子了,Plugin 接口管理了每个插件的生命周期,主要包括:init(出生)与 destroy(死亡)。

通过扩展 Smart 的插件机制,我们的 Job 插件也就初步实现了!

最后想与大家分享的是,用过 Spring 任务调度的朋友,是否想过一个问题:

能否不要在 Tomcat 启动的时自动创建 Job 呢?我们其实是想通过代码的方式,控制 Job 的启动、停止、暂停、恢复,更加灵活地控制 Job 的这些行为。

可惜目前的 Spring 尚不支持以上这个特性,我们只能通过 Quartz API 来实现了。看来 Spring 虽然美丽,但并不完美。

如果您有了 Smart,情况就会完全不一样。我们可以定义一个 Job 类,此时不要定义 @Job 注解,那么该 Job 是不会自动启动的。随后我们可以通过 JobHelper 的 startJob 方法随时启动 Job,通过 stopJob 方法随时停止 Job,此外还有 pauseJob 与 resumeJob 方法,用来暂停 Job 与恢复 Job,这些都不再是梦想。因为 JobHelper 就是 Quartz 的一个简单封装,还有哪些功能非常实用,将来都可以在 JobHelper 类中进行扩展。

下面这段代码或许会让您今天的心情充满愉悦!

public class SmartJobTest extends BaseTest {    @Test    public void test() {        JobHelper.startJob(SmartHelloJob.class, "0/1 * * * * ?");        sleep(3000);        JobHelper.stopJob(SmartHelloJob.class);        sleep(3000);    }    private void sleep(long ms) {        try {            Thread.sleep(ms);        } catch (InterruptedException e) {            e.printStackTrace();        }    }}

这就是 Smart 对 Quartz 的极简封装!Quartz 所有的一切都隐藏在 JobHelper 背后了,您不妨回头去比较一下 SmartJobTest 与 QuartzJobTest 差异,相信您会和我有同样的感受。

以上便是 Smart Job 插件的实现原理与开发过程,期待您的意见与建议,因为这些反馈会让我学到更多的东西!

Smart Job 插件源码地址:

转载于:https://my.oschina.net/huangyong/blog/184620

你可能感兴趣的文章
Android实现手机摄像头的自动对焦
查看>>
ASCII流程图
查看>>
Linux知识积累(5) 关机shutdown和重启reboot
查看>>
HTML5为输入框添加语音输入功能
查看>>
[LeetCode] Find Permutation 找全排列
查看>>
os.environ() 说明
查看>>
Python学习札记(二十) 函数式编程1 介绍 高阶函数介绍
查看>>
tomcat安装不成功.提示:failed to install tomcat6 service ,check your setting and permissions
查看>>
[转]当当网高可用架构之道--转
查看>>
ROS学习网址【原创】
查看>>
mysql数据库对时间进行默认的设置
查看>>
喵哈哈村的魔法考试 Round #3 (Div.2) 题解
查看>>
音频 API 一览
查看>>
hive的select重命名字段显示成中文
查看>>
JVM类加载机制与对象的生命周期
查看>>
zabbix主动被动模式说明/区别
查看>>
神奇的AC
查看>>
数据库防火墙——实现数据库的访问行为控制、危险操作阻断、可疑行为审计...
查看>>
PCIE_DMA实例一:xapp1052详细使用说明
查看>>
MySQL也有潜规则 – Select 语句不加 Order By 如何排序?
查看>>