初步实现 Job 插件

原创
2013/12/15 22:05
阅读数 6.9K

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

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

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

我们先来看看 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. 需要指定一个 cron 表达式“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:我们想使用您的任务调度功能。此时必须通过一个简单的配置才行:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:task="http://www.springframework.org/schema/task"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/task
       http://www.springframework.org/schema/task/spring-task.xsd">

    <task:annotation-driven/>

</beans>

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

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

@Component
public 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<Class<?>, Scheduler> jobMap = new HashMap<Class<?>, 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<Class<?>> 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<? extends org.quartz.Job>) 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<? extends Job> 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 启动时与停止时,它都可以察觉到这些事件,这不就是“观察者模式”的最佳实践吗?

@WebListener
public 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 看起来应该更加的丰满了!

@WebListener
public 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<Plugin> 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<Plugin> pluginList = new ArrayList<Plugin>();

    static {
        try {
            // 获取并遍历所有的 Plugin 类(实现了 Plugin 接口的类)
            List<Class<?>> 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<Plugin> 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 插件源码地址:http://git.oschina.net/huangyong/smart-plugin-job

展开阅读全文
加载中
点击加入讨论🔥(15) 发布并加入讨论🔥
打赏
15 评论
44 收藏
3
分享
返回顶部
顶部