显示导航

Grails 事件

处理常见场景的 Grails 事件。用户在应用程序中注册,而应用程序向用户发送欢迎电子邮件。

作者:Sergio del Amo

Grails 版本 5.0.1

1 入门

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

1.1 所需内容

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

  • 一些业余时间

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

  • 安装了 JDK 1.8 或更高版本,且已正确配置了 JAVA_HOME

1.2 指南完成方式

你可以按照以下步骤进行操作

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。

您的项目已包含事件依赖项。

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.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

4 Grails 帮助

Object Computing, Inc. (OCI) 赞助创建本指南。提供各种咨询和支持服务。

OCI 是 Grails 的家

认识团队