显示导航

将 Grails 应用部署到 Google Cloud

了解如何将 Grails 3 应用程序部署到 Google App Engine Java 可扩展环境,并将其与 Google Cloud Storage 和 Google Cloud SQL 集成

作者:Sergio del Amo、Mathew Moss

Grails 版本 3.3.0

1 Grails 培训

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

2 入门

在本指南中,您将把 Grails 3 应用程序部署到 Google App Engine 可扩展环境,将图片上传到 Google Cloud Storage,并使用 Cloud SQL 提供的 MySQL 数据库。

3 费用

本指南使用付费服务;您可能需要在 Google Cloud 中启用结算才能完成此指南中的某些步骤。

4 你需要的东西

要完成本指南,您将需要以下内容:

  • 一些闲暇时间

  • 一个像样的文本编辑器或 IDE

  • 已安装 JDK 1.7 或更高版本且恰当地配置了 JAVA_HOME

5 如何完成

要开始,请执行以下操作

下载并解压源代码或克隆 Git 代码库

git clone https://github.com/grails-guides/grails-google-cloud.git

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

  • initial 初始项目。一个简单的 Grails 应用,带有额外的代码,让你能够快速上手。

  • complete 已完成的示例。是指南介绍的步骤的执行结果,并将这些更改应用到了初始文件夹。

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

cd initial

并按照下一节中的说明进行操作。

或者,你可以直接转到已完成的示例

cd complete

尽管你可以直接转到已完成的示例,但要部署应用,你需要完成 Google Cloud 中的几个配置步骤

  • 注册 Cloud SDK 并安装 Cloud SDK。

  • 在当前 Google Cloud 项目中初始化 App Engine 应用。

  • 在 Cloud SQL 实例中创建 Mysql 数据库。

  • 为项目启用 Cloud Datastore API

此外,你还需要修改 application.yml 配置,使其指向正确的 Cloud SQL 数据库和 Cloud Storage Bucket。查看指南步骤,了解详情。

6 编写应用

6.1 领域类

我们希望在 Cloud SQL 数据库中保留一些测试数据。初始项目包括一个 Grails 领域类,用于将 Book 实例映射到 MySQL 表。

一个领域类满足了模型视图控制器 (MVC) 模式中的 M,并表示一个映射到底层数据库表的持久实体。在 Grails 中,一个领域是位于 grails-app/domain 目录中的类。

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

import grails.compiler.GrailsCompileStatic

@GrailsCompileStatic
class Book {
    String name
    String featuredImageUrl
    String fileName

    static constraints = {
        name unique: true
        featuredImageUrl nullable: true
        fileName nullable: true
    }
}

6.2 种子数据

当应用启动时,我们将添加一些种子数据。具体来说,我们会保留一本清单。

修改 BootStrap.groovy

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

import groovy.transform.CompileStatic

@CompileStatic
class BootStrap {
    def init = { servletContext ->
        Book.saveAll(
            new Book(name: 'Grails 3: A Practical Guide to Application Development'),
            new Book(name: 'Falando de Grails',),
            new Book(name: 'The Definitive Guide to Grails 2'),
            new Book(name: 'Grails in Action'),
            new Book(name: 'Grails 2: A Quick-Start Guide'),
            new Book(name: 'Programming Grails')
        )
    }
    def destroy = {
    }
}

6.3 显示书本

作为应用的主页,我们希望在应用启动后显示保留的书本;我们在 BootStrap.groovy 中保存的那些

我们修改 UrlMappings.groovy,将主页映射到由 BookController 解析

替换

grails-app/controllers/demo/UrlMappings.groovy

"/"(view:"/index")

使用

grails-app/controllers/demo/UrlMappings.groovy

'/'(controller: 'book')

我们略微修改了 Grails 的静态脚手架命令 generate-all 的输出,以提供领域类 Book 的 CRUD 功能。

你可以在初始项目中找到代码:BookControllerBookGormService 和 GSP 视图。

7 Cloud SDK

注册Google Cloud Platform并创建一个新项目

create project
create project 2

我们为项目命名为 grailsgooglecloud

为你的操作系统安装 Cloud SDK

安装 SDK 后,在你的终端运行 init 命令

$ gcloud init

系统将提示您选择要使用的 Google 帐号和项目。

8 Google App Engine

我们将把本指南中开发的 Grails 应用程序部署到Google App Engine 灵活环境

