在你的 Grails 应用程序内计划定期任务
了解如何在你的 Grails 应用程序内使用 Schwartz 计划定期任务
作者:伊万·洛佩兹,塞尔吉奥·德尔·阿莫
Grails 版本 3.3.2
1 培训
Grails 培训 - 由创建并积极维护 Grails 框架的人员开发和提供!
2 开始使用
现在通常需要每晚午夜、每小时乃至每周多次运行某种 cron 或计划任务……在 Java 世界中,我们已经使用 Quartz 库很多年了。
在本指南中,你将了解如何使用 Grails Schwartz 插件 在 Grails 应用程序内计划定期任务
2.1 如何完成指南
请执行以下操作以开始
-
下载并解压源代码
或
-
克隆 Git 存储库
git clone https://github.com/grails-guides/grails-schwartz.git
Grails 指南存储库包含两个文件夹
-
initial
初始项目。通常是简单的 Grails 应用,还有一些额外代码让你快速入门。 -
complete
已完成的示例。这是完成指南中介绍的所有步骤并将这些更改应用到initial
文件夹中的结果。
要完成指南,请转到 initial
文件夹
-
cd
进入grails-guides/grails-schwartz/initial
并按照下一部分中的说明进行操作。
如果你 cd 进入 grails-guides/grails-schwartz/complete ,则可以直接转到已完成示例 |
3 撰写指南
3.1 安装插件
要安装插件,请修改 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
块中
compile 'com.agileorbit:schwartz:1.0.1'
3.2 为程序包 demo 设置日志级别 INFO
在此指南中,我们将使用多个日志语句来展示作业执行。
在 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 作业的基本框架是
class HelloWorldJobService implements SchwartzJob { (1)
void execute(JobExecutionContext context) throws JobExecutionException {
(2)
}
void buildTriggers() {
(3)
}
}
1 | 实施 SchwartzJob |
2 | 作业逻辑在此 |
3 | 作业的不同触发器 |
我们修改上一个代码,并添加以下内容
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
调用的附加服务。此方法将您的业务逻辑与调度逻辑分开。此外,它促进了测试和维护。我们来看一个示例
创建以下服务
package demo
import groovy.transform.CompileStatic
@CompileStatic
class EmailService {
void send(String user) {
log.info "Sending email to ${user}"
}
}
然后创建作业
@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 配置可写为
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` 的新服务。
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 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`。
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`。
示例代码可能如下所示。
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 | 在事务中包装作业执行。 |
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 插件文档 以了解详细信息。