显示导航

在你的 Grails 应用程序内计划定期任务

了解如何在你的 Grails 应用程序内使用 Schwartz 计划定期任务

作者:伊万·洛佩兹,塞尔吉奥·德尔·阿莫

Grails 版本 3.3.2

1 培训

Grails 培训 - 由创建并积极维护 Grails 框架的人员开发和提供!

2 开始使用

现在通常需要每晚午夜、每小时乃至每周多次运行某种 cron 或计划任务……​在 Java 世界中,我们已经使用 Quartz 库很多年了。

在本指南中,你将了解如何使用 Grails Schwartz 插件 在 Grails 应用程序内计划定期任务

2.1 如何完成指南

请执行以下操作以开始

Grails 指南存储库包含两个文件夹

  • initial 初始项目。通常是简单的 Grails 应用,还有一些额外代码让你快速入门。

  • complete 已完成的示例。这是完成指南中介绍的所有步骤并将这些更改应用到 initial 文件夹中的结果。

要完成指南,请转到 initial 文件夹

  • cd 进入 grails-guides/grails-schwartz/initial

并按照下一部分中的说明进行操作。

如果你 cd 进入 grails-guides/grails-schwartz/complete,则可以直接转到已完成示例

3 撰写指南

3.1 安装插件

要安装插件,请修改 build.gradle

build.gradle
buildscript {
    repositories {
        mavenLocal()
        maven { url "https://repo.grails.org/grails/core" }
    }
    dependencies {
        classpath "org.grails:grails-gradle-plugin:$grailsVersion"
        classpath "gradle.plugin.com.energizedwork.webdriver-binaries:webdriver-binaries-gradle-plugin:1.1"
        classpath "gradle.plugin.com.energizedwork:idea-gradle-plugins:1.4"
        classpath "org.grails.plugins:hibernate5:${gormVersion-".RELEASE"}"
        classpath "com.bertramlabs.plugins:asset-pipeline-gradle:2.14.6"
        classpath 'com.agileorbit:schwartz:1.0.1' (1)
    }
}
1 Schwartz 插件

同时还应将以下内容添加到 dependencies 块中

build.gradle
compile 'com.agileorbit:schwartz:1.0.1'

3.2 为程序包 demo 设置日志级别 INFO

在此指南中,我们将使用多个日志语句来展示作业执行。

logback.groovy 的末尾添加下一条语句

grails-app/conf/logback.groovy
logger('demo', INFO, ['STDOUT'], false)

以上行将为程序包 demo 配置一个日志记录器,其日志级别为 INFO,使用 STDOUT 追加器且适应性设置为 false。

3.3 创建简单作业

该插件提供了一个新的 Grails 命令来创建新作业。只需执行

./grailsw create-job HelloWorld

这将创建文件 grails-app/services/demo/HelloWorldJobService.groovy,并提供一些关于如何触发该作业的示例注销。

请注意,Schwartz 作业并非 Grails 工件,但该插件使用以下缀JobService 命名作业的惯例。作业文件默认情况下放置在 grails-app/services 目录中,尽管有不同的选项可仅创建一个 POGO。

Schwartz 作业的基本框架是

grails-app/services/demo/HelloWorldJobService.groovy
class HelloWorldJobService implements SchwartzJob { (1)

    void execute(JobExecutionContext context) throws JobExecutionException {
        (2)
    }

    void buildTriggers() {
        (3)
    }
}
1 实施 SchwartzJob
2 作业逻辑在此
3 作业的不同触发器

我们修改上一个代码,并添加以下内容

grails-app/services/demo/HelloWorldJobService.groovy
void execute(JobExecutionContext context) throws JobExecutionException {
    log.info "{}:{}", context.trigger.key, new SimpleDateFormat("dd/M/yyyy hh:mm:ss").format(new Date()) (1)
}

void buildTriggers() {
    (2)
    triggers <<
        factory('Simple Job every 10 seconds')
        .intervalInSeconds(10)
        .build()

    (3)
    triggers <<
        factory('Simple Job every 45 seconds')
        .startDelay(5000)
        .intervalInSeconds(45)
        .build()
}
1 作业名称和日期
2 每 10 秒创建一个触发器
3 每 45 秒创建一个另一个触发器,初始延迟为 5 秒(5000 毫秒)

现在启动应用程序

./gradlew bootRun

几秒钟之后,您会看到以下输出

