显示导航

Grails 3 和 RabbitMQ 的消息队列

了解如何将消息队列与 Grails 3 和 RabbitMQ-Native 插件一起使用

作者: Ben Rhine、Sergio del Amo

Grails 版本 3.3.2

1 培训

Grails 培训 - 由创建和积极维护 Grails 框架的人员开发并提供!

2 入门

在本指南中,我们将向你展示如何使用 Grails 3 应用设置和使用 RabbitMQ。我们将使用 rabbitmq-native 插件

2.1 需要准备什么

若要完成本指南,你需要具备以下条件

  • 一些上手时间

  • 一个合适的文本编辑器或 IDE

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

2.2 如何完成指南

要开始,请执行以下操作

Grails 指南仓库包含三个文件夹

  • initial 初始项目。通常是一个简单的 Grails 应用,还包含一些附加代码来让你有一个良好的开端。

  • complete

  • complete-analytics

在本指南中,你将创建两个 Grails 应用。completecomplete-analytics 这两个应用都是完整示例。这是按照指南给出的步骤操作并将这些更改应用于 `initial` 文件夹得到的结果。

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

  • grails-guides/grails-rabbitmq/initialcd

并依照后续章节的说明执行。

如果您cdgrails-guides/grails-rabbitmq/completegrails-guides/grails-rabbitmq/complete-analytics,可以直接转到完整示例

3 应用程序概览

在本指南中,我们会设置一个消息队列,以便两个不同的应用程序相互通信。本指南中,我们有一个列出书籍及书籍详细信息的应用。我们想跟踪每个书籍的查看次数。我们额外添加一个分析应用,以跟踪每个书籍的查看次数。

appsummary

4 消息队列 - RabbitMQ

消息队列的构建提供了异步通信,这意味着发送方和接收方不需要同时与消息队列进行交互。当发送一条消息时,它将被添加到队列中并存储起来,直到接收方检索该消息。消息队列既可以在应用内部也可以在多个应用之间外部使用。存在多种不同的消息队列实现,但 RabbitMQ 是其中一项更流行的实现,也是我们将在本指南中使用的一项。

4.1 设置 RabbitMQ 服务

有多种不同的安装和运行 RabbitMQ 的方法可以让 RabbitMQ 在您的系统中启动并运行。您可以按照 此处的 RabbitMQ 自身说明进行操作,但我们建议您按照我们设置它的方法来操作,以便保持一致性。

为了实现类似于真实世界的设置,我们选择使用 Docker 来设置 RabbitMQ,这是快速、简单的方法,如果您遇到任何问题,它也便于推倒重来。Docker 是一个容器引擎,可以快速简单地安装许多不同的框架和产品,并且有助于避免本地系统中的差异,从而为要运行的产品提供一个干净的实例。我们假设您已在系统上安装 Docker,但如果您尚未安装,可以从 此处进行安装。

确保 Docker 在本地系统上正在运行(您应该看到一头小鲸鱼)

dockerIcon

一旦您知道 Docker 已启动并从终端运行,请运行以下命令。

$ docker run -d --hostname my-rabbit --name some-rabbit -p 15672:15672 -p 4369:4369 -p 5671:5671 -p 5672:5672 -p 15671:15671 -p 25672:25672 rabbitmq:3-management

上述命令可以帮您做很多事情,它会下载并运行 RabbitMQ 的 Docker 实例,设置主机名、给实例一个名称、配置您成功访问 RabbitMQ 所需的端口,并包含管理界面,因此我们可以更加深入地了解消息队列的工作方式。命令完成后,将看到类似于以下内容的输出。

dockerCmdSuccess

除了 Docker,您可能还会喜欢一个更有视觉效果的界面来查看 RabbitMQ 容器的工作情况。如果是这种情况,那么请继续下载“Kitematic”,这是一个面向底层 Docker 服务的图形用户界面。Docker 甚至会帮助您从其菜单中获取 Kitematic。

dockerMenu

如果你安装了 Kitematic,现在应该看到你的 RabbitMQ 实例在左侧运行。

kitematicRabbit

如上所述,我们包括了 RabbitMQ 管理界面,以便我们可以看到一些有关我们消息队列的详细信息。你可以访问位于 localhost:15672 的管理界面。

rabbitMQAdminLogin

要登录,请使用用户名/密码 guest/guest。成功登录后,你将看到以下页面

