Grails 事件
处理常见场景的 Grails 事件。用户在应用程序中注册,而应用程序向用户发送欢迎电子邮件。
作者:Sergio del Amo
Grails 版本 5.0.1
1 入门
在本指南中,你将使用 Grails 事件 来处理一个常见场景。用户在应用程序中注册,而应用程序向用户发送欢迎电子邮件。例如,要求用户验证其电子邮件地址。用户注册后,我们将通过发布事件来触发电子邮件通知。
1.1 所需内容
你需要以下内容才能完成本指南
-
一些业余时间
-
一个不错的文本编辑器或 IDE
-
安装了 JDK 1.8 或更高版本,且已正确配置了
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 推出了一个新的 Events API,替换了以前基于 Reactor 2.x(不再维护且已弃用)的实现。
在 Grails 3.3 及更高版本中,引入了一个新的 EventBus 抽象。和 PromiseFactory 概念一样,EventBus 接口有一些实现,用于常见的异步框架,如 GPars 和 RxJava。
您的项目已包含事件依赖项。
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.gorm.transactions.Rollback
import grails.testing.mixin.integration.Integration
@Integration
class RegisterControllerSpec extends GebSpec {
NotificationService notificationService
UserService userService
@Rollback
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