DEFAULT.Simple Job every 10 seconds -> Fri Jan 19 13:24:53 CET 2018 (1)
DEFAULT.Simple Job every 45 seconds -> Fri Jan 19 13:24:58 CET 2018 (2)
DEFAULT.Simple Job every 10 seconds -> Fri Jan 19 13:25:03 CET 2018 (3)
DEFAULT.Simple Job every 10 seconds -> Fri Jan 19 13:25:13 CET 2018
DEFAULT.Simple Job every 10 seconds -> Fri Jan 19 13:25:23 CET 2018
DEFAULT.Simple Job every 10 seconds -> Fri Jan 19 13:25:33 CET 2018
DEFAULT.Simple Job every 10 seconds -> Fri Jan 19 13:25:43 CET 2018
DEFAULT.Simple Job every 45 seconds -> Fri Jan 19 13:25:43 CET 2018 (4)
DEFAULT.Simple Job every 10 seconds -> Fri Jan 19 13:25:53 CET 2018
1 应用程序启动后,首次执行 10 秒的作业
2 应用程序启动后,45 秒的作业在 5 秒后启动。请参见触发器配置中的 startDelay
3 第一次执行后的 10 秒,再次执行 10 秒的作业
4 第一次执行后的 45 秒,再次执行 45 秒的作业

3.4 服务中的业务逻辑

尽管先前的示例是有效的,但您通常不希望将业务逻辑放入作业中。更好的做法是创建 JobService 调用的附加服务。此方法将您的业务逻辑与调度逻辑分开。此外,它促进了测试和维护。我们来看一个示例

创建以下服务

grails-app/services/demo/EmailService.groovy
package demo

import groovy.transform.CompileStatic

@CompileStatic
class EmailService {

    void send(String user) {
        log.info "Sending email to ${user}"
    }
}

然后创建作业

grails-app/services/demo/DailyEmailJobService.groovy
@CompileStatic
@Slf4j
class DailyEmailJobService implements SchwartzJob {

    EmailService emailService (1)

    void execute(JobExecutionContext context) throws JobExecutionException {
        emailService.send('[email protected]') (2)
    }

    void buildTriggers() {
        triggers << factory('Daily email job')
            .cronSchedule("0 30 4 1/1 * ? *") (3)
            .build()
    }
}
1 注入服务
2 调用它
3 每天上午 4:30 触发一次作业

Cron 符号通常令人费解。为简化触发器定义,该插件带有几个构建器。这些构建器支持流畅 API,最终您会用可读且直观的代码来定义触发器和 IDE 自动完成。

上一个 cron 配置可写为

grails-app/services/demo/todayat/DailyEmailJobService.groovy
import com.agileorbit.schwartz.SchwartzJob
import demo.EmailService
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.quartz.JobExecutionContext
import org.quartz.JobExecutionException
import static org.quartz.DateBuilder.todayAt
import static org.quartz.DateBuilder.tomorrowAt

@CompileStatic
@Slf4j
class DailyEmailJobService implements SchwartzJob {
    final int HOUR = 4
    final int MINUTE = 30
    final int SECONDS = 0

    EmailService emailService (1)

    void execute(JobExecutionContext context) throws JobExecutionException {
        emailService.send('[email protected]') (2)
    }

    Date dailyDate() {
        Date startAt = todayAt(HOUR, MINUTE, SECONDS)
        if (startAt.before(new Date())) {
            return tomorrowAt(HOUR, MINUTE, SECONDS)
        }
        startAt
    }

    void buildTriggers() {
        Date startAt = dailyDate()
        triggers << factory('Daily email job')
                .startAt(startAt)
                .intervalInDays(1)
                .build()

    }
}

3.5 使用数据手动调度作业

想象一下以下场景。您想在每个用户注册到您的应用后向他们发送一封电子邮件。您想询问他们对与该服务首次交互的体验。

对于本指南,我们打算在 1 分钟后调度一个作业以触发。

修改 `BootStrap.groovy` 以两次调用名为 `RegisterService` 的新服务。

grails-app/init/demo/BootStrap.groovy
package demo

import groovy.transform.CompileStatic

@CompileStatic
class BootStrap {

        RegisterService registerService

    def init = { servletContext ->
            registerService.register('[email protected]')
            sleep(20_000)
            registerService.register('[email protected]')
    }
    def destroy = {
    }
}

创建一个 `RegisterService.groovy`。

grails-app/services/demo/RegisterService.groovy
package demo

import com.agileorbit.schwartz.QuartzService
import com.agileorbit.schwartz.builder.BuilderFactory
import groovy.time.TimeCategory
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.quartz.JobDataMap
import org.quartz.JobDetail
import org.quartz.JobKey
import org.quartz.Trigger
import org.quartz.TriggerBuilder

import java.text.SimpleDateFormat

@Slf4j
@CompileStatic
class RegisterService {

    QuartzService quartzService

        void register(String email) {
                log.info 'saving {} at {}', email, new SimpleDateFormat("dd/M/yyyy hh:mm:ss").format(new Date())
                scheduleFollowupEmail(email)
        }

        @CompileDynamic
        Date startAtDate() { (1)
                Date startAt = new Date()
                use(TimeCategory) {
                        startAt = startAt + 1.minute
                }
                startAt
        }