rabbitAdminHome

有关此特定 Docker 容器的更多信息,请参阅文档此处

5 图书应用程序

我们已为 initial/grails-app/assets/images 文件夹添加了必要的资产(图书封面图像)。

继续前进,我们需要向我们的应用程序添加其他功能,因为我们不再只使用单个应用程序。首先,我们首先需要添加一个 Book 域。

grails-app/domain/demo/Book.groovy
package demo

import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
class Book {
    String image
    String title
    String author
    String about
    String href
    static mapping = {
        image nullable: false
        title nullable: false
        author nullable: false
        about nullable: false
        href nullable: false
        about type: 'text'
    }
}

我们将使用自定义的 findAll 来返回我们的图书列表,为此我们将需要使用额外的 BookImage 对象。首先在 src/main/groovy 目录中创建你的 BookImage

src/main/groovy/demo/BookImage.groovy
package demo

import groovy.transform.CompileStatic

@CompileStatic
class BookImage {
    Long id
    String image
}

Book 创建默认 CRUD 动作,利用GORM 数据服务

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

import grails.gorm.services.Service
import grails.gorm.transactions.ReadOnly
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import org.grails.datastore.mapping.query.api.BuildableCriteria
import org.hibernate.transform.Transformers

interface IBookDataService {
    Book save(String title, String author, String about, String href, String image)
    Number count()
    Book findById(Long id)
}

@Service(Book)
abstract class BookDataService implements IBookDataService {

    @CompileDynamic
    @ReadOnly
    List<BookImage> findAll() {
        BuildableCriteria c = Book.createCriteria()
        c.list {
            resultTransformer(Transformers.aliasToBean(BookImage))
            projections {
                property('id', 'id')
                property('image', 'image')
            }
        }
    }
}

接下来,将我们的新控制器连接到我们刚刚创建的服务。我们的索引将利用我们的自定义 findAll 来返回图书的完整列表,而我们的展示将利用数据服务 findById

grails-app/controllers/demo/BookController.groovy
package demo

import groovy.transform.CompileStatic

@CompileStatic
class BookController {

    static allowedMethods = [index: 'GET', show: 'GET']

    BookDataService bookDataService

    def index() {
        [bookList: bookDataService.findAll()]
    }

    def show(Long id) {
        [bookInstance: bookDataService.findById(id)]
    }

}

然后,我们需要使用我们的 Bootstrap.groovy 实际创建图书数据

grails-app/init/demo/Bootstrap.groovy
package demo

import groovy.transform.CompileStatic

@CompileStatic
class BootStrap {

    public final static List< Map<String, String> > GRAILS_BOOKS = [
            [
                    title : 'Grails 3 - Step by Step',
                    author: 'Cristian Olaru',
                    href: 'https://grailsthreebook.com/',
                    about : 'Learn how a complete greenfield application can be implemented quickly and efficiently with Grails 3 using profiles and plugins. Use the sample application that accompanies the book as an example.',
                    image: 'grails_3_step_by_step.png',
            ],
            [
                    title : 'Practical Grails 3',
                    author: ' Eric Helgeson',
                    href  : 'https://www.grails3book.com/',
                    about : 'Learn the fundamental concepts behind building Grails applications with the first book dedicated to Grails 3. Real, up-to-date code examples are provided, so you can easily follow along.',
                    image: 'pratical-grails-3-book-cover.png',
            ],
            [
                    title : 'Falando de Grails',
                    author: 'Henrique Lobo Weissmann',
                    href  : 'http://www.casadocodigo.com.br/products/livro-grails',
                    about : 'This is the best reference on Grails 2.5 and 3.0 written in Portuguese. It&#39;s a great guide to the framework, dealing with details that many users tend to ignore.',
                    image: 'grails_weissmann.png',
            ],
            [
                    title : 'Grails Goodness Notebook',
                    author: 'Hubert A. Klein Ikkink',
                    href  : 'https://leanpub.com/grails-goodness-notebook',
                    about : 'Experience the Grails framework through code snippets. Discover (hidden) Grails features through code examples and short articles. The articles and code will get you started quickly and provide deeper insight into Grails.',
                    image: 'grailsgood.png',
            ],
            [
                    title : 'The Definitive Guide to Grails 2',
                    author: 'Jeff Scott Brown and Graeme Rocher',
                    href  : 'http://www.apress.com/9781430243779',
                    about : 'As the title states, this is the definitive reference on the Grails framework, authored by core members of the development team.',
                    image: 'grocher_jbrown_cover.jpg',
            ],
            [
                    title : 'Grails in Action',
                    author: 'Glen Smith and Peter Ledbrook',
                    href  : 'http://www.manning.com/gsmith2/',
                    about : 'The second edition of Grails in Action is a comprehensive introduction to Grails 2 focused on helping you become super-productive fast.',
                    image: 'gsmith2_cover150.jpg',
            ],
            [
                    title : 'Grails 2: A Quick-Start Guide',
                    author: 'Dave Klein and Ben Klein',
                    href  : 'http://www.amazon.com/gp/product/1937785777?tag=misa09-20',
                    about : 'This revised and updated edition shows you how to use Grails by iteratively building a unique, working application.',
                    image : 'bklein_cover.jpg',
            ],
            [
                    title : 'Programming Grails',
                    author: 'Burt Beckwith',
                    href  : 'http://shop.oreilly.com/product/0636920024750.do',
                    about : 'Dig deeper into Grails architecture and discover how this application framework works its magic.',
                    image: 'bbeckwith_cover.gif'
            ]
    ] as List< Map<String, String> >

