显示导航

Grails + @Scheduled

学习如何在 Grails 应用程序中使用 Spring 任务执行和计划来安排定期任务

作者: Ben Rhine

Grails 版本 4.0.1

1 培训

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

2 开始使用

如今,需要每隔午夜、每小时或每周运行几次的某种“cron”或计划任务已经相当常见。在 Java 领域,多年来我们一直使用“Quartz”库来执行此操作。

在该指南中,你将学习如何使用本机 Spring 任务执行和计划在 Grails 应用程序内安排定期任务。

有关其他文档,请参阅: @Scheduled TaskScheduler PeriodicTrigger

2.1 如何完成指南

按照以下步骤开始

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

  • initial 初始项目。通常是一个简单的 Grails 应用程序,其中包含一些其他代码以便于你快速入门。

  • complete 已完成的示例。它是按照指南介绍的步骤进行操作并把这些更改应用于 initial 文件夹的结果。

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

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

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

如果您在 grails-guides/grails-scheduled/complete 中执行 cd,则可以直接转到已完成示例

3 编写计划

幸运的是,对于我们来说,使用 Spring 任务执行和计划对于 Grails 来说是一个现成可用功能。因此,无需添加任何其他依赖关系。让我们继续开始。

3.1 将包 demo 的日志级别设置为 INFO

在本指南中,我们将使用若干日志语句以展示任务执行。

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

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

上述行使用日志级别 INFO 为包 demo 配置了一个记录器,其中使用 STDOUT 追加器,并将自适应性设置为 false。

3.2 创建一个简单任务

由于计划是原生功能。只需执行

./grailsw create-service HelloWorldJob

这将使用空模板方法创建文件 grails-app/services/demo/HelloWorldJobService.groovy

基本框架为

grails-app/services/demo/HelloWorldJobService.groovy
@Transactional (1)
class HelloWorldJobService {

    def serviceMethod() {
        (2)
    }
}
1 默认情况下,服务为 Transactional
2 任务逻辑位于这里

让我们修改以前的代码并添加以下内容

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

import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.springframework.scheduling.annotation.Scheduled

import java.text.SimpleDateFormat

@Slf4j (1)
@CompileStatic (2)
class HelloWorldJobService {

    static lazyInit = false (3)

    @Scheduled(fixedDelay = 10000L) (4)
    void executeEveryTen() {
        log.info "Simple Job every 10 seconds :{}", new SimpleDateFormat("dd/M/yyyy hh:mm:ss").format(new Date())
    }

    @Scheduled(fixedDelay = 45000L, initialDelay = 5000L) (5)
    void executeEveryFourtyFive() {
        log.info "Simple Job every 45 seconds :{}", new SimpleDateFormat("dd/M/yyyy hh:mm:ss").format(new Date())
    }
}
1 尽管 Grails 工件(服务、控制器)自动注入记录器,但我们使用 @Slf4j 注释类以获得更好的 IDE 支持。根据您使用的 Grails 版本,@Slf4j 注释会对静态编译、记录器名称等方面产生影响,如 Grails 快速记录日志 中所述。
2 由于此服务不需要事务,所以移除 @Transaction。添加 @CompileStatic 以提高性能。
3 默认情况下,Grails 的服务是延迟初始化的。您可以禁用此功能并使用 lazyInit 属性使初始化急切化。在不强制让服务急切化的情况下,任务不会执行。
4 每 10 秒创建触发器
5 以 5 秒的初始延迟(5000 毫秒)每 45 秒创建另一个触发器

现在启动应用程序

./gradlew bootRun

并在几秒钟后,您将看到以下输出