App Engine 允许开发人员专注于他们最擅长的事情:编写代码。App Engine 灵活环境基于 Google Compute Engine,可在平衡负载的同时自动增减您的应用规模。它原生支持微服务、授权、SQL 和 NoSQL 数据库、流量拆分、日志记录、版本控制、安全扫描和内容分发网络。

运行命令

gcloud app create

在当前 Google Cloud 项目中初始化一个 App Engine 应用程序。

您需要选择要放置 App Engine 应用程序的区域。

8.1 Google App Engine Gradle 插件

要部署到 App Engine,我们将使用Google App Engine Gradle 插件

将该插件添加为 buildscript 依赖项

build.gradle
buildscript {
    repositories {
        mavenLocal()
        maven { url "https://repo.grails.org/grails/core" }
    }
    dependencies {
        classpath "org.grails:grails-gradle-plugin:$grailsVersion"
        classpath "com.bertramlabs.plugins:asset-pipeline-gradle:2.14.2"
        classpath "org.grails.plugins:hibernate5:${gormVersion-".RELEASE"}"
        classpath 'com.google.cloud.tools:appengine-gradle-plugin:1.3.2'
    }
}

应用该插件

build.gradle
apply plugin: 'com.google.cloud.tools.appengine'

8.2 应用程序部署配置

要部署到 Google App Engine,我们需要添加文件 src/main/appengine/app.yaml

它描述了应用程序的部署配置

/src/main/appengine/app.yaml
runtime: java
env: flex

runtime_config:
    jdk: openjdk8
    server: jetty9

health_check:
    enable_health_check: False

resources:
    cpu: 1
    memory_gb: 2.3

manual_scaling:
    instances: 1

这里,app.yaml 指定了应用程序使用的运行时,并设置 env:flex,指定应用程序使用灵活环境

上面显示的最小 app.yaml 应用程序配置文件对于简单的 Grails 应用程序来说已经足够了。根据应用程序使用的大小、复杂性以及功能,您可能需要更改和扩展此基本配置文件。有关可通过 app.yaml 进行配置的详细信息,请参阅使用 app.yaml 配置应用程序指南。

有关 Java 运行时工作原理的详细信息,请参阅Java 8 / Jetty 9.3 运行时

8.3 SpringBoot Jetty

如上一个应用程序引擎配置文件中所示,我们使用 Jetty。

Grails 构建于 SpringBoot 之上。根据 SpringBoot 的文档,我们需要对部署到 Jetty 而不是 Tomcat执行以下更改。

替换

build.gradle
compile "org.springframework.boot:spring-boot-starter-tomcat"

使用

build.gradle
provided "org.springframework.boot:spring-boot-starter-jetty"
重要的是,您将 spring-boot-starter-jetty 依赖项设置为 provided

我们还需要排除一些依赖项

build.gradle
configurations {
    compile.exclude module: "tomcat-juli"
    compile.exclude module: "spring-boot-starter-tomcat"
    compile.exclude group: "com.google.guava", module: "guava-jdk5"
}

9 Cloud SQL

本指南期间开发的 Grails 应用程序将使用通过Cloud SQL创建的 MySQL 数据库

Cloud SQL 是一项完全托管的数据库服务,可以轻松地在云中设置、维护、管理和管理您的关系型 PostgreSQL BETA 和 MySQL 数据库。Cloud SQL 提供高性能、可扩展性和便利性。Cloud SQL 托管在 Google Cloud Platform 上,为在任何位置运行的应用程序提供数据库基础设施。

启用 Cloud SQL API

如果您尚未启用Cloud SQLCloud SQL API,请前往项目信息中心并启用它们。

cloudsql 1
cloudsql 2
cloudsql 3
cloudsql 4
cloudsqlapi 1

9.1 创建 Cloud SQL 实例

我们将创建一个新的 Cloud SQL 实例,并将其与之前创建的同一项目关联起来。

转到信息中心的 Cloud SQL 部分

cloudsql 5

以下屏幕截图展示了这一过程

cloudsql 6
cloudsql 7
cloudsql 8
cloudsql 9

实例准备就绪后,我们创建一个数据库

cloudsql 10
cloudsql 11
cloudsql 12

9.2 使用 Cloud SQL 的数据源

正如使用 Cloud SQL与弹性环境文档中介绍的那样,我们需要添加几个运行时依赖项,并将正式的网址配置为使用我们之前创建的 Cloud SQL MySQL 数据库。

添加 MySQL 依赖项;JDBC 库和 Cloud SQL MySQL 套接字工厂。

build.gradle
    runtime 'mysql:mysql-connector-java:6.0.5'
    runtime 'com.google.cloud.sql:mysql-socket-factory-connector-j-6:1.0.3'

