Grails 事件
Grails 事件处理常见方案。用户在应用程序中注册,然后应用程序向用户发送一封欢迎电子邮件。
作者:塞尔吉奥·德尔·阿莫
Grails 版本 3.3.11
1 入门
在本指南中,你将使用 Grails 事件 处理一个常见场景。用户在一个应用程序中注册,然后应用程序向用户发送一封欢迎电子邮件。例如,要求他验证其电子邮件地址。我们将在用户注册时发布一个事件以触发电子邮件通知。
1.1 你需要什么
要完成本指南,你需要以下内容
-
一段时间
-
一个不错的文本编辑器或 IDE
-
安装 JDK 1.7 或更高版本,并正确配置了
JAVA_HOME
1.2 如何完成本指南
按照以下步骤开始
-
下载并解压缩源代码
或
-
克隆 Git 代码库
git clone https://github.com/grails-guides/grails-events.git
Grails 指南代码库包含两个文件夹
-
initial
初始项目。通常是一个简单的 Grails 应用程序,其中包含一些附加代码,以便你快速入门。 -
complete
一个完整的示例。它是按照指南所述的步骤进行操作并在initial
文件夹中应用这些更改的结果。
要完成指南,请转到 initial
文件夹
-
cd
到grails-guides/grails-events/initial
按照后续章节中的说明操作。
如果您 cd 到 grails-guides/grails-events/complete ,则您可以直接进入完整示例 |
2 编写应用程序
Grails 3.3 引入了新的事件 API,它替换了先前的基于 Reactor 2.x(不再维护且弃用)的实现。
在 Grails 3.3 及更高版本中,引入了新的 EventBus 抽象。与 PromiseFactory 概念类似,EventBus 接口有适用于 GPars 和 RxJava 等常见异步框架的实现。
您的项目已经包含 events 依赖项
compile "org.grails.plugins:events"
Grails Events 功能文档可在 async.grails.org 中找到。
2.1 域类
添加两个域类。
package demo
class User {
String firstName
String lastName
String email
static constraints = {
firstName nullable: false
lastName nullable: false
email nullable: false, email: true, unique: true
}
}
package demo
class Notification {
String email
String subject
static constraints = {
subject nullable: false
email nullable: false, email: true
}
}
2.2 数据服务
GORM 6.1 中引入的 数据服务 通过使用 GORM 逻辑自动实现抽象类或接口的功能免去了实现服务层逻辑的工作。
为之前的域类添加两个数据服务类。
package demo
import grails.gorm.services.Service
@Service(User)
interface UserService {
int count()
void deleteByEmail(String email)
}
package demo
import grails.gorm.services.Service
@Service(Notification)
interface NotificationService {
int count()
void deleteByEmail(String email)
}
2.3 Url 映射
修改 UrlMappings
以映射处理用户注册的端点。
"/signup"(controller: 'register', action: 'index')
"/register"(controller: 'register', action: 'save')
/signup
展示了一个注册表单。
/register
处理一个保存用户的 POST 提交。
2.4 视图
创建一个包含用户注册表单的 GSP 文件。
<html>
<head>
<title>Sign Up</title>
<meta name="layout" content="main" />
<style type="text/css">
ol li {
list-style-type: none;
margin: 10px auto;
}
ol li label {
width: 120px;
}
</style>
</head>
<body>
<div id="content" role="main">
<g:hasErrors bean="${signUpInstance}">
<ul class="errors">
<g:eachError bean="${signUpInstance}">
<li><g:message error="${it}"/></li>
</g:eachError>
</ul>
</g:hasErrors>
<p class="message">${flash.message}</p>
<h1><g:message code="register.title" default="Register Form"/></h1>
<g:form controller="register" action="save" method="POST">
<ol>
<li>
<label for="firstName">First Name</label>
<g:textField name="firstName" id="firstName" value="${signUpInstance.firstName}" />
</li>
<li>
<label for="lastName">Last Name</label>
<g:textField name="lastName" id="lastName" value="${signUpInstance.lastName}" />
</li>
<li>
<label for="email">Email</label>
<g:textField name="email" id="email" value="${signUpInstance.email}" />
</li>
<li>
<g:submitButton name="Submit" id="submit"/>
</li>
</ol>
</g:form>
</div>
</body>
</html>
2.5 服务
添加一个在数据库中处理用户注册的服务。
package demo
import grails.events.annotation.Publisher
import grails.gorm.transactions.Transactional
import grails.validation.ValidationException
import groovy.transform.CompileStatic
import org.springframework.context.MessageSource
import org.springframework.dao.DuplicateKeyException
@CompileStatic
class RegisterService {
MessageSource messageSource
@Transactional
@Publisher (1)
User saveUser(User user) {
user.save(failOnError: true) (2)
user
}
SaveUserResult register(RegisterCommand cmd, Locale locale) {
User user = cmd as User
try {
saveUser(user)
return new SaveUserResult(message: userSavedMessage(cmd.email, locale))
} catch (ValidationException | DuplicateKeyException validationException) {
return new SaveUserResult(error: userSavedErrorMessage(cmd.email, locale))
}
}
String userSavedErrorMessage(String email, Locale locale) {
messageSource.getMessage('user.saved.error',
[email] as Object[],
"Error while saving user with ${email} email address",
locale)
}
String userSavedMessage(String email, Locale locale) {
messageSource.getMessage('user.saved',
[email] as Object[],
"User saved with ${email} email address",
locale)
}
}
@CompileStatic
class SaveUserResult {
String message
String error
}
1 | 使用 @Publisher 注释该方法。该事件 ID 使用方法名称 saveUser |
2 | 如果验证失败,则引发异常。这会回滚事务。如果回滚事务,则不会触发任何事件。 |
我们在一个不同的服务中使用事件。使用者与发送者完全解耦。在实际应用程序中,您可能会向刚注册的用户发送一封电子邮件,要求他验证自己的电子邮件地址。在本指南中,我们在数据库中保存一个 Notification
。
package demo
import grails.events.annotation.Subscriber
import grails.gorm.transactions.Transactional
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
@Slf4j
@CompileStatic
class WelcomeEmailService {
@Transactional
@Subscriber (1)
void saveUser(User user) {
Notification notification = new Notification(email: user.email, subject: 'Welcome to Grails App')
// TODO Send Email
if ( !notification.save() ) {
log.error('unable to save notification {}', notification.errors.toString())
}
}
}
1 | 要使用事件,请使用 Subscriber 注释。方法名称 saveUser 默认用于事件 ID,尽管它可以以单词“on”开头。换句话说,对于本指南示例,方法名称 saveUser 或 onSaveUser 都可以工作。 |
2.6 控制器
创建一个命令对象来处理表单数据绑定和验证。
package demo
import grails.validation.Validateable
class RegisterCommand implements Validateable {
String firstName
String lastName
String email
static constraints = {
firstName nullable: false
lastName nullable: false
email nullable: false, email: true
}
Object asType(Class clazz) {
if (clazz == User) {
def user = new User()
copyProperties(this, user)
return user
}
else {
super.asType(clazz)
}
}
def copyProperties(source, target) {
source.properties.each { key, value ->
if (target.hasProperty(key) && !(key in ['class', 'metaClass'])) {
target[key] = value
}
}
}
}
创建一个包含两个操作的控制器,一个操作显示注册表单,另一个操作处理表单提交。
package demo
import groovy.transform.CompileStatic
@CompileStatic
class RegisterController {
static allowedMethods = [index: 'GET', save: 'POST']
RegisterService registerService
def index() {
[signUpInstance: new RegisterCommand()]
}
def save(RegisterCommand cmd) {
if ( cmd.hasErrors() ) {
render view: 'index', model: [signUpInstance: cmd]
return
}
SaveUserResult result = registerService.register(cmd, request.locale)
flash.message = result.message
if ( result.error ) {
flash.error = result.error
render view: 'index', model: [signUpInstance: cmd]
return
}
redirect action: 'index'
}
}
2.7 测试
创建一个功能测试,以验证在创建 User
时,由于事件触发,创建了 Notification
实例。
package demo
import geb.Page
import geb.module.TextInput
class SignUpPage extends Page {
static url = '/signup'
static content = {
firstNameInput { $(name: "firstName").module(TextInput) }
lastNameInput { $(name: "lastName").module(TextInput) }
emailInput { $(name: "email").module(TextInput) }
submitButton { $('#submit') }
}
void submit(String firstName, String lastName, String email) {
firstNameInput.text = firstName
lastNameInput.text = lastName
emailInput.text = email
submitButton.click()
}
}
package demo
import geb.spock.GebSpec
import grails.testing.mixin.integration.Integration
import spock.lang.Ignore
@Ignore
@Integration
class RegisterControllerSpec extends GebSpec {
NotificationService notificationService
UserService userService
def "If you signup a User, an Event triggers which causes a Notification to be saved"() {
when: 'you signup with a non existing user'
SignUpPage page = to SignUpPage
page.submit('Sergio', 'del Amo', '[email protected]')
then: 'the user gets created and a notification is saved due to the event being triggered'
userService.count() == old(userService.count()) + 1
notificationService.count() == old(notificationService.count()) + 1
when: 'you try to signup with a user which is already in the database'
page = to SignUpPage
page.submit('Sergio', 'del Amo', '[email protected]')
then: 'The user is not saved and no event gets triggered'
noExceptionThrown()
userService.count() == old(userService.count())
notificationService.count() == old(notificationService.count())
cleanup:
userService.deleteByEmail('[email protected]')
notificationService.deleteByEmail('[email protected]')
}
}
3. 运行测试
运行测试的方法
./grailsw
grails> test-app
grails> open test-report
或
./gradlew check
open build/reports/tests/index.html