Simple Job every 45 seconds :14/2/2018 02:06:50 (1)
Simple Job every 10 seconds :14/2/2018 02:06:55 (2)
Simple Job every 10 seconds :14/2/2018 02:07:05 (3)
Simple Job every 10 seconds :14/2/2018 02:07:15
Simple Job every 10 seconds :14/2/2018 02:07:25
Simple Job every 45 seconds :14/2/2018 02:07:35 (4)
Simple Job every 10 seconds :14/2/2018 02:07:35
1 应用程序启动后不久的第一个 10 秒任务执行
2 应用程序启动后仅过 5 秒,45 秒的任务才会启动。请参见触发器配置中的 startDelay
3 第一个执行后仅过 10 秒的第二个 10 秒任务执行
4 第一个执行后仅过 45 秒的第二个 45 秒任务执行
在使用 @Scheduled 时,必须设置 static lazyInit = false,否则您的计划不会运行。
@Scheduled 要求使用常量值。这意味着,对 fixedDelayinitialDelaycron 等尝试生成/构建值将不起作用,并且会导致编译错误。

3.3 服务中的业务逻辑

尽管前一个示例有效,但通常情况下你不会希望将业务逻辑放置在作业中。一种更好的方法是创建一个额外的服务,由 JobService 调用。此方法将业务逻辑与调度逻辑解耦。此外,它还便于测试和维护。让我们看一个示例

创建以下服务

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

import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j

import java.text.SimpleDateFormat

@Slf4j
@CompileStatic
class EmailService {

    void send(String user, String message) {
        log.info "Sending email to {} : {} at {}",
                user, message, new SimpleDateFormat("dd/M/yyyy hh:mm:ss").format(new Date())
    }
}

然后创建作业

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

    static lazyInit = false (1)

    EmailService emailService (2)

    @Scheduled(cron = "0 30 4 1/1 * ?") (4)
    void execute() {
        emailService.send('[email protected]', 'Test Message') (3)
    }
}
1 强制初始化服务,而不强制初始化服务,作业将不会执行
2 注入服务
3 调用它
4 每天凌晨 04:30 触发作业

cron 表达式通常比较令人困惑,为了帮助提供明确的逻辑并让高级调度变得更容易,接下来我们将使用 TaskScheduler 来了解作业执行。

3.4 Task Scheduler Bean

要开始使用更多高级的调度技术,首先需要创建一个 TaskScheduler Bean 并进行注册。

创建以下配置文件,其中包含一个新的任务调度程序 Bean

src/main/groovy/demo/SchedulingConfiguration.groovy
package demo

import groovy.transform.CompileStatic
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler

@CompileStatic
@Configuration (1)
class SchedulingConfiguration {

    @Bean
    ThreadPoolTaskScheduler threadPoolTaskScheduler() {
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler()
        threadPoolTaskScheduler.setThreadNamePrefix("ThreadPoolTaskScheduler") (2)
        threadPoolTaskScheduler.initialize()
        threadPoolTaskScheduler
    }
}
1 使配置可被发现
2 所有相关线程都将以 ThreadPoolTaskScheduler 为前缀

如果你希望允许并发执行,请设置线程池大小,例如 threadPoolTaskScheduler.setPoolSize(5)

默认情况下,ThreadPoolTaskScheduler 仅有一个可用于执行的线程,这意味着任务不会并发执行

现在,将你的应用配置为自动发现新配置,以便你的新 Bean 可以与标准 Grails 依赖项注入配合使用

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

import grails.boot.GrailsApp
import grails.boot.config.GrailsAutoConfiguration
import groovy.transform.CompileStatic
import org.springframework.context.annotation.ComponentScan
import org.springframework.scheduling.annotation.EnableScheduling

@CompileStatic
@ComponentScan('demo') (1)
@EnableScheduling (2)
class Application extends GrailsAutoConfiguration {
    static void main(String[] args) {
        GrailsApp.run(Application, args)
    }
}
1 启用对其他配置文件的发现。
2 在此处使用 Spring @EnableScheduling 来触发调度。

3.5 可运行任务

创建并连接 TaskScheduler 后,你需要为其创建可运行任务以执行。

创建以下任务

grails-app/tasks/demo/EmailTask.groovy
package demo

import groovy.transform.CompileStatic

@CompileStatic
class EmailTask implements Runnable { (1)

    String email (2)
    String message (3)
    EmailService emailService (4)

    EmailTask(EmailService emailService, String email, String message) { (5)
        this.emailService = emailService
        this.email = email
        this.message = message
    }