    public final static List< Map<String, String> > GROOVY_BOOKS = [
            [
                    title: 'Making Java Groovy',
                    author: 'Ken Kousen',
                    href: 'http://www.manning.com/kousen/',
                    about: 'Make Java development easier by adding Groovy. Each chapter focuses on a task Java developers do, like building, testing, or working with databases or restful web services, and shows ways Groovy can make those tasks easier.',
                    image: 'Kousen-MJG.png',
            ],
            [
                    title: 'Groovy in Action, 2nd Edition',
                    author: 'Dierk König, Guillaume Laforge, Paul King, Cédric Champeau, Hamlet D\'Arcy, Erik Pragt, and Jon Skeet',
                    href: 'http://www.manning.com/koenig2/',
                    about: 'This is the undisputed, definitive reference on the Groovy language, authored by core members of the development team.',
                    image: 'regina.png',
            ],
            [
                    title: 'Groovy for Domain-Specific Languages',
                    author: 'Fergal Dearle',
                    href: 'http://www.packtpub.com/groovy-for-domain-specific-languages-dsl/book',
                    about: 'Learn how Groovy can help Java developers easily build domain-specific languages into their applications.',
                    image: 'gdsl.jpg',
            ],
            [
                    title: 'Groovy 2 Cookbook',
                    author: 'Andrey Adamovitch, Luciano Fiandeso',
                    href: 'http://www.packtpub.com/groovy-2-cookbook/book',
                    about: 'This book contains more than 90 recipes that use the powerful features of Groovy 2 to develop solutions to everyday programming challenges.',
                    image: 'g2cook.jpg',
            ],
            [
                    title: 'Programming Groovy 2',
                    author: 'Venkat Subramaniam',
                    href: 'http://pragprog.com/book/vslg2/programming-groovy-2',
                    about: 'This book helps experienced Java developers learn to use Groovy 2, from the basics of the language to its latest advances.',
                    image: 'vslg2.jpg'
            ],
    ] as List< Map<String, String> >

    BookDataService bookDataService

    def init = { servletContext ->
        for (Map<String, String> bookInfo : (GRAILS_BOOKS + GROOVY_BOOKS)) {
            bookDataService.save(bookInfo.title, bookInfo.author, bookInfo.about, bookInfo.href, bookInfo.image)
        }
    }

    def destroy = {
    }
}

最后,我们更新 URL 映射,以便我们的默认 URL 显示我们的图书列表

grails-app/controllers/demo/UrlMappings.groovy
package demo

class UrlMappings {

    static mappings = {
        "/$controller/$action?/$id?(.$format)?"{
            constraints {
                // apply constraints here
            }
        }

        "/"(controller: "book") (1)
        "500"(view:'/error')
        "404"(view:'/notFound')
    }
}
1 更新后的默认 URL

运行应用程序

$ ./gradlew bootRun
booksHome

5.1 RabbitMQ 发布者

initial 应用程序添加 RabbitMQ Native 插件。

build.gradle
compile "org.grails.plugins:rabbitmq-native:3.4.4"

