Grails + @Scheduled
学习如何在 Grails 应用程序中使用 Spring 任务执行和计划来安排定期任务
作者: Ben Rhine
Grails 版本 4.0.1
1 培训
Grails 培训 - 由创建并积极维护 Grails 框架的人员开发和提供的!。
2 开始使用
如今,需要每隔午夜、每小时或每周运行几次的某种“cron”或计划任务已经相当常见。在 Java 领域,多年来我们一直使用“Quartz”库来执行此操作。
在该指南中,你将学习如何使用本机 Spring 任务执行和计划在 Grails 应用程序内安排定期任务。
有关其他文档,请参阅: @Scheduled TaskScheduler PeriodicTrigger
2.1 如何完成指南
按照以下步骤开始
-
下载并解压缩源文件
或
-
克隆 Git 存储库
git clone https://github.com/grails-guides/grails-scheduled.git
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
的末尾添加下一条语句
logger('demo', INFO, ['STDOUT'], false)
上述行使用日志级别 INFO 为包 demo
配置了一个记录器,其中使用 STDOUT 追加器,并将自适应性设置为 false。
3.2 创建一个简单任务
由于计划是原生功能。只需执行
./grailsw create-service HelloWorldJob
这将使用空模板方法创建文件 grails-app/services/demo/HelloWorldJobService.groovy
。
基本框架为
@Transactional (1)
class HelloWorldJobService {
def serviceMethod() {
(2)
}
}
1 | 默认情况下,服务为 Transactional |
2 | 任务逻辑位于这里 |
让我们修改以前的代码并添加以下内容
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 要求使用常量值。这意味着,对 fixedDelay 、initialDelay 、cron 等尝试生成/构建值将不起作用,并且会导致编译错误。 |
3.3 服务中的业务逻辑
尽管前一个示例有效,但通常情况下你不会希望将业务逻辑放置在作业中。一种更好的方法是创建一个额外的服务,由 JobService
调用。此方法将业务逻辑与调度逻辑解耦。此外,它还便于测试和维护。让我们看一个示例
创建以下服务
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())
}
}
然后创建作业
@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
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 依赖项注入配合使用
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 后,你需要为其创建可运行任务以执行。
创建以下任务
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 | 任务构造器:设置 email 、message 和 emailService |
6 | 运行任务 |
3.6 手动调度作业
想象以下场景。你希望在用户注册到你的应用后 2 小时向他们发送电子邮件。你希望询问他在第一次与你的服务互动时的体验。
对于本指南,我们将调度一个作业在 1 分钟后触发。
修改 BootStrap.groovy
以两次调用一个名为 RegisterService
的新服务。
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
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 的配置
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 中注册作业。
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 任务执行和计划文档 以了解更多内容。