    @Override
    void run() { (6)
        emailService.send(email, message)
    }
}
1 实现 Runnable
2 存储收件人电子邮件地址以供以后使用
3 存储传入消息
4 存储传入服务
5 任务构造器:设置 emailmessageemailService
6 运行任务

3.6 手动调度作业

想象以下场景。你希望在用户注册到你的应用后 2 小时向他们发送电子邮件。你希望询问他在第一次与你的服务互动时的体验。

对于本指南,我们将调度一个作业在 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 groovy.time.TimeCategory
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
import java.text.SimpleDateFormat

@Slf4j
@CompileStatic
class RegisterService {

        ThreadPoolTaskScheduler threadPoolTaskScheduler (1)
        EmailService emailService (2)

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

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

        void scheduleFollowupEmail(String email, String message) {
                threadPoolTaskScheduler.schedule(new EmailTask(emailService, email, message), startAtDate()) (4)
        }
}
1 注入 TaskScheduler Bean 的依赖项
2 注入 emailService 的依赖项
3 此方法返回一分钟后的日期
4 调度触发器

如果您执行上述代码,您会在我们安排它后一分钟看到作业执行,并带有提供的电子邮件地址。

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.EmailService            : Sending email to [email protected] : Follow up - How was your experience at 23/1/2018 07:56:21
INFO demo.EmailService            : Sending email to [email protected] : Follow up - How was your experience at 23/1/2018 07:56:41

3.7 使用 TaskSchedule 的简化 Cron

在之前的 cron 示例中,我们提到,很多时候 cron 令人生畏且不直观。为了缓解一些困惑,下面是使用 TaskScheduler 编写的之前 cron 的配置

grails-app/services/demo/todayat/DailyMailJobService.groovy
import demo.EmailService
import demo.EmailTask
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.apache.commons.lang.time.DateUtils
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
import java.time.Duration

@Slf4j
@CompileStatic
class DailyMailJobService {
    private final int HOUR = 4
    private final int MINUTE = 30
    private final int SECONDS = 0
    private static final long MILLISECONDS_IN_DAY = Duration.ofDays(1).getSeconds() * 1000 (4)

    ThreadPoolTaskScheduler threadPoolTaskScheduler (1)
    EmailService emailService (2)

    void register(String email, String message) {
        scheduleDailyEmail(email, message)
    }

    void scheduleDailyEmail(String email, String message) { (5)
        threadPoolTaskScheduler.scheduleAtFixedRate(new EmailTask(emailService, email, message), dailyDate(), MILLISECONDS_IN_DAY)
    }

    Date dailyDate() { (3)
        Date startAt = new Date(hours: HOUR, minutes: MINUTE, seconds: SECONDS)
        if(startAt.before(new Date())) {
            return (DateUtils.addDays(startAt, 1))
        }
        startAt
    }
}
1 注入 TaskScheduler Bean 的依赖项
2 注入 emailService 的依赖项
3 计算开始时间
4 计算重复间隔
5 根据开始时间和重复间隔安排任务

根据 TaskScheduler 编写您的计划后,在 bootstrap 中注册作业。

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

import demo.todayat.DailyMailJobService
import groovy.transform.CompileStatic

@CompileStatic
class BootStrap {

        RegisterService registerService
    DailyMailJobService dailyMailJobService

    def init = { servletContext ->
        final String followUp = 'Follow up - How was your experience'
            registerService.register('[email protected]', followUp)
            sleep(20_000)
            registerService.register('[email protected]', followUp)
        dailyMailJobService.register('[email protected]', 'Daily Reminder')
    }
    def destroy = {
    }
}

3.8 小结

在本指南中,我们学习了如何使用固定延迟、初始延迟和 cron 使用 @Scheduled 注释配置作业,以及如何使用 TaskScheduler 和任务手动配置作业。此外,我们了解到 @Scheduled 或 TaskScheduler 的默认配置防止了并发执行。

要了解更多信息,请查看 Spring 任务执行和计划文档 以了解更多内容。

4 Grails 帮助

Object Computing, Inc. (OCI) 赞助了本指南的编写。提供多种咨询和支持服务。

OCI 是 Grails 的家

认识团队