一旦我们添加了依赖项,我们就可以继续添加连接到 RabbitMQ 所需的配置。

grails-app/conf/application.yml
rabbitmq:
    connections:
      - name: main (1)
        host: localhost (2)
        port: 5672
        username: guest  (3)
        password: guest  (4)
    queues:
      - name: bookQueue  (5)
1 所有连接都需要一个名称,但你可以根据需要为其命名
2 RabbitMQ 所在的位置。在这种情况下,它在我们的本地系统上,但它也可以是 Web URL 或 IP 地址
3 默认用户名guest
4 默认密码guest
5 创建 bookqueue

当查看图书详细信息时,我们会向 RabbitMQ 发布一条消息。为了做到这一点,我们创建了一个拦截器

grails-app/controllers/demo 中创建一个名为 BookShowInterceptor.groovy 的新拦截器。

grails-app/controllers/demo/BookShowInterceptor.groovy
package demo

import com.budjb.rabbitmq.publisher.RabbitMessagePublisher
import groovy.transform.CompileStatic

@CompileStatic
class BookShowInterceptor {
    RabbitMessagePublisher rabbitMessagePublisher  (1)

    BookShowInterceptor() { (2)
        match(controller:"book", action:"show")
    }

    boolean after() { (3)
        final Book book = (Book) model.bookInstance (4)

        rabbitMessagePublisher.send { (5)
            routingKey = "bookQueue"
            body = [id: book.id, title: book.title]
        }
        true
    }
}
1 注入 rabbitMessagePublisher
2 拦截当图书控制器调用 show 方法时
3 after方法在操作后但渲染视图前执行。它返回 true 以继续处理到视图。
4 该模型在after方法中可用,用以检索关于当前对象的数据
5 使用特定队列发送指定消息。使用 send 发送消息时,不期望返回消息。

6 构建分析应用程序

为此附加应用程序创建一个新的 Grails 3 应用程序。例如,使用 Grails 应用程序 Forge或命令行

$ grails create-app initial-analytics --profile=rest-api

对于本指南的多应用程序部分,我们需要能够同时运行这两个应用程序。要避免运行端口冲突,请更新您的initial-analytics应用程序application.yml以包含以下内容。

grails-app/conf/application.yml
server:
  port: 8090

创建一个域类BookPageView,它将跟踪一本图书已被浏览的次数。

grails-app/domain/demo/BookPageView.groovy
package demo

import grails.compiler.GrailsCompileStatic
import grails.rest.Resource

@Resource
@GrailsCompileStatic
class BookPageView {
    Long bookId
    String bookName
    Integer views

    static constraints = {
        bookId nullable: false, unique: true
        bookName nullable: false
        views nullable: false, min: 0
    }
}

与之前一样,为我们的BookPageView创建默认操作,利用数据服务。

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

import grails.gorm.services.Query
import grails.gorm.services.Service
import grails.gorm.transactions.Transactional
import groovy.transform.CompileStatic

@CompileStatic
interface IBookPageViewDataService {
    void delete(Serializable id)

    BookPageView save(Long bookId, String bookName, Integer views)

    BookPageView findByBookId(Long bookId)

    @Query("select $b.views from ${BookPageView b} where $b.bookId = $bookIdParam") (1)
    List<Integer> findViewsByBookId(Long bookIdParam)

    @Query("update ${BookPageView b} set ${b.views} = ${b.views} + 1 where $b.bookId = $bookIdParam") (2)
    Number updateViews(Long bookIdParam)
}

@Service(BookPageView)
abstract class BookPageViewDataService implements IBookPageViewDataService {

    @Transactional
    void increment(Long bookId, String bookName) {
        List<Integer> views = findViewsByBookId(bookId)
        if (!views) {
            save(bookId, bookName, 1)
        } else {
            updateViews(bookId)
        }
    }
}
1 使用 JPA-QL 查询执行投影
2 使用 JPA-QL 实现更新操作

添加一个集成测试

complete-analytics/src/integration-test/groovy/demo/BookPageViewDataServiceSpec.groovy
package demo

import grails.testing.mixin.integration.Integration
import spock.lang.IgnoreIf
import spock.lang.Specification

@IgnoreIf( { System.getenv('TRAVIS') as boolean } )
@Integration
class BookPageViewDataServiceSpec extends Specification {

    BookPageViewDataService bookPageViewDataService

