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 如何完成该指南
若要开始学习,请执行以下操作
-
下载并解压源码
或
-
克隆Git存储库
git clone https://github.com/grails-guides/grails-scheduled.git
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
末尾添加下一条语句
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 {
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 值。这意味着尝试对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 ${user} : ${message}"+ ' at ' + new SimpleDateFormat("dd/M/yyyy hh:mm:ss").format(new Date())
}
}
然后创建作业
@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 的以下配置文件
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 依赖项注入配合使用
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 后,您需要为其创建可运行任务以执行。
创建以下任务
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
的新服务两次。
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.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 编写计划后,在启动程序中注册作业。
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 任务执行和计划文档 以了解更多信息。