production环境datasource配置替换为指向application.yml中的 Cloud SQL MySQL 数据库

grails-app/conf/application.yml
environments:
    production:
        dataSource:
            dialect: org.hibernate.dialect.MySQL5InnoDBDialect
            driverClassName: com.mysql.cj.jdbc.Driver
            dbCreate: update
            url: jdbc:mysql://google/grailsgooglecloud?socketFactory=com.google.cloud.sql.mysql.SocketFactory&cloudSqlInstance=inner-topic-174815:us-central1:grailsgooglecloud&useSSL=true
            username: root
            password: grailsgooglecloud

production 数据源网址使用的是自定义网址,自定义网址使用多个组件构建而成

jdbc:mysql://google/{DATABASE_NAME}?socketFactory=com.google.cloud.sql.mysql.SocketFactory&cloudSqlInstance={INSTANCE_NAME}&useSSL=true

  • DATABASE_NAME 使用创建数据库时使用的数据库名称。

  • INSTANCE_NAME 可以在 Cloud SQL 实例详情中找到实例名称

cloudsql 13
  • USERNAME / PASSWORD 本指南中,我们使用用户名:root和我们在创建 SQL 实例时输入的密码;请参阅之前的章节。

Cloud SQL Socket Factory for JDBC 驱动程序 Github 存储库包含示例/开始中的工具,可以帮助生成 JDBC URL,并验证能否建立连接。

10 Cloud Storage

我们允许用户上传书籍封面图片。为了将图片存储在 Cloud 中,我们使用Google Cloud Storage

Google Cloud Storage 是为开发者和企业提供的统一的对象存储,从实时数据服务到数据分析/ML 再到数据归档,它都适用。

如果尚未启用,请为项目启用 Cloud Storage API

cloudstorage 1
cloudstorage 2
cloudstorage 3

您可以像以下图片所示的那样创建一个 Cloud Storage 存储分区。我们给存储分区命名为grailsbucket

cloudstorage 4
cloudstorage 5
cloudstorage 6

向你的项目依赖项添加 Cloud Storage 依赖项

build.gradle
    compile 'com.google.cloud:google-cloud-storage:1.2.3'

我还需要排除 com.google.guava:guava-jdk5

build.gradle
configurations {
    compile.exclude module: "tomcat-juli"
    compile.exclude module: "spring-boot-starter-tomcat"
    compile.exclude group: "com.google.guava", module: "guava-jdk5"
}

将以下配置(Cloud Storage 存储分区和项目 ID)参数附加到 application.yml

grails-app/conf/application.yml
googlecloud:
    projectid: grailsguide-176214
    cloudStorage:
        bucket: grailsguidebucket

以下配置参数由以下所述服务使用。

创建一个 Grails 命令对象来管理文件上传参数。

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

import grails.validation.Validateable
import org.springframework.web.multipart.MultipartFile

class FeaturedImageCommand implements Validateable {
    MultipartFile featuredImageFile
    Long id
    Long version

    static constraints = {
        id nullable: false
        version nullable: false
       featuredImageFile  validator: { MultipartFile val, FeaturedImageCommand obj ->
            if ( val == null ) {
                return false
            }
            if ( val.empty ) {
                return false
            }

            ['jpeg', 'jpg', 'png'].any { String extension ->
                 val.originalFilename?.toLowerCase()?.endsWith(extension)
            }
        }
    }
}

将两个控制器操作添加到 BookController

grails-app/controllers/demo/BookController.groovy
UploadBookFeaturedImageService uploadBookFeaturedImageService

@Transactional(readOnly = true)
def editFeaturedImage(Book book) {
     respond book
}
@CompileDynamic
def uploadFeaturedImage(FeaturedImageCommand cmd) {

    if (cmd.hasErrors()) {
         respond(cmd.errors, model: [book: cmd], view: 'editFeaturedImage')
        return
    }

    def book = uploadBookFeaturedImageService.uploadFeaturedImage(cmd)
    if (book == null) {
        notFound()
        return
    }

    if (book.hasErrors()) {
        respond(book.errors, model: [book: book], view: 'editFeaturedImage')
        return
    }

    request.withFormat {
        form multipartForm {
            flash.message = message(code: 'default.updated.message', args: [message(code: 'book.label', default: 'Book'), book.id])
            redirect book
        }
        '*' { respond book, [status: OK] }
    }
}

之前的控制器操作使用服务来管理我们的业务逻辑。创建 `UploadBookFeaturedImageService.groovy`

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