    def "test increments"() {
        expect:
        !bookPageViewDataService.findByBookId(2)

        when:
        bookPageViewDataService.increment(2, 'Practical Grails 3')
        BookPageView bookPageView = bookPageViewDataService.findByBookId(2)

        then:
        bookPageView
        bookPageViewDataService.findByBookId(2).views == 1

        when:
        bookPageViewDataService.increment(2, 'Practical Grails 3')
        bookPageView = bookPageViewDataService.findByBookId(2)

        then:
        bookPageView
        bookPageViewDataService.findByBookId(2).views == 2

        cleanup:
        bookPageViewDataService.delete(bookPageView.id)
    }
}

6.1 RabbitMQ 消费者

我们需要创建一个新的消费者,我们在initial-analytics应用程序中创建消费者。首先导航到分析应用程序

$ cd ~/yourProjectLocation/initial-analytics

然后和之前一样创建一个新的消费者

$ ./grailsw create-consumer demo.BookPageViewConsumer

编辑生成的消费者,使其如下所示

grails-app/rabbit-consumers/demo/BookPageViewConsumer.groovy
package demo

import com.budjb.rabbitmq.consumer.MessageContext
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j

@Slf4j
@CompileStatic
class BookPageViewConsumer {

    BookPageViewDataService bookPageViewDataService (1)

    static rabbitConfig = [ (2)
            queue: "bookQueue"
    ]

    /**
     * Handle an incoming RabbitMQ message.
     *
     * @param body    The converted body of the incoming message.
     * @param context Properties of the incoming message.
     * @return
     */
    def handleMessage(Map body, MessageContext messageContext) {
        log.debug '{}', body.toString()
        bookPageViewDataService.increment((Long) body.id, (String) body.title) (3)
    }
}
1 添加我们的页面视图服务
2 告诉我们的消费者连接到哪个队列
3 告诉我们的消费者在收到消息时执行什么操作。调用我们的自定义增量操作。

7 运行应用程序

要查看我们的两个应用程序使用 RabbitMQ 异步通信,我们必须同时运行这两个应用程序。为此,打开两个单独的终端。在第一个终端中 …​

$ cd ~/yourProjectLocation/initial

然后

$ ./gradlew bootRun

接下来在第二个终端中 …​

$ cd ~/yourProjectLocation/initial-analytics

然后

$ ./gradlew bootRun

现在在运行这两个应用程序时,打开两个浏览器标签。在第一个标签页中导航到http://localhost:8080,您应看到一个可用的图书列表。

booksHome

然后在您的第二个标签页中导航到http://localhost:8090/BookPageView,您应看到一个空数组。

analyticsHome

选择一本要点击的图书,点击后您应看到图书描述

bookDescription

如果您去刷新分析页面,您现在将看到该书已被浏览 1 次。

viewCountUpdate

对几本图书执行此操作,返回并重新访问一本以前的图书,然后刷新您的分析页面以查看每本书的浏览量。

multiView

此外,如果您查看运行分析应用程序的终端,则可以看到它收到的消息。

consumerMessageReceived

8 Rabbit 管理

如第 3.1 节 [setupRabbit] 中所述,我们可以访问位于 http://localhost:15672 的 Rabbit 管理控制台。继续导航至控制台并登录。登录后,从菜单中选择“队列”。

messageQueues

在这里,您可以看到我们用应用程序创建的两个队列。现在,选择“概述”标签,然后返回到管理主页。查看主页时,请注意队列中有 0 条消息。

emptyQueue

此时,使用 Ctrl-C 从终端停止分析应用程序,然后返回到仍在运行中的应用程序,单击并查看更多书籍。查看更多书籍后,返回到管理主页,您将看到已生成新消息并在队列中等待。

updatedAdminHome

继续从终端重新启动分析应用程序

$ ./gradlew bootRun

分析应用程序重新启动后,您将看到它在处理等待中的消息,当您返回 Rabbit 管理时,您会看到消息队列已返回到 0。

processingPrePostedMessages
rabbitAdminHome

9 后续步骤

要进一步了解,请阅读此处提供的插件文档 here。此外,您还可以随时阅读此处提供的 RabbitMQ 文档 here,并查看此处提供的各种不同语言示例 here

10 您是否需要有关 Grails 的帮助?

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

OCI 是 Grails 的所在地

结识团队