将 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 目录中的类。
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。
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 功能。
你可以在初始项目中找到代码:BookController
、BookGormService
和 GSP 视图。
7 Cloud SDK
注册Google Cloud Platform并创建一个新项目
![create project](../img/create-project.png)
![create project 2](../img/create-project-2.png)
我们为项目命名为 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
依赖项
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'
}
}
应用该插件
apply plugin: 'com.google.cloud.tools.appengine'
8.2 应用程序部署配置
要部署到 Google App Engine,我们需要添加文件 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执行以下更改。
替换
compile "org.springframework.boot:spring-boot-starter-tomcat"
使用
provided "org.springframework.boot:spring-boot-starter-jetty"
重要的是,您将 spring-boot-starter-jetty 依赖项设置为 provided 。 |
我们还需要排除一些依赖项
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 SQL和Cloud SQL API,请前往项目信息中心并启用它们。
![cloudsql 1](../img/cloudsql-1.png)
![cloudsql 2](../img/cloudsql-2.png)
![cloudsql 3](../img/cloudsql-3.png)
![cloudsql 4](../img/cloudsql-4.png)
![cloudsqlapi 1](../img/cloudsqlapi-1.png)
9.1 创建 Cloud SQL 实例
我们将创建一个新的 Cloud SQL 实例,并将其与之前创建的同一项目关联起来。
转到信息中心的 Cloud SQL 部分
![cloudsql 5](../img/cloudsql-5.png)
以下屏幕截图展示了这一过程
![cloudsql 6](../img/cloudsql-6.png)
![cloudsql 7](../img/cloudsql-7.png)
![cloudsql 8](../img/cloudsql-8.png)
![cloudsql 9](../img/cloudsql-9.png)
实例准备就绪后,我们创建一个数据库
![cloudsql 10](../img/cloudsql-10.png)
![cloudsql 11](../img/cloudsql-11.png)
![cloudsql 12](../img/cloudsql-12.png)
9.2 使用 Cloud SQL 的数据源
正如使用 Cloud SQL与弹性环境文档中介绍的那样,我们需要添加几个运行时依赖项,并将正式的网址配置为使用我们之前创建的 Cloud SQL MySQL 数据库。
添加 MySQL 依赖项;JDBC 库和 Cloud SQL MySQL 套接字工厂。
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 数据库
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](../img/cloudsql-13.png)
-
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](../img/cloudstorage-1.png)
![cloudstorage 2](../img/cloudstorage-2.png)
![cloudstorage 3](../img/cloudstorage-3.png)
您可以像以下图片所示的那样创建一个 Cloud Storage 存储分区。我们给存储分区命名为grailsbucket
![cloudstorage 4](../img/cloudstorage-4.png)
![cloudstorage 5](../img/cloudstorage-5.png)
![cloudstorage 6](../img/cloudstorage-6.png)
向你的项目依赖项添加 Cloud Storage 依赖项
compile 'com.google.cloud:google-cloud-storage:1.2.3'
我还需要排除 com.google.guava:guava-jdk5
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
googlecloud:
projectid: grailsguide-176214
cloudStorage:
bucket: grailsguidebucket
以下配置参数由以下所述服务使用。
创建一个 Grails 命令对象来管理文件上传参数。
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
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`
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 交互的代码封装在一个服务中
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
类添加此方法
@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 文件来呈现上传表单
<!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…"/></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](../img/welcometograils.png)
如果您转到 App Engine 管理面板中的版本部分,您将看到已部署的应用。
12 日志记录
对于您想要检查的版本,请在诊断下拉菜单中选择日志
![logs](../img/logs.png)
写入 stdout 和 stderr 的应用程序日志消息会自动收集,并且可以在 Log Viewer 中查看。
我们将创建一个控制器并使用 INFO 级别记录,并验证日志记录语句是否在 Log Viewer 中可见。
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 Storage 存储分区
要删除 Cloud Storage 存储分区
在 Cloud Platform 控制台中,转至 Cloud Storage 浏览器。
点击要删除的存储分区旁边的复选框。
点击页面顶部的删除按钮以删除存储分区。
14 了解更多
如果您想了解有关 Google Cloud 和 Grails 集成的更多信息,请签出更完整的示例应用。
使用 Grails 的 Google Cloud 书架 应用程序展示了如何使用各种 Google Cloud Platform 产品,包括本指南中描述的部分服务和其他服务,例如
-
Google Cloud Vision API
-
Google Cloud Translation API
-
使用 Google Identity Platform 进行身份验证