import groovy.util.logging.Slf4j
import groovy.transform.CompileStatic

@Slf4j
@CompileStatic
class UploadBookFeaturedImageService {

    BookGormService bookGormService

    GoogleCloudStorageService googleCloudStorageService

    private static String fileSuffix() {
        new Date().format('-YYYY-MM-dd-HHmmssSSS')
    }

    Book uploadFeaturedImage(FeaturedImageCommand cmd) {
        String fileName = "${cmd.featuredImageFile.originalFilename}${fileSuffix()}"

        log.info "cloud storage file name $fileName"

        String fileUrl = googleCloudStorageService.storeMultipartFile(fileName, cmd.featuredImageFile)

        log.info "cloud storage media url $fileUrl"

        def book = bookGormService.updateFeaturedImageUrl(cmd.id, cmd.version, fileName, fileUrl)
        if ( !book || book.hasErrors() ) {
            googleCloudStorageService.deleteFile(fileName)
        }
        book
    }
}

与 Cloud Storage 交互的代码封装在一个服务中

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

import com.google.cloud.storage.Acl
import com.google.cloud.storage.BlobId
import com.google.cloud.storage.BlobInfo
import com.google.cloud.storage.Storage
import com.google.cloud.storage.StorageOptions
import grails.config.Config
import grails.core.support.GrailsConfigurationAware
import org.springframework.web.multipart.MultipartFile
import groovy.transform.CompileStatic

@SuppressWarnings('GrailsStatelessService')
@CompileStatic
class GoogleCloudStorageService implements GrailsConfigurationAware {

    Storage storage = StorageOptions.defaultInstance.service

    // Google Cloud Platform project ID.
    String projectId

    // Cloud Storage Bucket
    String bucket

    @Override
    void setConfiguration(Config co) {
        projectId = co.getRequiredProperty('googlecloud.projectid', String)
        bucket = co.getProperty('googlecloud.cloudStorage.bucket', String, projectId)
    }

    String storeMultipartFile(String fileName, MultipartFile multipartFile) {
        storeInputStream(fileName, multipartFile.inputStream)
    }

    String storeInputStream(String fileName, InputStream inputStream) {
        BlobInfo blobInfo = storage.create(readableBlobInfo(bucket, fileName), inputStream)
        blobInfo.mediaLink
    }

    String storeBytes(String fileName, byte[] bytes) {
        BlobInfo blobInfo = storage.create(readableBlobInfo(bucket, fileName), bytes)
        blobInfo.mediaLink
    }

    private static BlobInfo readableBlobInfo(String bucket, String fileName) {
        BlobInfo.newBuilder(bucket, fileName)
                // Modify access list to allow all users with link to read file
                .setAcl([Acl.of(Acl.User.ofAllUsers(), Acl.Role.READER)])
                .build()
   }

   boolean deleteFile(String fileName) {
       BlobId blobId = BlobId.of(bucket, fileName)
       storage.delete(blobId)
   }
}

如果将图片上传到 Google Cloud 成功,我们将在域类中保存对媒体 URL 的引用。

BookGormService 类添加此方法

grails-app/services/demo/BookGormService.groovy
@SuppressWarnings('LineLength')
Book updateFeaturedImageUrl(Long id, Long version, String fileName, String featuredImageUrl, boolean flush = false) {
    Book book = Book.get(id)
    if ( !book ) {
        return null
    }
    book.version = version
    book.fileName = fileName
    book.featuredImageUrl = featuredImageUrl
    book.save(flush: flush)
    book
}

我们需要添加一个 GSP 文件来呈现上传表单

grails-app/views/book/editFeaturedImage.gsp
<!DOCTYPE html>
<html>
    <head>
        <meta name="layout" content="main" />
        <g:set var="entityName" value="${message(code: 'book.label', default: 'Book')}" />
        <title><g:message code="default.edit.label" args="[entityName]" /></title>
   </head>
   <body>
       <a href="#edit-book" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content&hellip;"/></a>
       <div class="nav" role="navigation">
       <ul>
           <li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
           <li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
          <li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
      </ul>
     </div>
     <div id="edit-book" class="content scaffold-edit" role="main">
        <h1><g:message code="book.featuredImage.edit.label" default="Edit Featured Image" /></h1>
        <g:if test="${flash.message}">
            <div class="message" role="status">${flash.message}</div>
        </g:if>
       <g:hasErrors bean="${this.book}">
           <ul class="errors" role="alert">
               <g:eachError bean="${this.book}" var="error">
                   <li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
              </g:eachError>
          </ul>
       </g:hasErrors>
       <g:uploadForm name="uploadFeaturedImage" action="uploadFeaturedImage">
           <g:hiddenField name="id" value="${this.book?.id}" />
           <g:hiddenField name="version" value="${this.book?.version}" />
           <input type="file" name="featuredImageFile" />
           <fieldset class="buttons">
              <input class="save" type="submit" value="${message(code: 'book.featuredImage.upload.label', default: 'Upload')}" />
          </fieldset>
      </g:uploadForm>
    </div>
