显示导航

Grails 事件

Grails 事件处理常见方案。用户在应用程序中注册,然后应用程序向用户发送一封欢迎电子邮件。

作者:塞尔吉奥·德尔·阿莫

Grails 版本 3.3.11

1 入门

在本指南中,你将使用 Grails 事件 处理一个常见场景。用户在一个应用程序中注册,然后应用程序向用户发送一封欢迎电子邮件。例如,要求他验证其电子邮件地址。我们将在用户注册时发布一个事件以触发电子邮件通知。

1.1 你需要什么

要完成本指南,你需要以下内容

  • 一段时间

  • 一个不错的文本编辑器或 IDE

  • 安装 JDK 1.7 或更高版本,并正确配置了 JAVA_HOME

1.2 如何完成本指南

按照以下步骤开始

Grails 指南代码库包含两个文件夹

  • initial 初始项目。通常是一个简单的 Grails 应用程序,其中包含一些附加代码,以便你快速入门。

  • complete 一个完整的示例。它是按照指南所述的步骤进行操作并在 initial 文件夹中应用这些更改的结果。

要完成指南,请转到 initial 文件夹

  • cdgrails-guides/grails-events/initial

按照后续章节中的说明操作。

如果您 cdgrails-guides/grails-events/complete,则您可以直接进入完整示例

2 编写应用程序

Grails 3.3 引入了新的事件 API,它替换了先前的基于 Reactor 2.x(不再维护且弃用)的实现。

在 Grails 3.3 及更高版本中,引入了新的 EventBus 抽象。与 PromiseFactory 概念类似,EventBus 接口有适用于 GPars 和 RxJava 等常见异步框架的实现。

您的项目已经包含 events 依赖项

build.gradle
    compile "org.grails.plugins:events"

Grails Events 功能文档可在 async.grails.org 中找到。

2.1 域类

添加两个域类。

grails-app/domain/demo/User.groovy
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
    }
}
grails-app/domain/demo/Notification.groovy
package demo

class Notification {
    String email
    String subject

    static constraints = {
        subject nullable: false
        email nullable: false, email: true
    }
}

2.2 数据服务

GORM 6.1 中引入的 数据服务 通过使用 GORM 逻辑自动实现抽象类或接口的功能免去了实现服务层逻辑的工作。

为之前的域类添加两个数据服务类。

grails-app/services/demo/UserService.groovy
package demo

import grails.gorm.services.Service

@Service(User)
interface UserService {
    int count()
    void deleteByEmail(String email)
}
grails-app/services/demo/NotificationService.groovy
package demo

import grails.gorm.services.Service

@Service(Notification)
interface NotificationService {
    int count()
    void deleteByEmail(String email)
}

2.3 Url 映射

修改 UrlMappings 以映射处理用户注册的端点。

grails-app/controllers/demo/UrlMappings.groovy
        "/signup"(controller: 'register', action: 'index')
        "/register"(controller: 'register', action: 'save')

/signup 展示了一个注册表单。

/register 处理一个保存用户的 POST 提交。

2.4 视图

创建一个包含用户注册表单的 GSP 文件。

grails-app/views/register/index.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 服务

添加一个在数据库中处理用户注册的服务。

grails-app/services/demo/RegisterService.groovy
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

grails-app/services/demo/WelcomeEmailService.groovy
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”开头。换句话说,对于本指南示例,方法名称 saveUseronSaveUser 都可以工作。

2.6 控制器

创建一个命令对象来处理表单数据绑定和验证。

grails-app/controllers/demo/RegisterCommand.groovy
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
            }
        }
    }
}

创建一个包含两个操作的控制器,一个操作显示注册表单,另一个操作处理表单提交。

grails-app/controllers/demo/RegisterController.groovy
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 实例。

src/integration-test/groovy/demo/SignUpPage.groovy
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()
    }
}
src/integration-test/groovy/demo/RegisterControllerSpec.groovy
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

4. Grails 帮助

Object Computing, Inc. (OCI) 赞助本指南的编写。各种咨询和支持服务可随时获得。

OCI 是 Grails 的家

团队成员