        void scheduleFollowupEmail(String email) {
                JobDataMap jobDataMap = new JobDataMap()
                jobDataMap.put('email', email)
                Trigger trigger = TriggerBuilder.newTrigger()
                                .forJob(JobKey.jobKey(FollowupEmailJobService.simpleName)) (2)
                                .startAt(startAtDate())
                                .usingJobData(jobDataMap) (3)
                                .build()
                quartzService.scheduleTrigger(trigger) (4)
        }
}
1 此方法返回一分钟后的一个日期。
2 我们可以通过 JobService 的 simpleName 找到必需的 `Jobkey`。
3 我们可以将数据传递到作业执行中。
4 调度触发器。

创建一个 `FollowupEmailJobService.groovy`。

grails-app/services/demo/FollowupEmailJobService.groovy
package demo

import com.agileorbit.schwartz.SchwartzJob
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.quartz.JobDataMap
import org.quartz.JobExecutionContext
import org.quartz.JobExecutionException

import java.text.SimpleDateFormat

@Slf4j
@CompileStatic
class FollowupEmailJobService implements SchwartzJob {

    void execute(JobExecutionContext context) throws JobExecutionException {
                JobDataMap jobDataMap = context.mergedJobDataMap
                String email = jobDataMap.getString('email') (1)
                log.info 'Sending followup email to: {} at {}', email, new SimpleDateFormat("dd/M/yyyy hh:mm:ss").format(new Date())
        }

        @Override
        void buildTriggers() {

        }
}
1 提取作业的数据。

如果您执行以上代码,您将看到作业在我们调度它之后一分钟执行,并带有已提供的电子邮件地址。

INFO demo.RegisterService         : saving [email protected] at 23/1/2018 07:55:21
INFO demo.RegisterService         : saving [email protected] at 23/1/2018 07:55:41
INFO demo.FollowupEmailJobService : Sending followup email to: [email protected] at 23/1/2018 07:56:21
INFO demo.FollowupEmailJobService : Sending followup email to: [email protected] at 23/1/2018 07:56:41

3.6 并发作业执行

想象您用 Grails 开发了 SaaS 应用程序。每天上午 7:00,您想检查是否有用户的订阅已过期。如果已过期,您想为新周期创建一个发票,保存发票并在数据库中更新该用户的订阅到期日。

您想禁用此类作业的并发执行。仅使用 Quartz 时,您需要通过 `@DisallowConcurrentExecution` 为作业添加注释。使用 Schwartz 时,您仍然可以这么做,或者如果您愿意,您可以实现 trait `StatefulSchwartzJob` 代替 `SchwartzJob`。`StatefulSchwartzJob` 扩展了 `SchwartzJob` 并添加了两个注释:`@DisallowConcurrentExecution`、`@PersistJobDataAfterExecution`。

示例代码可能如下所示。

grails-app/services/demo/GenerateInvoiceJobService.groovy
package demo

import com.agileorbit.schwartz.StatefulSchwartzJob
import grails.gorm.transactions.Transactional
import org.quartz.JobExecutionContext
import org.quartz.JobExecutionException
import static org.quartz.DateBuilder.todayAt
import static org.quartz.DateBuilder.tomorrowAt

class GenerateInvoiceJobService implements StatefulSchwartzJob {
    final int HOUR = 7
    final int MINUTE = 0
    final int SECONDS = 0

    GenerateInvoiceService generateInvoiceService

    @Transactional (1)
    @Override
    void execute(JobExecutionContext context) throws JobExecutionException {
        generateInvoiceService.generateInvoices()
    }

    @Override
    void buildTriggers() {
        Date startAt = dailyDate()
        triggers << factory('Daily email job at 7:00 AM')
                .startAt(startAt)
                .intervalInDays(1)
                .build()

    }


    Date dailyDate() {
        Date startAt = todayAt(HOUR, MINUTE, SECONDS)
        if (startAt.before(new Date())) {
            return tomorrowAt(HOUR, MINUTE, SECONDS)
        }
        startAt
    }

}
1 在事务中包装作业执行。
grails-app/services/demo/GenerateInvoiceService.groovy
package demo

import groovy.transform.CompileStatic

@CompileStatic
class GenerateInvoiceService {

    void generateInvoices() {
        // Check if users subscription has finished.
        // Generate an invoice for a new period and save it in the database
                // update user's subscription expiration date
    }
}

3.7 总结

本指南期间我们学习了如何手动使用 cron 语句配置作业以及通过插件流 API。此外,我们学习了如何禁用并发执行或传递作业数据。

请参阅 Grails Schwartz 插件文档 以了解详细信息。

4 Grails 的帮助

Object Computing, Inc. (OCI) 赞助了本指南的创作。我们提供各种咨询和支持服务。

OCI是 Grails 之家

了解团队