显示导航

Grails + @Scheduled

了解如何在 Grails 应用程序内使用 Spring 任务执行和计划以计划周期性任务

作者:Ben Rhine

Grails 版本 3.3.2

1 培训

Grails 培训——由创建和积极维护 Grails 框架的人员开发并交付!.

2 入门

如今,每晚 12 点、每小时或每周几次等按计划运行某种类型的cron任务或计划任务的情况非常普遍。在 Java 世界中,过去一直使用Quartz库来执行此操作。

在此指南中,你将了解如何在 Grails 应用程序中使用原生Spring 任务执行和计划来计划周期性任务。

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

2.1 如何完成该指南

若要开始学习,请执行以下操作

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

  • initial 初始项目。通常是一个简单的 Grails 应用程序,以及一些额外的代码作为开端。

  • complete 已完成的示例。它是根据指南提供步骤和将这些更改应用于initial文件夹的结果。

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

  • grails-guides/grails-scheduled/initial中进行cd

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

如果你在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 {

    boolean 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 Logging Quickcast中所述。
2 删除@Transactional,因为此服务不需要事务。添加@CompileStatic来提高性能。
3 默认情况下,Grails 的服务延迟加载。你可以禁用此功能,并使用 lazyInit 属性实现热切初始化。在不强制将服务热切初始化的情况下,作业将不会执行。
4 每 10 秒创建一次触发器
5 再创建一个触发器,每 45 秒触发一次,并设置 5 秒(5000 毫秒)的初始延迟

现在启动应用程序

./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 45 秒作业会在应用启动后 5 秒才启动。请参阅触发器配置中的startDelay
3 首次执行后 10 秒,10 秒作业的第二次执行
4 首次执行后 45 秒,45 秒作业的第二次执行
当使用@Scheduled时,需要boolean lazyInit = false,否则计划不会运行。
@Scheduled要求使用 CONSTANT 值。这意味着尝试对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 ${user} : ${message}"+ ' at ' + new SimpleDateFormat("dd/M/yyyy hh:mm:ss").format(new Date())
    }
}

然后创建作业

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

    boolean 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 org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler

@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

@CompileStatic
@ComponentScan('demo') (1)
class Application extends GrailsAutoConfiguration {
    static void main(String[] args) {
        GrailsApp.run(Application, args)
    }
}
1 启用对其他配置文件的发现

3.5 Runnable 任务

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

创建以下任务

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

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 任务构造函数:设置 email、`message 和服务
6 运行任务

3.6 手动调度作业

设想以下场景。您希望在用户注册您的应用程序后 2 小时向他们发送一封电子邮件。您想询问他们在初次使用您的服务时的体验。

在本指南中,我们将调度一个作业,以便在一分钟后触发。

修改 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.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 (startAt + 1)
        }
        startAt
    }
}
1 注入我们的 TaskScheduler Bean
2 注入 emailService
3 计算开始时间
4 计算重复间隔
5 使用开始时间和重复间隔安排任务

使用 TaskScheduler 编写计划后,在启动程序中注册作业。

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 总结

在本指南中,我们学习了如何使用 @Scheduled 批注使用 fixedDelay、initialDelay 和 cron 配置作业,以及如何使用 TaskScheduler 和 tasks 手动配置作业。此外,我们了解到 @Scheduled 或 TaskScheduler 的默认配置可以阻止并发执行。

如需了解更多信息,请查阅 Spring 任务执行和计划文档 以了解更多信息。

4 Grails 帮助

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

OCI 是 Grails 的主办方

认识团队