</body>
</html>

11 部署应用

要将应用部署到 Google App Engine,请运行

$ ./gradlew appengineDeploy

初始部署可能需要一段时间。完成后,您将能够访问您的应用

welcometograils

如果您转到 App Engine 管理面板中的版本部分,您将看到已部署的应用。

12 日志记录

对于您想要检查的版本,请在诊断下拉菜单中选择日志

logs

写入 stdout 和 stderr 的应用程序日志消息会自动收集,并且可以在 Log Viewer 中查看。

我们将创建一个控制器并使用 INFO 级别记录,并验证日志记录语句是否在 Log Viewer 中可见。

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

import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j

@CompileStatic
@Slf4j
class LegalController {
    def index() {
        log.info 'inside legal controller'
        render 'Legal Terms'
    }
}

我们在 grails-app/conf/logback.groovy 中添加下一行

logger 'demo', INFO, ['STDOUT'], false

将包 demo 下类的 INFO 语句记录到 STDOUT 追加程序,附加性为 false

如果您将应用重新部署到 App Engine 并访问 /legal 端点,您将在 Log Viewer 中看到日志记录语句。

查看 编写应用程序日志 文档以进一步了解 Flexible Environment 中的日志。

使用 stdout(用于输出)和 stderr(用于错误)编写您的应用程序日志。请注意,这不会提供您可以在 Log Viewer 中用于筛选的日志级别;然而,Log Viewer 确实提供了其他筛选功能,例如文本、时间戳等。

13 清理工作

完成本指南后,您可以清理在 Google Cloud Platform 上创建的资源,这样将来就不会为它们付费。以下部分介绍如何删除或关闭这些资源。

删除项目

消除计费最简单的方法是删除您为本教程创建的项目。

要删除项目

删除项目会产生以下后果
  • 如果您使用现有项目,您还将删除您在该项目中所做的任何其他工作。

  • 您不能重新使用已删除项目的项目 ID。如果您创建了您计划将来使用的自定义项目 ID,那么您应该删除项目中的资源。这样可确保使用项目 ID 的 URL(例如 appspot.com URL)仍然可用。

  • 如果您正在探索多个教程和快速入门,那么重复使用项目(而不是删除它们)可以防止您超过项目配额限制。

在 Cloud Platform 控制台中,转到项目页面。

在项目列表中,选择要删除的项目并点击删除项目。选中项目名称旁边的复选框后,点击删除项目

在对话框中,输入项目 ID,然后点击关闭以删除项目。

删除或关闭特定资源

您可以逐一删除或关闭您在教程期间创建的部分资源。

删除应用版本

要删除应用版本

在 Cloud Platform 控制台中,转至 App Engine 版本页面。

点击要删除的非默认应用版本旁边的复选框。

删除 App Engine 应用的默认版本只能通过删除您的项目来实现。但是,您可以在 Cloud Platform 控制台中停止默认版本。此操作会关闭与版本关联的所有实例。您可以根据需要稍后重新启动这些实例。

在 App Engine 标准环境中,只有当您的应用采用手动或基本扩缩时,您才能停止默认版本。

点击页面顶部的删除按钮以删除应用版本。

删除 Cloud SQL 实例

要删除 Cloud SQL 实例

在 Cloud Platform 控制台中,转至 SQL 实例页面。

点击要删除的 SQL 实例的名称。

点击页面顶部的删除按钮以删除实例。

删除 Cloud Storage 存储分区

要删除 Cloud Storage 存储分区

在 Cloud Platform 控制台中,转至 Cloud Storage 浏览器。

点击要删除的存储分区旁边的复选框。

点击页面顶部的删除按钮以删除存储分区。

14 了解更多

如果您想了解有关 Google Cloud 和 Grails 集成的更多信息,请签出更完整的示例应用。

使用 Grails 的 Google Cloud 书架 应用程序展示了如何使用各种 Google Cloud Platform 产品,包括本指南中描述的部分服务和其他服务,例如

15 Grails 帮助

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

OCI 正是 Grails 的家

认识团队成员