翻译小组成员:

如果出现乱码,请使用UTF-8编码浏览!!!

目录

1. 入门
2. 使用入门
2.1 下载与安装
2.2 升级Grails 1.0.x
2.3 创建应用程序
2.4 Hello World 示例
2.5 在IDE中使用设置
2.6 规约胜于配置
2.7 运行应用程序
2.8 测试应用程序
2.9 部署应用程序
2.10 创建Artefacts(工件)
2.10 支持的Java EE容器
2.11 生成应用程序
3. 配置
3.1 基础配置
3.1.1 内置选项
3.1.2 日志
3.2 环境
3.3 数据源
3.3.1 数据源与环境
3.3.2 JNDI 数据源
3.3.3 自动数据源迁移
3.4 外部配置
3.5 版本控制
4. 命令行
4.1 创建Gant 脚本
4.2 重用 Grails 脚本
4.3 脚本Events
4.4 自定义构建
4.5 Ant和Maven
5. 对象关联映射 (GORM)
5.1 快速入门指南
5.1.1 CRUD基础
5.2 GORM中进行Domain建模
5.2.1 GORM中的关联
5.2.1.1 One-to-one
5.2.1.2 One-to-many
5.2.1.3 Many-to-many
5.2.1.4 集合类型基础
5.2.2 GORM中的组合
5.2.3 GORM中的继承
5.2.4 Sets, Lists 和 Maps
5.3 持久化基础
5.3.1 保存与更新
5.3.2 删除对象
5.3.3 级联更新和删除
5.3.4 立即加载和延迟加载
5.3.5 悲观锁和乐观锁
5.4 GORM查询
5.4.1 动态查询器
5.4.2 条件查询
5.4.3 Hibernate查询语言(HQL)
5.5 高级GORM特性
5.5.1 事件和自动实现时间戳
5.5.2 自定义ORM映射
5.5.2.1 表名和列名
5.5.2.2 缓存策略
5.5.2.3 继承策略
5.5.2.4 自定义数据库标识符
5.5.2.5 复合主键
5.5.2.6 数据库索引
5.5.2.7 乐观锁和版本定义
5.5.2.8 立即加载和延迟加载
5.5.2.9 自定义级联行为
5.5.2.10 自定义Hibernate的类型
5.5.3 缺省排序
5.6 事务编程
5.7 GORM与约束
6. Web层
6.1 控制器(Controllers)
6.1.1 理解控制器(Controller)与操作(Action)
6.1.2 控制器(Controller) 与作用域
6.1.3 Models(模型)与Views(视图)
6.1.4 重定向与链接
6.1.5 Controller(控制器)拦截器
6.1.6 数据绑定
6.1.7 XML与JSON响应
6.1.8 文件上传
6.1.9 命令对象
6.1.10 处理重复的表单提交
6.2 Groovy Server Pages
6.2.1 GSP基础
6.2.1.1 变量与作用域
6.2.1.2 逻辑和迭代
6.2.1.3 页面指令
6.2.1.4 表达式
6.2.2 GSP标签
6.2.2.1 变量与作用域
6.2.2.2 逻辑和迭代
6.2.2.3 搜索和过滤
6.2.2.4 链接和资源
6.2.2.5 表单和字段
6.2.2.6 标签作为方法调用
6.2.3 视图(View)与模板(Templates)
6.2.4 使用Sitemesh布局Sitemesh
6.2.5 Sitemesh 内容块
6.3 标签库
6.3.1 变量与作用域
6.3.2 简单标签
6.3.3 逻辑标签
6.3.4 迭代标签
6.3.5 标签命名空间
6.3.6 使用JSP 标签库
6.4 URL映射
6.4.1 映射到控制器(Controller)和操作(Action)
6.4.2 嵌入式变量
6.4.3 映射到视图(Views)
6.4.4 映射到响应代码
6.4.5 映射到HTTP方法
6.4.6 映射通配符
6.4.7 自动重写链接
6.4.8 应用约束
6.5 Web流(Flow)
6.5.1 开始与结束状态
6.5.2 操作(Action)状态和视图(View)状态
6.5.3 流(Flow)执行事件
6.5.4 流(Flow)的作用域
6.5.5 数据绑定和验证
6.5.6 子流程和会话
6.6 过滤器
6.6.1 应用过滤器
6.6.2 过滤器(Filters)类型
6.6.3 变量与作用域
6.7 Ajax
6.7.1 用Prototype实现Ajax
6.7.1.1 远程链接
6.7.1.2 内容更新
6.7.1.3 远程表单提交
6.7.1.4 Ajax事件
6.7.2 用Dojo实现Ajax
6.7.3 用GWT实现Ajax
6.7.4 服务端的Ajax
6.8 内容协商
7. 验证
7.1 声明Constraints(约束)
7.2 验证约束
7.3 客户端验证
7.4 验证与国际化
7.5 验证非Domain 与命令行对象
8. Service 层
8.1 声明式事务处理
8.2 服务作用域
8.3 依赖注入与服务
8.4 在Java中使用服务
9. 测试
9.1 单元测试
9.2 集成测试
9.3 功能测试
10. 国际化
10.1 理解消息绑定
10.2 修改本地化
10.3 读取消息
10.4 脚手架和i18n
11. 安全性
11.1 防止攻击
11.2 编码和解码对象
11.3 认证
11.4 安全插件
11.4.1 Acegi
11.4.2 JSecurity
12. 插件
12.1 创建和安装插件
12.2 插件仓库
12.3 理解插件的结构
12.4 提供基础的工件
12.5 评估规约
12.6 参与构建事件
12.7 运行时配置中的钩子
12.8 运行时添加动态方法
12.9 参与自动重载
12.10 理解插件加载的顺序
13. Web服务
13.1 REST
13.2 SOAP
13.3 RSS 和 Atom
14. Grails和 Spring
14.1 Grails内部实现
14.2 配置其他Bean
14.3 运行时Spring与Beans DSL
14.4 The BeanBuilder DSL Explained
14.5 属性占位符配置
14.6 属性重载
15. Grails 与 Hibernate
15.1 通过Hibernate注解映射
15.2 进一步阅读
16. 脚手架
17. 部署

1. 介绍

当今的Java Web开发技术显得过于复杂,相对于它本身的需要来说。现在主流的Java Web框架也是异常复杂,而且没有很好的遵循 Don't Repeat Yourself (DRY) 法则。

因此我们要以一种新的思维方式来重新思考Web开发,Rails、Django和TurboGears这样的动态框架给我们铺平了道路。Grails建立在这些概念之上,它极大地降低了在Java平台上建立Web应用的复杂性。与那些框架不同的是,Grails是构建在现有的像Spring、Hibernate这样的Java技术之上。

Grails是个一栈式开发框架,它尝试通过核心技术和插件技术来解决许多Web开发难题。Grails包含了如下内容:

所有这些都非常易于使用,这得益于 Groovy 语言的强大以及 Domain Specific Languages (DSLs) 的广泛使用。

本文档将带你从Grails入门开始,最终能够使用Grails框架建设Web应用程序。

2. 入门

2.1 下载和安装

让Grails运行起来的第一步是安装发行包。请按照如下步骤:

如果Grails正常工作了那么你可以在终端窗口中键入 grails 命令并看到如下简单的输出:


Welcome to Grails 1.0 - http://grails.org/
Licensed under Apache Standard License 2.0
Grails home is set to: /Developer/grails-1.0
No script name specified. Use 'grails help' for more info

2.2从Grails 1.0.x升级

尽管Grails开发团队试图将从Grails 1.0.x升级到Grails 1.1时带来的影响降到最低限度,但仍然有一些事项需要你认真考虑。重大变化描述如下。

Groovy 1.6

Grails 1.1现在和Groovy 1.6协同工作并且不再支持针对Groovy 1.5的代码编译。如果你有一个使用Groovy 1.5编写的组件库,在将它用于Grails 1.1之前,你需要针对Groovy 1.6来重新编译它。

Java 5.0

Grails 1.1现在不再支持JDK 1.4,如果你希望正常使用Grails,那么建议你继续使用Grails 1.0.x系列直到你能够升级你的JDK。

配置的变化

1) 为了系统的一致性,设置项 grails.testing.reports.destDir 已经被重命名为 grails.project.test.reports.dir

2) 下列设置已经从 grails-app/conf/Config.groovy 文件中移到了 grails-app/conf/BuildConfig.groovy 文件:

3) 自从Java 5.0成为基线起, grails.war.java5.dependencies 选项已不再被支持(见上文)。

4) jsessionid的使用(现在被认为是有害的)默认是禁用的。如果你的应用程序需要用到jsessionid,你可以重新启用它,在 grails-app/conf/Config.groovy 文件中添加如下设置:

grails.views.enable.jsessionid=true

5) 用来配置Log4j的语法已经改变了。看看用户指南的 日志 一章可以获得更多信息。

插件的变化

Grails 1.1默认将不在你的 PROJECT_HOME/plugins 目录下储存插件。这可能导致你的应用程序出现编辑错误,解决办法是重新安装所有的插件或者在 grails-app/conf/BuildConfig.groovy 文件中设置下列属性:

grails.project.plugins.dir="./plugins"

脚本的变化

1) 如果你先前使用的是Grails 1.0.3或以下的版本,那么下边用于从GRAILS_HOME导入脚本的语法将不再被支持:

Ant.property(environment:"env")
grailsHome = Ant.antProject.properties."env.GRAILS_HOME"

includeTargets << new File ( "${grailsHome}/scripts/Bootstrap.groovy" )

取而代之的是 grailsScript 方法,它能导入一个命名的脚本:

includeTargets << grailsScript( "Bootstrap.groovy" )

2) 由于升级到了Gant,所有对变量 Ant 的引用应该改为 ant

3) 项目的根目录不再存于classpath中,像如下的资源加载方式将无法使用:

def stream = getClass().classLoader.getResourceAsStream("grails-app/conf/my-config.xml")

但是你可以使用Java文件API以及 basedir 属性来完成如上操作:

new File("${basedir}/grails-app/conf/my-config.xml").withInputStream { stream -> 
      // read the file  
}

命令行的变化

run-app-httpsrun-war-https 这两个命令已经被取消了,取而代之的是 run-app 命令带上特定的参数:

grails run-app -https

数据映射的变化

1) 枚举类型现在可以使用它们的String值来映射,而不再是ordinal值。当然你也可以通过如下方式改变你的映射来还原为旧的习惯:

static mapping = {
      someEnum enumType:"ordinal"
}

2) 双向的一对一关联现在可以使用在所有者端的一个单列和一个外键引用来映射。你不需要做任何修改,除非你想删除在相反端包含了重复数据的那一列。

REST支持

接收到的XML请求现在不能被自动解析了。要启用对REST请求的解析,你需要在URL映射中使用 parseRequest 变量:

"/book"(controller:"book",parseRequest:true)

其次,你也可以使用新的 resource 变量来开启默认解析:

"/book"(resource:"book")

2.3 创建一个应用程序

要创建一个Grails应用程序你首先需要熟悉 grails 命令的使用,使用方式如下:

grails [命令名称]

假如你需要执行的命令是 create-app


grails create-app helloworld

这将创建一个新的目录,其中包含了helloworld这个项目。你现在可以在终端里导航到这个目录:


cd helloworld

2.4 一个Hello World例子

要实现经典的"hello world!"例子你可以运行 create-controller 命令:


grails create-controller hello  

这将在 grails-app/controllers 目录中创建一个名为 HelloController.groovy 的控制器(参见控制器一章获得更多内容)。

控制器能用来处理web请求并用来实现“hello world!”的例子,我们的实现代码如下:

class HelloController {
        def world = {
                render "Hello World!"
        }
}

完工。现在使用另一个称为run-app的新命令来启动容器:


grails run-app

这将在8080端口开启一个服务器,现在可以通过http://localhost:8080/helloworld这个URL来访问你的应用程序了。

你将看到如下截图所示的内容:

这是由 web-app/index.gsp 文件所呈现的Grails介绍页面。你会注意到它已经发现了你的控制器的存在,点击链接来访问控制器,我们可以看到浏览器窗口中打印除了“Hello World!”的文本。

2.5 设置IDE

IntelliJ IDEA

目前用于Groovy和Grails开发的IDE中,最成熟、最全面的是IntelliJ IDEA 7.0和它的JetGroovy插件。在大型项目中,Grails团队优先推荐使用IDEA。

TextMate

由于Grails关注的是简洁性,所以我们可以使用一些更简单的编辑器,例如在Mac环境下的TextMate,它对Groovy/Grails有着优秀的支持,可以从Texmate bundles SVN获得它。

Eclipse

对于EclipseGroovy Eclipse 插件提供了语法高亮和代码自动完成等功能。

在Grails的Wiki上有更多关于Groovy Eclipse插件的详细讨论

Grails为你自动创建了用于Eclipse的.project 以及 classpath 文件,所以要在Eclipse中导入一个Grails项目,只需在“Package Explorer”中点右键并选择“Import”,随后选择“Existing project into Workspace”并“Browse”你的项目位置。

接着顺序点击“Ok”和“Finish”即可完成项目的导入和安装。

Grails也将自动安装一个项目对应的“Run Configuration”配置,随后可以在Eclipse的“Run”菜单中来使用它运行Grails。

2.6 约定优于配置

Grails使用“约定优于配置”原则来配置自己。这通常意味着文件的名称和位置被用来替代明确的配置,因此你需要熟悉Grails提供的目录结构。

以下是大致目录结构并链接到相关的章节:

2.7 运行应用程序

Grails应用程序可以使用 run-app 命令来运行在内置的Jetty服务器上,这个命令将默认在8080端口上启动一个服务器:

grails run-app

当然你也可以使用 server.port 变量来指定其他端口:

grails -Dserver.port=8090 run-app

更多关于 run-app 命令的信息可以在参考指南中找到。

2.8 测试应用程序

Grails 中的 create-* 系列命令可以为你在 test/integration 目录内创建集成测试代码框架。当然你还得自己来填写有效的逻辑测试代码,更多的信息可以参考 测试 一章。如果你要执行测试代码,那么可以运行 test-app 命令:

grails test-app

Grails也自动生成了用于Ant的 build.xml 文件,它可以委托Grails的 test-app 命令来运行测试代码:

ant test

当你使用如CruiseControl这样的持续集成平台来自动构建Grails应用程序时,使用Ant的方式将非常有用。

2.9 部署应用程序

Grails应用程序是通过Web应用程序档(WAR文件)的格式来部署的,它使用 war 命令来执行这个部署任务:

grails war

这将在你的项目根目录中产生一个WAR文件,你可以参照你的容器指南来部署它。

绝对不要使用 run-app 命令来作为部署的命令,因为它使Grails能够在运行期间自动重载,但这将带来许多性能和扩展性问题。

部署好Grails之后,你应该为你的容器JVM设置 -server 选项来分配足够的内容。一个较好的VM设置应该像这样:

-server -Xmx512M

2.10 创建工件

Grails提供了许多像 create-controllercreate-domain-class 这样方便的命令,可以使用它们来为你创建 控制器 等各种类型的工件。
这只是为了方便你进行开发,你也可以随意使用喜欢的IDE或文本编辑器来完成同样的工作。
例如,一个应用程序的基础部分是 域模型 ,我们可以像这样创建它:

grails create-domain-class book

这将在 grails-app/domain/Book.groovy 文件中创建一个域类,内容如下:

class Book {  
}

还有许多类似 create-* 这样的命令,你可以在命令行参考指南中了解它们

2.10 支持的 Java EE 容器

Grails支持相当广泛的容器,如下:

一些容器还有不少Bug,但在多数情况下它们都能工作的很好。在Grails的wiki站点你能找到一份 开发中已知问题列表

2.11 生成一个应用

要使用Grails快速开始,经常用到的一个特性叫做 脚手架 ,它可以用来生成一个应用的骨架。要开始这样做,你可以使用 generate-* 这样的命令,如 generate-all 可以用来生成一个 控制器 以及相关的 视图

grails generate-all Book

3. 配置

也许在这里谈论配置对于一个“约定优于配置”的框架来说,这可能比较奇怪,但这些配置通常都是一次性,我们最好还是先了解他们的大概。

由于Grails提供了默认设置,你确实可以在不做任何配置的情况下进行开发和应用。Grails也内嵌了一个Web容器和一个称为HSQLDB的内存数据库,这意味着你甚至都不用安装数据库了。

不过,在将来某些情况下你还是会想要安装一个真正的数据库的,我们将在随后的一些章节进行描述。

3.1 基本配置

Grails提供了一个名为 grails-app/conf/Config.groovy 的文件用来进行一般性配置。这个文件使用了Groovy的 ConfigSlurper 特性,除了它是由纯正的Groovy实现外,它与Java的properties文件是非常相似的,因此你可以在应用中重用定义的变量或者使用适合的Java类型!

你可以在这里添加你自己的配置,例如:

foo.bar.hello = "world"

配置完成后你就可以在你的应用程序里使用两种方式来访问这些设置了。最常用是通过 GrailsApplication 对象,它可以在控制器或标记库中作为一个变量来使用:

assert "world" == grailsApplication.config.foo.bar.hello

另一种方式是先获得对 ConfigurationHolder 类的引用,然后再通过它获得配置对象的引用:

import org.codehaus.groovy.grails.commons.*
…
def config = ConfigurationHolder.config
assert "world" == config.foo.bar.hello

3.1.1 内置选项

Grails提供了下列配置选项:

生成War文件

要获得使用这些选项的更多信息,可以参考部署一章

3.1.2 日志

日志基础

Grails使用它的通用配置方式来配置潜在的 Log4j 日志系统。要配置日志你需要修改位于 grails-app/conf 目录下的 Config.groovy 文件。

这个独特的 Config.groovy 文件允许你为 开发(development)测试(test)生产(production)环境(environments)分别进行日志的配置。Grails将适当地处理 Config.groovy 文件并配置Log4j。

从1.1版本的Grails开始,提供了一个 Log4j DSL,你可以像如下例子一样来配置Log4j:

log4j = {
    error  'org.codehaus.groovy.grails.web.servlet',  //  controllers
               'org.codehaus.groovy.grails.web.pages' //  GSP

warn 'org.mortbay.log' }

实际上,每个方法都可以转化为一个日志级别,你可以把你想要记录日志的包名作为方法的参数。

以下是一些有用的日志记录器:

顶级日志记录器

顶级日志记录器会被所有其他日志记录器继承。你可以使用root方法来配置顶级日志记录器:

root {
    error()
    additivity = true
}

下边的例子用来配置顶级日志记录器去记录错误级别的信息,它的上方是默认的标准输出目标。你也可以将顶级日志记录器配置为将日志输出到多个已命名的输出目标:

appenders {
        file name:'file', file:'/var/logs/mylog.log'
}
root {
    debug 'stdout', 'file'
    additivity = true
}

这里的顶级日志记录器将日志记录到了两个输出目标——默认的“stdout”输出目标和一个“file”输出目标。

你也可以通过参数方式进入Lorg4J闭包的方式来配置顶级日志记录器:

log4j = { root ->
    root.level = org.apache.log4j.Level.DEBUG
    …
}
闭包参数“root”是 org.apache.log4j.Logger 的一个实例,因此你可以查阅Log4J的API文档,找出哪些属性和方法对你有用。

自定义输出目标

使用Log4j你可以明确的定义输出目标。下边是默认可用的输出目标:

例如你可以配置一个滚动文件输出目标:

log4j = {
        appenders {
                rollingFile name:"myAppender", maxFileSize:1024, fileName:"/tmp/logs/myApp.log"
        }
}

每个进入输出目标的参数都会对应到 Appender 类的一个属性。上边的例子设置了RollingFileAppender 类的namemaxFileSizefileName属性。

如果你愿意通过自己编程来创建输出目标或者你已经有自己的输出目标实现,那么你可以简单地调用 appender 方法以及输出目标实例:

import org.apache.log4j.*

log4j = { appenders { appender new RollingFileAppender(name:"myAppender", maxFileSize:1024, fileName:"/tmp/logs/myApp.log") } }

现在你可以将输出目标的名称作为一个唯一值设置到某个日志级别方法中,这样日志就记录到一个特定的输出目标中。这些在上一节讲述过:

error myAppender:"org.codehaus.groovy.grails.commons"

自定义布局

Log4j DSL默认假设你想要使用 样板布局(PatternLayout) 日志格式。也有如下其他布局可用使用:

你可以使用layout设置来指定自定义的样板作为一个输出目标:

log4j = {
        appenders {
        console name:'customAppender', layout:pattern(conversionPattern: '%c{2} %m%n')
    }
}

这样的设置也可以用于内置的“stdout”输出目标,这样会将日志输出到控制台中:

log4j = {
    appenders {
        console name:'stdout', layout:pattern(conversionPattern: '%c{2} %m%n')
    }
}

完整的堆栈日志跟踪

当发生异常时,会产生大量来自Java和Groovy内部的堆栈日志信息。Grails过滤了那些典型的无关信息,同时聚焦到非Grails/Groovy核心类的信息上。

当这种情况发生时,完整的追踪信息总是会写到 StackTrace 日志记录器。这些日志被记录到一个称为stacktrace.log的文件中 - 当然你也可以修改 Config.groovy 文件来进行你想要的设置。例如,如果你更喜欢将完整的堆栈记录信息输出到标准输出,可以添加这样一行:

error stdout:"StackTrace"

你也可以将 grails.full.stacktrace 虚拟机属性设置为 true 来完全禁用堆栈跟踪过滤器:

grails -Dgrails.full.stacktrace=true run-app

约定的日志记录方式

所有的应用程序工件都有一个动态添加的 log 属性。这些工件类型包括 domain类控制器和标记库等。下边是一个使用例子:

def foo = "bar"
log.debug "The value of foo is $foo"

Grails使用 grails.app.<工件类型>.ClassName 来作为日志记录器的命名。下边是一个如何配置日志记录器去记录不同Grails工件的日志的例子:

log4j = {
        // 所有的应用程序工件设置
        info "grails.app"
        // 一个特定的控制器设置
        debug "grails.app.controller.YourController"
        // 一个特定的domain类设置
        debug "grails.app.domain.Book"
        // 所有的标记库设置
        info "grails.app.tagLib"

}

工件名称(<工件类型>)也是按照约定命名的,一些常见的如下列表:

3.2 环境

多环境配置

Grails支持“多环境配置”的概念。grails-app/conf中的Config.groovyDataSource.groovy两个文件能够使用ConfigSlurper提供的语法来应用“多环境配置”的特性。以下例子是Grails提供的默认 DataSource 里的定义:

dataSource {
    pooled = false                          
    driverClassName = "org.hsqldb.jdbcDriver"       
    username = "sa"
    password = ""                           
}
environments {
    development {
        dataSource {
            dbCreate = "create-drop" // 可选“create”、“createeate-drop”和“update”中的一个
            url = "jdbc:hsqldb:mem:devDB"
        }
    }   
    test {
        dataSource {
            dbCreate = "update"
            url = "jdbc:hsqldb:mem:testDb"
        }
    }   
    production {
        dataSource {
            dbCreate = "update"
            url = "jdbc:hsqldb:file:prodDb;shutdown=true"
        }
    }
}

注意配置文件的开头部分提供的是公共配置,紧接着的 environments 代码块则指定了用于独立环境配置的数据源信息,包括dbCreateurl属性。这样的语法也可以用与Config.groovy文件。

针对不同环境的打包和运行

Grails的命令行 已经内建了针对特定环境来执行任何命令的能力。格式为:

grails [环境名] [命令名]

另外,已经有三个Grails的预制环境:devprodtest分别用于 开发生产测试。例如要为 test 环境创建一个WAR包,你可以这样做:

grails test war

如果你有自建的其他环境需要使用,可以通过 grails.env 变量来设置并用于任何命令:

grails -Dgrails.env=UAT run-app

可编程的环境检测

在你的Gant脚本或系统启动类的代码中,你可以使用 Environment 类来检测环境:

import grails.util.Environment

...

switch(Environment.current) { case Environment.DEVELOPMENT: configureForDevelopment() break case Environment.PRODUCTION: configureForProduction() break }

3.3 数据源

Grails是基于Java技术构建的,因此要在其中安装数据源必然需要一些JDBC(这种技术并不只支持Java数据库连接)的知识。

根本上来说,如果你正在使用的另一种数据库,而不是Grails内嵌的HSQLDB,那么你就需要为它准备一个JDBC驱动。例如使用MySQL数据库,就需要 Connector/J

这个JDBC驱动。通常这些JDBC驱动都是以JAR文件格式发行的。将需要的JAR文件放到项目的 lib 目录下即可。

一旦你把JAR文件放到了正确的位置,你还需要熟悉位于 grails-app/conf/DataSource.groovy 的Grails数据库描述文件。这个文件包含了数据源的定义,其中有下列这些设置:

一个用于MySQL数据库的典型配置可以像这样:

dataSource {
        pooled = true
        dbCreate = "update"
        url = "jdbc:mysql://localhost/yourDB"
        driverClassName = "com.mysql.jdbc.Driver"
        username = "yourUser"
        password = "yourPassword"   
}

在配置数据源的时候不要在配置项之前包含类型声明或def关键字,否则Groovy会把它们当作本地变量定义并且不对它们进行处理。例如下边的例子就是无效的:

dataSource {
        boolean pooled = true // 类型声明导致它被当作是一个本地变量
        …
}

3.3.1 数据源和环境

前边的配置范例假设你想要对所有的环境做一些配置,包括:生产、测试和开发等。

Grails的数据源定义是“环境感知”的,因此你可以针对需要的环境这样配置:

dataSource {
        // 这里放置公共设置
}                     
environments {
  production {
     dataSource {
          url = "jdbc:mysql://liveip.com/liveDb"                                    
     }                     
  }
}

3.3.2 JNDI数据源

许多Java EE容器通常都支持通过 Java命名与目录接口 (JNDI)来获取 数据源 实例。有时你可能需要通过JNDI去查找一个 数据源

Grails支持像下边这样的JNDI数据源定义:

dataSource {
    jndiName = "java:comp/env/myDataSource"
}

JNDI的名称格式在不同的容器中会有不同,但是在定义 数据源 的方式上是一致的。

3.3.3 自定数据库迁移

DataSourcedbCreate 属性是非常重要的,它会指示Grails在运行期间使用 GORM 类来自动生成数据库表。选项如下:

create-dropcreate 都会删除所有存在的数据,因此请小心使用!

In 部署 模式下 dbCreate 默认被设置为“create-drop”:

dataSource {
        dbCreate = "create-drop" // one of 'create', 'create-drop','update'
}

在每次应用程序重启时都会自动删除并重建数据库表。显然,这不应该用于生产环境。

尽管目前Grails还不支持Rails风格的开箱迁移特性,但有两个插件可以提供Grails类似的简单能力:LiquiBase插件和DbMigrate插件都可以通过grails list-plugins命令获得。

3.4 外部配置

大多数情况下, grails-app/conf 目录下的 Config.groovy 默认配置文件是足够使用了,但可能有某些特殊情况让你想要在主应用程序框架 之外 维护一个配置文件。例如你使用WAR文件部署了系统,管理员会经常需要修改配置文件来改变系统的特性,但又要避免每次修改都得重新打包生成WAR文件。

为了支持这种外部配置文件的部署方案,你需要在Config.groovy文件的grails.config.locations设置中指明你的外部配置文件所在位置:

grails.config.locations = [ "classpath:${appName}-config.properties",
                            "classpath:${appName}-config.groovy",
                            "file:${userHome}/.grails/${appName}-config.properties",
                            "file:${userHome}/.grails/${appName}-config.groovy"]

上边的例子演示了从classpath和USER_HOME这些不同的位置来加载配置文件(包括Java属性(properties)文件和 ConfigSlurper 配置)。

最终所有的配置文件都被合并到了 GrailsApplication 对象的 config 属性中,就可以通过这个属性来获取配置信息了。

Grails也支持Spring中定义的属性占位(property place holder)概念和属性重载(property override)配置,更多信息请查看 Grails和Spring一章。

3.5 版本管理

版本管理基础

Grails已经内置了对版本管理的支持。当首次使用 create-app 命令创建应用程序的时候,应用程序的版本就被设置为 0.1 了。这个版本信息被记录在项目根目录下的应用程序元数据文件 application.properties 里边。

需要改变你的应用程序版本时你可以运行 set-version 命令:

grails set-version 0.2

版本信息被用在各种命令中,例如 war 命令就会将应用程序版本附加到创建的WAR文件末尾。

运行期间检测版本

你可以使用Grails对应用程序元数据的支持来检测应用程序版本,也就是使用 GrailsApplication 类。例如在 控制器 里你可以使用隐藏的 grailsApplication 变量:

def version = grailsApplication.metadata['app.version']

如果你需要获得的不是应用程序的版本而是Grails环境的版本,那么可以这样做:

def grailsVersion = grailsApplication.metadata['app.grails.version']

也可以使用 GrailsUtil 类:

import grails.util.*
def grailsVersion = GrailsUtil.grailsVersion

4. 命令行

Grails的命令行系统是构建于 Gant 之上,Gant就是使用Groovy对 Apache Ant 进行了简单的包装。

然而,Grails通过约定规则以及grails命令的使用带来了一些改进。当你键入如下内容时:

grails [命令名称]
为了Gant脚本的执行,Grails会在下列目录中做一次搜索:

Grails将把小写的命令名称(如run-app)转换为单词连写的格式。因此如果键入的是

grails run-app

,那么Grails将会搜索下列文件:

如果找到多个同名的文件,Grails将要求你选择执行其中的一个。当Grails执行一个Gant脚本的时候,它会首先调用定义在脚本文件中的“default”任务。如果找不到“default”任务,Grails将退出并报错。

获得可用的命令及其帮助信息:

grails help

这个命令将输出Grails当前所知的命令列表和使用说明:

Usage (optionals marked with *): 
grails [environment]* [target] [arguments]*

Examples: grails dev run-app grails create-app books

Available Targets (type grails help 'target-name' for more info): grails bootstrap grails bug-report grails clean grails compile ...

参考本使用指南左侧菜单中的命令行指南,可以获得更多的命令行的信息。

4.1 创建Gant脚本

你可以在项目的根目录下运行 create-script 命令来创建你自己的Gant脚本。例如如下命令:

grails create-script compile-sources

这将创建一个叫做 scripts/CompileSources.groovy 的脚本。Gant脚本本身与规范的Groovy脚本非常相似,除了它支持“targets”的概念以及它们之间的依赖关系:

target(default:"default任务是由Grails来执行的") {
        depends(clean, compile)
}
target(clean:"清除一些东西") {
        ant.delete(dir:"output")
}
target(compile:"编译一些源码") {
        ant.mkdir(dir:"mkdir")
        ant.javac(srcdir:"src/java", destdir:"output")
}

如上面的脚本所说明的,这个内置的 ant 变量可以访问 Apache Ant API

在以前的Grails中(1.0.3和以下),这个变量是 Ant,即第一个字母是大写的。

你也可以依赖其他的任务,只要在 default 任务中使用 depends 方法说明。

默认任务(default)

在上边的例子中,我们使用明确的名称“default”来指明一个任务。这是为一个脚本文件定义默认任务的一种方式。可选的另一种方式是使用 setDefaultTarget() 方法:

target("clean-compile": "对应用程序源文件执行清理并编译。") {
        depends(clean, compile)
}
target(clean:"清除文件") {
        ant.delete(dir:"output")
}
target(compile:"编译源码") {
        ant.mkdir(dir:"mkdir")
        ant.javac(srcdir:"src/java", destdir:"output")
}

setDefaultTarget("clean-compile")

这样将允许你从其他脚本中直接调用默认的任务。另外,尽管在这个例子中我们把调用 setDefaultTarget() 这一行放在了脚本文件的最后,但你可以把它放在任何位置,只要它位于它要引用的那个任务 之后(在这个例子中这个任务就是“clean-compile”)。

哪种方式更好?坦率地说,你可以使用你喜欢的那种方式——看起来这两种方式都没有什么突出的优势。我们应该讨论的一个问题是,如果你想要允许任何其他脚本都能调用你的“default”任务,那么你应该把它移动到一个没有默认任务的共享脚本文件中。关于这些内容,我们将在下一章节进行更多讨论。

4.2 重用Grails脚本

Grails带了许多开箱即用的命令行功能,你会发现这在你自己的脚本中那个会很有用(查看参考指南的命令行指南部分可以获得所有命令的详细信息)。尤其是使用 compilepackagebootstrap 脚本。

下边的bootstrap脚本例子允许你启动一个Spring的 ApplicationContext 实例,通过它来访问数据源等(集成测试时可以这样用):

includeTargets << grailsScript("_GrailsBootstrap")

target ('default': "Load the Grails interactive shell") { depends( configureProxy, packageApp, classpath, loadApp, configureApp )

Connection c try { // 使用连接一些事情 c = appCtx.getBean('dataSource').getConnection() } finally { c?.close() } }

从其他脚本文件引入任务

Gant允许你从另一个Gant脚本文件中引入所有任务(除了“default”)。然后你就可以依赖或调用这些已经被定义在当前脚本文件中的任务了。实现的途径是 includeTargets 属性。使用左移操作符来简单的“附加”一个文件或类:

includeTargets << new File("/path/to/my/script.groovy")
includeTargets << gant.tools.Ivy
不用太担心关于使用一个类的语法,它是相当专业的。要是你感兴趣,可以看看Gant的文档。

核心的Grails任务

如你在本章开头部分所看到的例子,当使用includeTargets来包含核心的Grails任务时,既没有使用基于文件的语法也没有使用基于类的语法。取而代之的,你应该使用Grails命令启动器提供的特殊的 grailsScript() 方法(注意这个方法在一般的Gant脚本中是不可用的,只有在Grails环境中才行)。

grailsScript() 方法的语法是非常简单易读的:简单的把你想要包含的Grails脚本文件的名称传入,不需要任何路径信息。以下是一个你可能想要重用的Grails脚本列表:
脚本 描述
_GrailsSettings 你确实应该包括这个!幸运的是,它已经被所有其他Grails脚本文件自动包括了(_GrailsProxy),因此你通常不必明确的包括它。
_GrailsEvents 如果你想要触发事件,你应该包括这个。添加一个 event(String eventName, List args) 方法。另外,这也被几乎所有其他Grails脚本文件包括。
_GrailsClasspath 安装编译、测试和运行用的classpath。如果你想使用它们,就包含这个脚本。另外,这也由几乎所有其他Grails脚本包含。
_GrailsProxy 如果你需要访问互联网,为了避免遇到代理引起的问题请包含这个脚本。
_GrailsArgParsing 提供一个 parseArguments 任务,就像字面上的意思:当运行你的脚本的时候解析用户提供的参数。把参数添加到 argsMap 属性中。
_GrailsTest 包含所有共享的测试代码。如果你要添加额外的测试这将非常有用。
_GrailsRun 为你提供在配置好的servlet容器中运行应用程序时需要的一切,可以是正常的运行(runApp/runAppHttps),也可以是来自于一个WAR文件(runWar/runWarHttps)。

这些由Grails提供的脚本很值得对它们进行深入的分析,从而找出哪些类型的任务是可以使用的。任何脚本文件都是以“_”作为前缀以便进行重用。

在Grails 1.1版本之前,“_Grails...”这样的脚本文件是不可用的。而通常会包含对应命令脚本,例如“Init.groovy”或“Bootstrap.groovy”。

同样,在Grails 1.0.4版本之前,是无法使用 grailsScript() 方法的,你只能使用 includeTargets << new File(...) 并指明脚本的完整位置。(例如: $GRAILS_HOME/scripts)。

脚本结构

你可能对这些下划线词语作为Grails脚本的名称感到疑惑。用_internal_作为一个脚本或者用没有对应的“command”的其他单词,这些就是Grails的决定方式。因此无法运行例如“grails _grails-settings”这样的命令。这也就是为什么它们没有个默认的任务。

内部脚本是和代码共享重用相关的。实际上,我们建议在自己的脚本中使用类似的方式:把你的所有任务放入一个内部脚本中可以更容易的共享,然后提供简单的命令脚本来解析任何命令行参数并委托给内部脚本中的任务。假如你有一个脚本要运行一些功能测试——你可以将它们像这样分离:

./scripts/FunctionalTests.groovy:

includeTargets << new File("${basedir}/scripts/_FunctionalTests.groovy")

target(default: " 这个 项目运行功能测试。") { depends(runFunctionalTests) }

./scripts/_FunctionalTests.groovy:

includeTargets << grailsScript("_GrailsTest")

target(runFunctionalTests: "运行功能测试。") { depends(...) … }

以下是在编写脚本时常用的一些指导方案:

4.3 钩子事件

Grails提供了钩住脚本事件的能力。这里指的是当Grails的任务和插件脚本执行的时候能触发的一些事件。

这个机制是故意简单化和松散的规定的。可能的事件列表是不会以任何方式固定的,所以可以钩住那些被插件脚本触发的事件,在核心目标脚本中没有类似的事件。

定义事件处理器

事件处理器是定义在称为 _Events.groovy 的脚本文件中。Grails会在以下位置搜索这些脚本:

无论事件在何时被激发, 所有 已经注册到该事件的处理器都会被执行。需要注意的是处理器的注册工作会由Grails自动进行,你只需要在相关的 _Events.groovy 文件中声明即可。

在Grails 1.0.4版本之前,脚本文件被命名为 Events.groovy,它没有前下划线。

事件处理器是分块定义在 _Events.groovy 文件中,使用“event”作为名称的开头部分。下边的例子可以被放在你的 /scripts 目录中来展示这个特性:

eventCreatedArtefact = { type, name ->
   println "Created $type $name"
}

eventStatusUpdate = { msg -> println msg }

eventStatusFinal = { msg -> println msg }

你可以看到这儿有三个处理器分别是:eventCreatedArtefacteventStatusUpdateeventStatusFinal。Grails提供了一些标准的事件,它们在命令行参考指南中有描述。例如compile命令会激发下列事件:

触发事件

要简单地触发一个包含Init.groovy脚本的事件并调用event()闭包:

includeTargets << grailsScript("_GrailsEvents")

event("StatusFinal", ["Super duper plugin action complete!"])

公共事件

下表是一些可以被利用的公共事件:

事件 参数 描述
StatusUpdate message 传入一个标志当前脚本状态或进展的字符串
StatusError message 传入一个标志来自当前脚本的错误信息的字符串
StatusFinal message 传入一个标志最终脚本状态消息的字符串,例如:当编译一个任务时,即使任务还没有退出脚本环境
CreatedArtefact artefactType,artefactName 当一个 create-xxxx 脚本已执行完成并创建了一个工件时调用
CreatedFile fileName 当一个项目的源码文件被创建时调用,但不包括那些由Grails管理的固定文件
Exiting returnCode 当脚本环境即将正常的退出时调用
PluginInstalled pluginName 在一个插件被安装之后调用
CompileStart kind 当编译过程开始时调用,针对这几种类型的编译——源文件和测试文件
CompileEnd kind 当编译过程完成时调用,针对这几种类型的编译——源文件和测试文件
DocStart kind 当生成文档过程即将开始时调用——生成javadoc或groovydoc时
DocEnd kind 当生成文档过程已经结束时调用——生成javadoc或groovydoc时
SetClasspath rootLoader 在classpath初始化时调用以便插件可以通过 rootLoader.addURL(...)来扩大classpath。注意这种扩大classpath是在事件脚本被加载 之后进行的,因此你不能使用这种方式来加载你的事件脚本需要导入的类,即使你可以通过名称来加载类。
PackagingEnd none 当打包结束时调用(这个调用是在Jetty服务器被启动之前并在web.xml文件被生成之后)
ConfigureJetty Jetty Server object 在Jetty web服务器的配置被初始化之后调用。

4.4 自定义构建

Grails无疑是一个固执己见框架,并且它喜欢按照约定来进行配置,但这并不意味着你 不能 去配置它。在本章,我们将看到你可以如何去影响和修改标准的Grails构建。

默认

为了自定义一个构建,你首先需要知道你可以自定义些什么。Grails构建配置的核心就是 grails.util.BuildSettings 类,它包含了大量有用的信息。它控制了哪些类被编译、应用程序依赖什么以及其他类似的设置。

以下是一个配置选项和它们的默认值的集录:
属性 配置选项 默认值
grailsWorkDir grails.work.dir $USER_HOME/.grails/<grailsVersion>
projectWorkDir grails.project.work.dir <grailsWorkDir>/projects/<baseDirName>
classesDir grails.project.class.dir <projectWorkDir>/classes
testClassesDir grails.project.test.class.dir <projectWorkDir>/test-classes
testReportsDir grails.project.test.reports.dir <projectWorkDir>/test/reports
resourcesDir grails.project.resource.dir <projectWorkDir>/resources
projectPluginsDir grails.plugins.dir <projectWorkDir>/plugins
globalPluginsDir grails.global.plugins.dir <grailsWorkDir>/global-plugins

BuildSettings 类也有一些其他属性,但是它们应该被只读处理:
属性 描述
baseDir 项目的位置。
userHome 用户的主目录。
grailsHome 正在使用的Grails的安装位置(也许为null)。
grailsVersion 被项目使用的Grails的版本。
grailsEnv 当前的Grails环境。
compileDependencies 编译时项目依赖的 文件 实例列表。
testDependencies 测试时项目依赖的 文件 实例列表。
runtimeDependencies 运行时项目依赖的 文件 实例列表。

当然,如果你不能获得这些属性那么它们并没有多好。幸运的是这很容易实现:通过grailsSettings脚本变量可以得到一个 BuildSettings 实例用于你的脚本。你也可以在你的代码中通过使用 grails.util.BuildSettingsHolder 类来访问它,但是并不推荐这样做。

覆盖默认值

所有在第一个表中的属性都可以被一个系统属性或配置选项所覆盖——简单地使用“config option”名称。例如,要改变项目工作目录,你可以运行这个命令:

grails -Dgrails.project.work.dir=work compile
或者将这个选项添加到你的 grails-app/conf/BuildConfig.groovy 文件中:
grails.project.work.dir = "work"
注意默认值带有许多它们依赖的属性值,因此像这样设置项目工作目录也将迁移编译好的类、测试类、资源和插件。

如果你同时使用系统属性和配置选项将发生什么?当然是系统属性被采用了,因为它优先于 BuildConfig.groovy 文件,而后者优先于默认值。

BuildConfig.groovy 文件是 grails-app/conf/Config.groovy 的姐妹文件,——过去包含的选项仅仅影响构建,但是之后包含的就影响正在运行的应用程序了。这并不局限于第一个表中的选项:你会发现构建配置选项在文档中到处都是,比如其中一些就用来指定内嵌的servlet容器应该运行在哪个端口上或者决定哪些文件应该被打包到WAR文件中。

可用的构建设置

名称 描述
grails.server.port.http 指定内嵌的servlet容器应该运行的端口(“run-app”和“run-war”命令使用)。整型。
grails.server.port.https 指定内嵌的servlet容器用于HTTPS的运行端口(“run-app https”和“run-war https”)。整型。
grails.config.base.webXml 指定用于应用程序的自定义web.xml文件的路径(取代使用web.xml模板)。
grails.compiler.dependencies 将额外的依赖添加到编译器classpath的传统方式。设置它到一个包含“fileset()”入口的闭包。
grails.testing.patterns 一个Ant路径格式的列表,允许你控制哪些文件可以被包含在测试中。这些格式不应该包括测试用例后缀,它们将在下一个属性中设置。
grails.testing.nameSuffix 默认的,测试类都假定有一个“Tests”的后缀。你可以设置这个选项来改变它为你想要的任何内容。例如:另一个公共后缀是“Test”。
grails.war.destFile 一个包含了生成的WAR文件的文件路径的字符串,除了它的全名意外(包括扩展名)。例如,“target/my-app.war”。
grails.war.dependencies 一个包含“fileset()”入口的闭包,它允许你完全控制什么内容可以被放入WAR文件的“WEB-INF/lib”目录中。
grails.war.copyToWebApp 一个包含“fileset()”入口的闭包,它允许你完全控制什么内容可以被放入WAR文件的根目录中。它覆盖了包含“web-app”目录下所有内容的那种默认习惯。
grails.war.resources 一个闭包,它的第一个参数作为分段目录的位置。你可以使用任何Ant任务来做你想做的任何事。通常这用来在目录被打包成WAR之前从分段目录中删除文件。

4.5 Ant和Maven

如果你的团队或公司的所有其他项目都在使用像Ant或Maven这样的标准的构建工具进行构建的,当你使用Grails命令行来构建你的应用程序时你可能成为害群之马。幸运的是,今天你可以很容易的将Grails构建系统集成到正在使用的主要构建工具中(嗯,至少是在Java项目中使用的那种构建工具)。

Ant集成

当你通过 create-app 命令来创建一个Grails应用程序时,Grails会自动为你创建一个 Apache Ant 工具使用的build.xml文件,这个文件包含了下列的任务:

这些任务都可以被Ant运行,例如:

ant war

为了实现依赖管理,构建文件已经被全面改进为使用 Apache Ivy,这意味着它可以自动下载所有需要的Grails JAR文件和其他以来的文件。你甚至不必在本地安装Grails就可以使用它了!这对于需要使用像CruiseControlHudson这样的持续集成系统进行自动构建时特别有用。

这里使用了Grails的Ant task来对现有的Grails构建系统进行钩子操作。这个任务允许你运行任何可用的Grails脚本,不只是由生成的构建文件所使用的那些。要使用某个任务,你必须先声明它:

<taskdef name="grailsTask"
         classname="grails.ant.GrailsTask"
         classpathref="grails.classpath"/>

这也引出了另外的问题:“grails.classpath”中应该是什么内容?这个任务本身是在“grails-bootstrap”这个JAR工件中的,因此至少这个工件需要在classpath中。同时也应该包含“groovy-all”这个JAR。对于定义这个任务,你只需要使用这个!下表列出了可用的属性:
属性 描述 是否必填
home 构建时需要用到的Grails安装目录的位置。 除非classpath被指定否则必填。
classpathref 载入Grails的Classpath。必须包含“grails-bootstrap”工件并且应该包含“grails-scripts”。 除非home被设置或者你使用classpath元素否则必填。
script 要运行的Grails脚本的名称,例如:“TestApp”。 必填。
args 要加入脚本中的参数,例如:“-unit -xml”。 不是必填。默认为“”。
environment 运行脚本时的Grails环境。 不是必填。默认为脚本的default。
includeRuntimeClasspath 高级设置:如果设为true则将应用程序的运行时classpath添加到构建classpath中。 不是必填。默认为true。

这个任务也支持下列内嵌元素,这些全都是标准的Ant路径结构:

要如何填写这些路径信息完全取决于你。如果你正在使用 home 属性并且把你自己的依赖内容放在了 lib 目录中,那么你不需要使用以上任何一个路径。如果想看看使用它们的例子,那么就查看为一个新应用而生成的Ant构建文件吧。

Maven集成

从1.1版本起,Grails通过一个Maven插件提供了与 Maven 2 的集成。当前作为基础的Maven插件,特别是由 Octo 创建的这个版本是非常有效的,它做得非常出色。

准备

为了使用这个新的插件,你只需要安装和设置Maven 2。这是因为 你不再需要单独的安装Grails为了使用Maven!

Grails集成Maven 2已经针对Maven 2.0.9及以上版本进行了设计和测试。它将无法工作在更早期的版本中。

为了让你的生活更轻松,我们强烈推荐你添加一个用于Grails的插件组到Maven的设置文件中($USER_HOME/.m2/settings.xml):

<settings><pluginGroups>
    <pluginGroup>org.grails</pluginGroup>
  </pluginGroups>
</settings>

另外,如果你已经有用于Grails设置的Octo Maven工具,那么你需要删除 com.octo.mtg 插件组。

创建一个 Grails Maven 项目

要简单地创建一个支持Maven的Grails项目只要运行下边的命令:

mvn archetype:generate -DarchetypeGroupId=org.grails \
    -DarchetypeArtifactId=grails-maven-archetype \
    -DarchetypeVersion=1.0-SNAPSHOT \
    -DarchetypeRepository=http://snapshots.repository.codehaus.org \
    -DgroupId=example -DartifactId=my-app

无论你想为你的应用选择哪个group ID和artifact ID,一切内容格式都必须像上面写的那样。这将创建一个新的Maven项目以及一个POM文件和一系列其他文件。你不会看到有什么是像一个Grails应用。因此,下一步就要创建一个你要使用的项目结构了:

cd my-app
mvn initialize

现在你已经有一个可以使用的Grails应用了。插件已经集成到了标准的构建周期,因此你可以使用标准的Maven语法来构建和打包你的应用程序了: mvn cleanmvn compilemvn testmvn package

你也可以利用许多已经被包装成Maven目标的Grails命令:

给现有项目加入Maven支持

创建一个全新的项目当然是一个很好的途径,但如果已经有一个项目了该怎么办呢?你应该不会愿意先创建一个新项目然后再把旧项目的内容拷贝进去的。解决方法是使用下列命令为现有项目创建一个POM文件:

mvn grails:create-pom -DgroupId=com.mycompany
当这个命令完成时,你就可以立即使用标准的语法了,如 mvn package 。需要注意的是当创建POM文件时你必须指定一个group ID。

添加Grails命令到 phase 中

标准的POM文件被创建是为了让Grails将合适的核心Grails命令附加到它们对应的构建语法上,因此“compile”对应“compile”语法,“war”对应“package”语法。当你想要将一个插件的命令附加到一个特定的phase上时,这可能没有什么帮助。典型的例子是功能测试。你如何确保你的功能测试(无论正在使用你决定的哪个插件)是使用“integration-test” phase来运行的?

恐怕不是:所有事情都是可能的。在这个例子中,你可以使用额外的“execution”块来将命令联合到一个 phase 上:

<plugin>
        <groupId>org.grails</groupId>
        <artifactId>grails-maven-plugin</artifactId>
        <version>1.0-SNAPSHOT</version>
        <extensions>true</extensions>
        <executions>
          <execution>
            <goals></goals>
          </execution>
          <!-- 添加 "functional-tests" 命令到 "integration-test" phase -->
          <execution>
            <id>functional-tests</id>
            <phase>integration-test</phase>
            <goals>
              <goal>exec</goal>
            </goals>
            <configuration>
              <command>functional-tests</command>
            </configuration>
          </execution>
        </executions>
      </plugin>

这也展示了 grails:exec 目标,它可以用来运行任何Grails命令。简单的将命令的名字作为 command 系统特性,还可以通过 args 特性来选择性地指定参数:

mvn grails:exec -Dcommand=create-webtest -Dargs=Book

5. 对象关联映射 (GORM)

Domain 类是任何商业应用的核心。 他们保存事务处理的状态,也处理预期的行为。 他们通过关联联系在一起, one-to-one 或 one-to-many。

GORM 是 Grails对象关联映射 (GORM)的实现。在底层,它使用 Hibernate 3 (一个非常流行和灵活的开源ORM解决方案), 但是因为Groovy天生的动态性,实际上,对动态类型和静态类型两者都支持,由于Grails的规约,只需要很少的配置涉及Grails domain 类的创建。

你同样可以在Java中编写 Grails domain 类。 请参阅在 Hibernate 集成上如果在Java中编写 Grails domain 类, 不过,它仍然使用动态持久方法。下面是GORM实战预览:

def book = Book.findByTitle("Groovy in Action")

book .addToAuthors(name:"Dierk Koenig") .addToAuthors(name:"Guillaume LaForge") .save()

5.1 快速入门指南

domain类可以使用 create-domain-class 命令来创建:

grails create-domain-class Person

这将在 grails-app/domain/Person.groovy 位置上创建类,如下:

class Person {      
}

如果在 DataSource 上设置dbCreate属性为"update", "create" or "create-drop", Grails 会为你自动生成/修改数据表格。

你可以通过添加属性来自定义类:

class Person {      
        String name
        Integer age
        Date lastVisit
}

一旦你拥有一个 domain 类,可以尝试通过在 shellconsole 上输入:

grails console

这会载入一个交互式GUI,便于你键入Groovy命令。

5.1.1 CRUD基础

尝试执行一些基础的 CRUD (Create/Read/Update/Delete) 操作。

Create

为了创建一个 domain 类,可以使用 Groovy new操作符, 设置它的属性并调用 save:

def p = new Person(name:"Fred", age:40, lastVisit:new Date())
p.save()

save 方法将使用底层的Hibernate ORM持久你的类到数据库中。

Read

Grails 会为你的domain类显式的添加一个隐式 id 属性,便于你检索:

def p = Person.get(1)
assert 1 == p.id

get 方法通过你指定的数据库标识符,从db中读取 Person对象 。 你同样可以使用 read 方法加载一个只读状态对象:

def p = Person.read(1)

在这种情况下,底层的 Hibernate 引擎不会进行任何脏读检查,对象也不能被持久化。 注意,假如你显式的调用 save 方法,对象会回到 read-write 状态.

Update

更新一个实体, 设置一些属性,然后,只需再次调用 save:

def p = Person.get(1)
p.name = "Bob"
p.save()

Delete

删除一个实体使用 delete 方法:

def p = Person.get(1)
p.delete()

5.2 GORM中进行Domain建模

当构建 Grails应用程序时,你必须考虑你要试图解决的问题域。 比如,你正在构建一个 Amazon 书店,你要考虑 books, authors, customers 和publishers 等等.

这些在GORM中被当做Groovy类 来进行建模,因此, Book 类可能拥有 title, release date,ISBN等等。 在后面章节将展示如何在GORM中进行domain建模。

创建domain类,你可以运行 create-domain-class ,如下:

grails create-domain-class Book

将会创建 grails-app/domain/Book.groovy类:

class Book {        
}

如果你想使用 packages 你可以把 Book.groovy类移动到 domain 目录的子目录下,并按照Groovy (和 Java)的 packaging 规则添加正确的 package

上面的类将会自动映射到数据库中名为 book的表格 (与类名相同). 可以通过 ORM Domain Specific Language定制上面的行为。

现在,你可以把这个domain类的属性定义成Java类型。 例如:

class Book {
        String title
        Date releaseDate
        String ISBN
}

每个属性都会被映射到数据库的列,列名的规则是所有列名小写,通过下划线分隔。 比如 releaseDate 映射到 release_date列。 SQL类型会自动检测来自Java的类型 , 但可以通过 ConstraintsORM DSL定制。

5.2.1 GORM中的关联

关联定义了domain类之间的相互作用。除非在两端明确的指定,否则关联只存在被定义的一方。

5.2.1.1 One-to-one

one-to-one 关联是最简单的种类,它只是把它的一个属性的类型定义为其他domain类。 考虑下面的例子:

Example A

class Face {
    Nose nose
}
class Nose {       
}

在这种情况下, 拥有一个FaceNose的one-to-one单向关联。 为了使它双向关联,需要定义另一端,如下:

Example B

class Face {
    Nose nose
}
class Nose {       
        Face face
}

这就是双向关联。不过, 在这种情况下,关联的双方并不能级联更新。

考虑下这样的变化:

Example C

class Face {
    Nose nose
}
class Nose {       
        static belongsTo = [face:Face]
}

在这种情况下,我们使用 belongsTo 来设置Nose "属于" Face。结果是,我们创建一个Face并save 它,数据库将 级联 更新/插入 Nose:

new Face(nose:new Nose()).save()

上面的示例,face 和 nose都会被保存。注意,逆向 不为 true,并会因为一个临时的Face导致一个错误:

new Nose(face:new Face()).save() // will cause an error

belongsTo另一个重要的意义在于,假如你删除一个 Face 实体, Nose 也会被删除:

def f = Face.get(1)
f.delete() // both Face and Nose deleted

如果没有belongsTo ,deletes 将被级联,并会得到一个外键约束错误,除非你明确的删除Nose:

// error here without belongsTo
def f = Face.get(1)
f.delete()

// no error as we explicitly delete both def f = Face.get(1) f.nose.delete() f.delete()

你可以保持上面的关联为单向,为了保证级联保存/更新,可以像下面这样:

class Face {
    Nose nose
}
class Nose {       
        static belongsTo = Face
}

注意,在这种情况下,我们没有在belongsTo使用map语法声明和明确命名关联。Grails 会把它当做单向。.下面的图表概述了3个示例:

5.2.1.2 One-to-many

one-to-many 关联是,当你的一个类,比如 Author ,拥有许多其他类的实体,比如 Book 。 在Grails 中定义这样的关联可以使用 hasMany :

class Author {
    static hasMany = [ books : Book ]

String name } class Book { String title }

在这种情况下,拥有一个单向的one-to-many关联。 Grails 将默认使用一个连接表映射这样的关联。

ORM DSL 允许使用外键关联作为映射单向关联的替代

对于 hasMany 设置,Grails将自动注入一个java.util.Set类型的属性到domain类。 用于迭代集合:

def a = Author.get(1)

a.books.each { println it.title }

Grails中默认使用的fetch策略是 "lazy", 意思就是集合将被延迟初始化。 如果你不小心,这会导致 n+1 问题

如果需要"eager" 抓取 ,需要使用 ORM DSL 或者指定立即抓取作为query的一部分

默认的级联行为是级联保存和更新,但不删除,除非 belongsTo 被指定:

class Author {
    static hasMany = [ books : Book ]

String name } class Book { static belongsTo = [author:Author] String title }

如果在one-to-many的多方拥有2个同类型的属性,必须使用mappedBy 指定哪个集合被映射:

class Airport {
        static hasMany = [flights:Flight]
        static mappedBy = [flights:"departureAirport"]
}
class Flight {
        Airport departureAirport
        Airport destinationAirport
}

如果多方拥有多个集合被映射到不同的属性,也是一样的:

class Airport {
        static hasMany = [outboundFlights:Flight, inboundFlights:Flight]
        static mappedBy = [outboundFlights:"departureAirport", inboundFlights:"destinationAirport"]
}
class Flight {
        Airport departureAirport
        Airport destinationAirport
}

5.2.1.3 Many-to-many

Grails支持many-to-many关联,通过在关联双方定义 hasMany ,并在关联拥有方定义 belongsTo :

class Book {
   static belongsTo = Author
   static hasMany = [authors:Author]
   String title
}
class Author {
   static hasMany = [books:Book]
   String name
}

Grails在数据库层使用一个连接表来映射many-to-many,在这种情况下,Author 负责持久化关联,并且是唯一可以级联保存另一端的一方 。

例如,下面这个可以进行正常级联保存工作:

new Author(name:"Stephen King")
                .addToBooks(new Book(title:"The Stand"))
                .addToBooks(new Book(title:"The Shining"))           
                .save()

而下面这个只保存 Book而不保存 authors!

new Book(name:"Groovy in Action")
                .addToAuthors(new Author(name:"Dierk Koenig"))
                .addToAuthors(new Author(name:"Guillaume Laforge"))             
                .save()

这是所期待的行为,就像Hibernate,只有many-to-many的一方可以负责管理关联。

当前,Grails的Scaffolding 特性支持many-to-many关联, 你必须自己编写关联的管理代码

5.2.1.4 集合类型基础

除了关联不同 domain 类外, GORM 同样支持映射基本的集合类型。比如,下面的类创建一个 nicknames 关联, 它是一个 StringSet 实体:

class Person {
    static hasMany = [nicknames:String]
}

GORM 将使用一个链接表,来映射上面的关联。你可以使用joinTable参数来改变各式各样的连接表映射:

class Person {
    static hasMany = [nicknames:String]

static mapping = { hasMany joinTable:[name:'bunch_o_nicknames', key:'person_id', column:'nickname', type:"text"] } }

上面的示例映射到表后看上去像这样:

bunch_o_nicknames Table

---------------------------------------------
| person_id         |     nickname          |
---------------------------------------------
|   1               |      Fred             |
---------------------------------------------

5.2.2 GORM中的组合

除了 association 之外, Grails 支持组合概念。在这种情况下,并不是把类映射到分离的表格,而是将这个类"embedded"到当前的表格内。 例如:

class Person {
        Address homeAddress
        Address workAddress
        static embedded = ['homeAddress', 'workAddress']
}
class Address {
        String number
        String code
}

所产生的映射看上去像这样:

如果你在grails-app/domain目录中定义了一个单独的Address类, 你同样会得到一个表格。如果你不想这样,你可以 利用Groovy在单个文件定义多个类的能力,让grails-app/domain/Person.groovy 文件中的Person类包含 Address类。

5.2.3 GORM中的继承

GORM 支持从抽象类的继承和具体持久化GORM实体的继承。例如:

class Content {
     String author
}
class BlogEntry extends Content {
    URL url
}
class Book extends Content {
    String ISBN
}
class PodCast extends Content {
    byte[] audioStream
}

上面的示例,我们拥有一个 Content父类和各式各样带有更多指定行为的子类。

注意事项

在数据库层, Grails默认使用一个类一个表格的映射附带一个名为class的识别列, 因此,父类 (Content) 和它的子类(BlogEntry, Book 等等.), 共享 相同的表格。

一个类一个表格的映射有个负面的影响,就是你 不能 有非空属性一起继承映射。 另一个选择是使用每个子类一个表格 ,你可以通过 ORM DSL启用。

不过, 过分使用继承与每个子类一个表格会带来糟糕的查询性能,因为,过分使用链接查询。总之,我们建议:假如你打算使用继承,不要滥用它,不要让你的继承层次太深。

多态性查询

继承的结果是你有能力进行多态查询。比如,在Content使用 list 方法,超类将返回所有Content子类:

def content = Content.list() // list all blog entries, books and pod casts
content = Content.findAllByAuthor('Joe Bloggs') // find all by author

def podCasts = PodCast.list() // list only pod casts

5.2.4 Sets, Lists 和 Maps

Sets对象

默认情况下,在中 GORM定义一个 java.util.Set 映射,它是无序集合,不能包含重复元素。 换句话,当你有:

class Author {
   static hasMany = [books:Book]
}

GORM会将books注入为 java.util.Set类型。 问题在于存取时,这个集合的无序的,可能不是你想要的。为了定制序列,你可以设置为 SortedSet:

class Author {
   SortedSet books
   static hasMany = [books:Book]
}

在这种情况下,需要实现 java.util.SortedSet ,这意味着,你的Book类必须实现 java.lang.Comparable:

class Book implements Comparable {
   String title
   Date releaseDate = new Date()

int compareTo(obj) { releaseDate.compareTo(obj.releaseDate) } }

上面的结果是,Author类的中的books集合将按Book的releasedate排序。

List对象

如果你只是想保持对象的顺序,添加它们和引用它们通过索引,就像array一样,你可以定义你的集合类型为 List:

class Author {
   List books
   static hasMany = [books:Book]
}

在这种情况下当你向books集合中添加一个新元素时,这个顺序将会保存在一个从0开始的列表索引中,因此你可以:

author.books[0] // get the first book

这种方法在数据库层的工作原理是:为了在数据库层保存这个顺序,Hibernate创建一个叫做books_idx的列,它保存着该元素在集合中的索引.

当使用List时,元素在保存之前必须先添加到集合中,否则Hibernate会抛出异常 (org.hibernate.HibernateException: null index column for collection):

// This won't work!
def book = new Book(title: 'The Shining')
book.save()
author.addToBooks(book)

// Do it this way instead. def book = new Book(title: 'Misery') author.addToBooks(book) author.save()

映射(Maps)对象

如果你想要一个简单的 string/value 对map,GROM可以用下面方法来映射:

class Author {
   Map books // map of ISBN:book names
}

def a = new Author() a.books = ["1590597583":"Grails Book"] a.save()

这种情况map的键和值都必须是字符串.

如果你想用一个对象的map,那么你可以这样做:

class Book {
  Map authors
  static hasMany = [authors:Author]
}

def a = new Author(name:"Stephen King")

def book = new Book() book.authors = [stephen:a] book.save()

static hasMany 属性定义了map中元素的类型,map中的key 必须 是字符串.

集合类型和性能

Java中的 Set 是一个不能有重复条目的集合类型. 为了确保添加到 Set 关联中的条目是唯一的,Hibernate 首先加载数据库中的全部关联. 如果你在关联中有大量的条目,那么这对性能来说是一个巨大的浪费.

这样做就需要 List 类型, 因为Hibernate需要加载全部关联以维持供应. 因此如果你希望大量的记录关联,那么你可以制作一个双向关联以便连接能在反面被建立。例如思考一下代码:

def book = new Book(title:"New Grails Book")
def author = Author.get(1)
book.author = author
book.save()

在这个例子中关联链接被child (Book)创建,因此没有必要手动操作集合以使查询更少和高效代码。由于Author有大量的关联的Book 实例,如果你写入像下面的代码,你可以看到性能的影响:

def book = new Book(title:"New Grails Book")
def author = Author.get(1)
author.addToBooks(book)
author.save()

5.3 持久化基础

关于Grails要记住的很重要的一点就是,Grails的底层使用 Hibernate 来进行持久化. 如果您以前使用的是 ActiveRecord 或者 iBatis 您可能会对Hibernate的"session"模型感到有点陌生.

本质上,Grails自动绑定Hibernate session到当前正在执行的请求上.这允许你像使用GORM的其他方法一样很自然地使用 savedelete 方法.

5.3.1 保存和更新

下面看一个使用 save 方法的例子:

def p = Person.get(1)
p.save()

一个主要的不同是当你调用save时候Hibernate不会执行任何SQL操作. Hibernate通常将SQL语句分批,最后执行他们.对你来说,这些一般都是由Grails自动完成的,它管理着你的Hibernate session.

也有一些特殊情况,有时候你可能想自己控制那些语句什么时候被执行,或者用Hibernate的术语来说,就是什么时候session被"flushed".要这样的话,你可以对save方法使用flush参数:

def p = Person.get(1)
p.save(flush:true)

请注意,在这种情况下,所有暂存的SQL语句包括以往的保存将同步到数据库。这也可以让您捕捉任何被抛出的异常,这在涉及乐观锁高度并发的情况下是很常用的:

def p = Person.get(1)
try {
        p.save(flush:true)
}
catch(Exception e) {
        // deal with exception
}

5.3.2 删除对象

下面是 delete 方法的一个例子:

def p = Person.get(1)
p.delete()

默认情况下在执行delete以后Grails将使用事务写入, 如果你想在适当的时候删除,这时你可以使用flush 参数:

def p = Person.get(1)
p.delete(flush:true)

使用 flush 参数也允许您捕获在delete执行过程中抛出的任何异常. 一个普遍的错误就是违犯数据库的约束, 尽管这通常归结为一个编程或配置错误. 下面的例子显示了当您违犯了数据库约束时如何捕捉DataIntegrityViolationException:

def p = Person.get(1)

try { p.delete(flush:true) } catch(org.springframework.dao.DataIntegrityViolationException e) { flash.message = "Could not delete person ${p.name}" redirect(action:"show", id:p.id) }

注意Grails没有提供 deleteAll 方法,因为删除数据是discouraged的,而且通常可以通过布尔标记/逻辑来避免.

如果你确实需要批量删除数据,你可以使用 executeUpdate 法来执行批量的DML语句:

Customer.executeUpdate("delete Customer c where c.name = :oldName", [oldName:"Fred"])

5.3.3 级联更新和删除

在使用GORM时,理解如何级联更新和删除是很重要的.需要记住的关键是 belongsTo 的设置控制着哪个类"拥有"这个关联.

无论是一对一,一对多还是多对多,如果你定义了 belongsTo ,更新和删除将会从拥有类到被它拥有的类(关联的另一方)级联操作.

如果你 没有 定义 belongsTo 那么就不能级联操作,你将不得不手动保存每个对象.

下面是一个例子:

class Airport {
        String name
        static hasMany = [flights:Flight]
}
class Flight {
        String number
        static belongsTo = [airport:Airport]
}

如果我现在创建一个 Airport 对象,并向它添加一些 Flight 它可以保存这个 Airport 并级联保存每个flight,因此会保存整个对象图:

new Airport(name:"Gatwick")
         .addToFlights(new Flight(number:"BA3430"))
         .addToFlights(new Flight(number:"EZ0938"))
         .save()

相反的,如果稍后我删除了这个 Airport 所有跟它关联的 Flight也都将会被删除:

def airport = Airport.findByName("Gatwick")
airport.delete()

然而,如果我将 belongsTo 去掉的话,上面的级联删除代码就了. 不能工作. 为了更好地理解, take a look at the summaries below that describe the default behaviour of GORM with regards to specific associations.

设置了belongsTo的双向一对多

class A { static hasMany = [bees:B] }
class B { static belongsTo = [a:A] }

如果是双向一对多,在多的一端设置了belongsTo,那么级联策略将设置一的一端为"ALL",多的一端为"NONE".

单向一对多

class A { static hasMany = [bees:B] }
class B {  }

如果是在多的一端没有设置belongsTo单向一对多关联,那么级联策略设置将为"SAVE-UPDATE".

没有设置belongsTo的双向一对多

class A { static hasMany = [bees:B] }
class B { A a }

如果是在多的一端没有设置belongsTo的双向一对多关联,那么级联策略将为一的一端设置为"SAVE-UPDATE" 为多的一端设置为"NONE".

设置了belongsTo的单向一对一

class A {  }
class B { static belongsTo = [a:A] }

如果是设置了belongsTo的单向一对一关联,那么级联策略将为有关联的一端(A->B)设置为"ALL",定义了belongsTo的一端(B->A)设置为"NONE".

请注意,如果您需要进一步的控制级联的行为,您可以参见 ORM DSL.

5.3.4 立即加载和延迟加载

在GORM中,关联默认是lazy的.最好的解释是例子:

class Airport {
        String name
        static hasMany = [flights:Flight]
}
class Flight {
        String number
        static belongsTo = [airport:Airport]
}

上面的domain类和下面的代码:

def airport = Airport.findByName("Gatwick")
airport.flights.each {
        println it.name
}

GORM GORM将会执行一个单独的SQL查询来抓取 Airport 实例,然后再用一个额外的for each查询逐条迭代 flights 关联.换句话说,你得到了N+1条查询.

根据这个集合的使用频率,有时候这可能是最佳方案.因为你可以指定只有在特定的情况下才访问这个关联的逻辑.

配置立即加载

一个可选的方案是使用立即抓取,它可以按照下面的方法来指定:

class Airport {
        String name
        static hasMany = [flights:Flight]
        static mapping = {
                flight fetch:"join"
        }
}

在这种情况下 Airport 实例对应的 flights 关联会被一次性全部加载进来(依赖于映射). 这样的好处是执行更少的查询,但是要小心使用,因为使用太多的eager关联可能会导致你将整个数据库加载进内存.

关联也可以用 ORM DSL 将关联声明为 non-lazy

使用批量加载Using Batch Fetching

虽然立即加载适合某些情况,它并不总是可取的,如果您所有操作都使用立即加载,那么您会将整个数据库加载到内存中,导致性能和内存的问题.替代立即加载是使用批量加载.实际上,您可以在"batches"中配置Hibernate延迟加载. 例如:

class Airport {
        String name
        static hasMany = [flights:Flight]
        static mapping = {
                flight batchSize:10
        }
}

在这种情况下,由于 batchSize 参数,当您迭代 flights 关联, Hibernate 加载10个批次的结果. 例如,如果您一个 Airport 有30个s, 如果您没有配置批量加载,那么您在对Airport的查询中只能一次查询出一个结果,那么要执行30次查询以加载每个flight. 使用批量加载,您对Airport查询一次将查询出10个Flight,那么您只需查询3次. 换句话说, 批量加载是延迟加载策略的优化. 批量加载也可以配置在class级别:

class Flight {
        …
        static mapping = {
                batchSize 10
        }
}

5.3.5 悲观锁和乐观锁

乐观锁

默认的GORM类被配置为乐观锁。乐观锁实质上是Hibernate的一个特性,它在数据库里一个特别的 version 字段中保存了一个版本号.

version 列读取包含当前你所访问的持久化实例的版本状态的 version 属性:

def airport = Airport.get(10)

println airport.version

当你执行更新操作时,Hibernate将自动检查version属性和数据库中version列,如果他们不同,将会抛出一个 StaleObjectException 异常,并且当前事物也会被回滚.

这是很有用的,因为它允许你不使用悲观锁(有一些性能上的损失)就可以获得一定的原子性。由此带来的负面影响是,如果你有一些高并发的写操作的话,你必须处理这个异常。这需要刷出(flushing)当前的session:

def airport = Airport.get(10)

try { airport.name = "Heathrow" airport.save(flush:true) } catch(org.springframework.dao.OptimisticLockingFailureException e) { // deal with exception }

你处理异常的方法取决于你的应用. 你可以尝试合并数据,或者返回给用户并让他们来处理冲突.

作为选择,如果它成了问题,你可以求助于悲观锁.

悲观锁

悲观锁等价于执行一个 SQL "SELECT * FOR UPDATE" 语句并锁定数据库中的一行. 这意味着其他的读操作将会被锁定直到这个锁放开.

在Grails中悲观锁通过 lock 方法执行:

def airport = Airport.get(10)
airport.lock() // lock for update
airport.name = "Heathrow"
airport.save()

一旦当前事物被提交,Grails会自动的为你释放锁. 可是,在上述情况下我们做的事情是从正规的SELECT“升级”到SELECT ..FOR UPDATE同时其它线程也会在调用get()和lock()之间更新记录。

为了避免这个问题,你可以使用静态的lock 方法,就像get方法一样传入一个id:

def airport = Airport.lock(10) // lock for update
airport.name = "Heathrow"
airport.save()

这个只有 SELECT..FOR UPDATE 时候可以使用.

尽管Grails和Hibernate支持悲观所,但是在使用Grails内置默认的 HSQLDB 数据库时不支持。如果你想测试悲观锁,你需要一个支持悲观锁的数据库,例如MySQL.

你也可以使用lock 方法在查询中获得悲观锁。例如使用动态查询:

def airport = Airport.findByName("Heathrow", [lock:true])

或者使用criteria:

def airport = Airport.createCriteria().get {
        eq('name', 'Heathrow')
        lock true
}

5.4 GORM查询

GORM提供了从动态查询器到criteria到Hibernate面向对象查询语言HQL的一系列查询方式.

Groovy通过 GPath 操纵集合的能力, 和GORM的像sort,findAll等方法结合起来,形成了一个强大的组合.

但是,让我们从基础开始吧.

获取实例列表

如果你简单的需要获得给定类的所有实例,你可以使用 list 方法:

def books = Book.list()

list 方法支持分页参数:

def books = Book.list(offset:10, max:20)

也可以排序:

def books = Book.list(sort:"title", order:"asc")

这里,Here, the sort 参数是您想要查询的domain类中属性的名字,argument is the name of the domain class property that you wish to sort on, and the order 参数要么以argument is either asc for asc结束ending or要么以 desc for desc结束ending.

根据数据库标识符取回

第二个取回的基本形式是根据数据库标识符取回,使用 get 方法:

def book = Book.get(23)

你也可以根据一个标识符的集合使用 getAll方法取得一个实例列表:

def books = Book.getAll(23, 93, 81)

5.4.1 动态查询器

GORM支持 动态查找器 的概念 . 动态查找器看起来像一个静态方法的调用,但是这些方法本身在代码中实际上并不存在.

而是在运行时基于一个给定类的属性,自动生成一个方法. 比如例子中的 Book 类:

class Book {
        String title
        Date releaseDate
        Author author
}                
class Author {
        String name
}

Book 类有一些属性,比如 title, releaseDateauthor. 这些都可以按照"方法表达式"的格式被用于 findByfindAllBy 方法:

def book = Book.findByTitle("The Stand")

book = Book.findByTitleLike("Harry Pot%")

book = Book.findByReleaseDateBetween( firstDate, secondDate )

book = Book.findByReleaseDateGreaterThan( someDate )

book = Book.findByTitleLikeOrReleaseDateLessThan( "%Something%", someDate )

方法表达式

在GORM中一个方法表达式由前缀,比如 findBy 后面跟一个表达式组成,这个表达式由一个或多个属性组成。基本形式是:

Book.findBy([Property][Comparator][Boolean Operator])?[Property][Comparator]

用'?' 标记的部分是可选的. 每个后缀都会改变查询的性质。例如:

def book = Book.findByTitle("The Stand")

book = Book.findByTitleLike("Harry Pot%")

在上面的例子中,第一个查询等价于等于后面的值, 第二个因为增加了 Like 后缀, 它等价于SQL的 like 表达式.

可用的后缀包括:

你会发现最后三个方法标注了参数的个数,他们的示例如下:

def now = new Date()
def lastWeek = now - 7
def book = Book.findByReleaseDateBetween( lastWeek, now )

books = Book.findAllByReleaseDateIsNull() books = Book.findAllByReleaseDateIsNotNull()

布尔逻辑(AND/OR)

方法表达式也可以使用一个布尔操作符来组合两个criteria:

def books = 
    Book.findAllByTitleLikeAndReleaseDateGreaterThan("%Java%", new Date()-30)

在这里我们在查询中间使用 And 来确保两个条件都满足, 但是同样地你也可以使用 Or:

def books = 
    Book.findAllByTitleLikeOrReleaseDateGreaterThan("%Java%", new Date()-30)

At the moment此时, 你最多只能用两个criteria做动态查询, 也就是说,该方法的名称只能含有一个布尔操作符. 如果你需要使用更多的, 你应该考虑使用 CriteriaHQL.

查询关联

关联也可以被用在查询中:

def author = Author.findByName("Stephen King")

def books = author ? Book.findAllByAuthor(author) : []

在这里如果 Author 实例不为null 我们在查询中用它取得给定 Author 的所有Book实例.

分页和排序

list 方法上可用的分页和排序参数一样,他们同样可以被提供为一个map用于动态查询器的最后一个参数:

def books = 
  Book.findAllByTitleLike("Harry Pot%", [max:3, 
                                         offset:2, 
                                         sort:"title",
                                         order:"desc"])

5.4.2 条件查询

Criteria 是一种类型安全的、高级的查询方法,它使用Groovy builder构造强大复杂的查询.它是一种比使用StringBuffer好得多的选择.

Criteria可以通过 createCriteria 或者 withCriteria 方法来使用. builder使用Hibernate的Criteria API, builder上的节点对应Hibernate Criteria API中 Restrictions 类中的静态方法. 用法示例:

def c = Account.createCriteria()
def results = c {
        like("holderFirstName", "Fred%")
        and {
                between("balance", 500, 1000)
                eq("branch", "London")
        }
        maxResults(10)
        order("holderLastName", "desc")
}

逻辑与(Conjunctions)和逻辑或(Disjunctions)

如前面例子所演示的,你可以用 and { } 块来分组criteria到一个逻辑AND:

and {
        between("balance", 500, 1000)
        eq("branch", "London")
}

逻辑OR也可以这么做:

or {
        between("balance", 500, 1000)
        eq("branch", "London")
}

你也可以用逻辑NOT来否定:

not {
        between("balance", 500, 1000)
        eq("branch", "London")
}

查询关联

关联可以通过使用一个跟关联属性同名的节点来查询. 比如我们说 Account 类有关联到多个 Transaction 对象:

class Account {
    …
    def hasMany = [transactions:Transaction]
    Set transactions
    …
}

我们可以使用属性名 transaction 作为builder的一个节点来查询这个关联:

def c = Account.createCriteria()
def now = new Date()
def results = c.list {
       transactions {
            between('date',now-10, now)
       }
}

上面的代码将会查找所有过去10天内执行过 transactionsAccount 实例. 你也可以在逻辑块中嵌套关联查询:

def c = Account.createCriteria()
def now = new Date()
def results = c.list {
     or {
        between('created',now-10,now)
        transactions {
             between('date',now-10, now)
        }
     }
}

这里,我们将找出在最近10天内进行过交易或者最近10天内新创建的所有用户.

投影(Projections)查询

投影被用于定制查询结果. 要使用投影你需要在criteria builder树里定义一个"projections"节点. projections节点内可用的方法等同于 Hibernate 的 Projections 类中的方法:

def c = Account.createCriteria()

def numberOfBranches = c.get { projections { countDistinct('branch') } }

使用可滚动的结果

Y你可以通过调用scroll方法来使用Hibernate的 ScrollableResults 特性:

def results = crit.scroll {
      maxResults(10)
}
def f = results.first()
def l = results.last()
def n = results.next()
def p = results.previous()

def future = results.scroll(10) def accountNumber = results.getLong('number')

下面引用的是Hibernate文档中关于ScrollableResults的描述:

结果集的迭代器(iterator)可以以任意步进的方式前后移动,而Query / ScrollableResults模式跟JDBC的PreparedStatement/ ResultSet也很像,其接口方法名的语意也跟ResultSet的类似.

不同于JDBC,结果列的编号是从0开始.

在Criteria实例中设置属性

如果在builder树内部的一个节点不匹配任何一项特定标准,它将尝试设置为Criteria对象自身的属性。因此允许完全访问这个类的所有属性。下面的例子是在Criteria Criteria实例上调用 setMaxResultssetFirstResult:

import org.hibernate.FetchMode as FM
        …
        def results = c.list {
                maxResults(10)
                firstResult(50)
                fetchMode("aRelationship", FM.EAGER)
        }

立即加载的方式查询

Eager and Lazy Fetching立即加载和延迟加载 这节,我们讨论了如果指定特定的抓取方式来避免N+1查询的问题。这个criteria查询也可以做到:

def criteria = Task.createCriteria()
def tasks = criteria.list{
     eq "assignee.id", task.assignee.id
     join 'assignee'
     join 'project'
     order 'priority', 'asc'
}

注意这个 join 方法的用法. This method indicates the criteria API that a JOIN query should be used to obtain the results.

方法引用

如果你调用一个没有方法名的builder,比如:

c { … }

默认的会列出所有结果,因此上面代码等价于:

c.list { … }

方法 描述
list 这是默认的方法。它会返回所有匹配的行。
get 返回唯一的结果集,比如,就一行。criteria已经规定好了,仅仅查询一行。这个方法更方便,免得使用一个limit来只取第一行使人迷惑。
scroll 返回一个可滚动的结果集
listDistinct 如果子查询或者关联被使用,有一个可能就是在结果集中多次出现同一行,这个方法允许只列出不同的条目,它等价于 CriteriaSpecification 类的DISTINCT_ROOT_ENTITY

5.4.3 Hibernate查询语言(HQL)

GORM也支持Hibernate的查询语言HQL,在Hibernate文档中的 Chapter 14. HQL: The Hibernate Query Language 可以找到它非常完整的参考手册。

GORM提供了一些使用HQL的方法,包括 find, findAllexecuteQuery. 下面是一个查询的例子:

def results =
      Book.findAll("from Book as b where b.title like 'Lord of the%'")

位置和命名参数

上面的例子中传递给查询的值是硬编码的,但是,你可以同样地使用位置参数:

def results =
      Book.findAll("from Book as b where b.title like ?", ["The Shi%"])

或者甚至使用命名参数:

def results =
      Book.findAll("from Book as b where b.title like :search or b.author like :search", [search:"The Shi%"])

多行查询

如果你需要将查询分割到多行你可以使用一个行连接符:

def results = Book.findAll("\
from Book as b, \
     Author as a \
where b.author = a and a.surname = ?", ['Smith'])

Groovy 的多行字符串对HQL查询无效

分页和排序

使用HQL查询的时候你也可以进行分页和排序。要做的只是简单指定分页和排序参数作为一个散列在方法的末尾调用:

def results =
      Book.findAll("from Book as b where b.title like 'Lord of the%' order by b.title asc",
                   [max:10, offset:20])

5.5 高级GORM特性

接下来的章节覆盖更多高级的GORM使用 包括 缓存、定制映射和事件。

5.5.1 事件和自动实现时间戳

GORM支持事件注册,只需要将事件作为一个闭包即可,当某个事件触发,比如删除,插入,更新。The following is a list of supported events下面就是所支持事件的列表:

为了添加一个事件需要在你的领域类中添加相关的闭包。

事件类型

beforeInsert事件

当一个对象保存到数据库之前触发

class Person {
   Date dateCreated

def beforeInsert = { dateCreated = new Date() } }

beforeUpdate事件

当一个对象被更新之前触发

class Person {
   Date dateCreated
   Date lastUpdated

def beforeInsert = { dateCreated = new Date() } def beforeUpdate = { lastUpdated = new Date() } }

beforeDelete事件

当一个对象被删除以后触发.

class Person {
   String name
   Date dateCreated
   Date lastUpdated

def beforeDelete = { new ActivityTrace(eventName:"Person Deleted",data:name).save() } }

onLoad事件

当一个对象被加载之后触发:

class Person {
   String name
   Date dateCreated
   Date lastUpdated

def onLoad = { name = "I'm loaded" } }

自动时间戳

上面的例子演示了使用事件来更新一个 lastUpdateddateCreated 属性来跟踪对象的更新。事实上,这些设置不是必须的。通过简单的定义一个 lastUpdateddateCreated 属性,GORM会自动的为你更新。

如果,这些行为不是你需要的,可以屏蔽这些功能。如下设置:

class Person {
   Date dateCreated
   Date lastUpdated
   static mapping = {
      autoTimestamp false
   }
}

5.5.2 自定义ORM映射

Grails 的域对象可以映射到许多遗留的模型通过 关系对象映射域语言。接下来的部分将带你领略它是可能的通过ORM DSL。

这是必要的,如果你高兴地坚持以约定来定义GORM对应的表,列名等。你只需要这个功能,如果你需要定制GORM 映射到遗留模型或进行缓存

自定义映射是使用静态的mapping 块定义在你的域类中的:

class Person {
  ..
  static mapping = {

} }

5.5.2.1 表名和列名

表名

类映射到数据库的表名可以通过使用 table关键字来定制:

class Person {
  ..
  static mapping = {
      table 'people'
  }
}

在上面的例子中,类会映射到 people 表来代替默认的 person表.

列名

同样,也是可能的定制某个列到数据库。比如说,你想改变列名例子如下:

class Person {
  String firstName
  static mapping = {
      table 'people'
      firstName column:'First_Name'
  }
}

在这个例子中,你定义了一个column块,此块包含的方法调用匹配每一个属性名称 (in this case firstName). 接下来使用命名的 column, 来指定字段名称的映射.

列类型

GORM还可以通过DSL的type属性来支持Hibernate类型. 包括特定Hibernate的 org.hibernate.usertype.UserType 的子类, which allows complete customization of how a type is persisted. 比如,有一个 PostCodeType 你可以象下面这样使用:

class Address {
   String number
   String postCode
   static mapping = {
      postCode type:PostCodeType
   }
}

另外如果你想将它映射到Hibernate的基本类型而不是Grails的默认类型,可以参考下面代码:

class Address {
   String number
   String postCode
   static mapping = {
      postCode type:'text'
   }
}

上面的例子将使 postCode 映射到数据库的SQL TEXT或者CLOB类型.

See the Hibernate documentation regarding Basic Types for further information.

一对一映射

在关联中,你也有机会改变外键映射联系,在一对一的关系中,对列的操作跟其他常规的列操作并无二异,例子如下:

class Person {
  String firstName
  Address address
  static mapping = {
      table 'people'
      firstName column:'First_Name'
          address column:'Person_Adress_Id'
  }
}

默认情况下 address 将映射到一个名称为 address_id 的外键. 但是使用上面的映射,我们改变外键列为 Person_Adress_Id.

一对多映射

在一个双向的一对多关系中,你可以象前节中的一对一关系中那样改变外键列,只需要在多的一端中改变列名即可。然而,在单向关联中,外键需要在关联自身中(即一的一端-译者注)指定。比如,给定一个单向一对多联系 PersonAddress 下面的代码会改变 address 表中外键:

class Person {
  String firstName
  static hasMany = [addresses:Address]
  static mapping = {
      table 'people'
      firstName column:'First_Name'
          addresses column:'Person_Address_Id'
  }
}

如果你不想在 address 表中有这个列,可以通过中间关联表来完成,只需要使用 joinTable 参数即可:

class Person {
  String firstName
  static hasMany = [addresses:Address]
  static mapping = {
      table 'people'
      firstName column:'First_Name'
      addresses joinTable:[name:'Person_Addresses', key:'Person_Id', column:'Address_Id']
  }
}

多对多映射

默认情况下, Grails中多对多的映射是通过中间表来完成的. 以下面的多对多关联为例:

class Group {
        …
        static hasMany = [people:Person]
}
class Person {
        …
        static belongsTo = Group
        static hasMany = [groups:Group]
}

在上面的例子中Grails将会创建一个 group_person 表包含外键 person_idgroup_id 对应 persongroup 表. 假如你需要改变列名,你可以为每个类指定一个列映射.

class Group {
   …
   static mapping = {
       people column:'Group_Person_Id'
   }       
}
class Person {
   …
   static mapping = {
       groups column:'Group_Group_Id'
   }       
}

你也可以指定中间表的名称:

class Group {
   …
   static mapping = {
       people column:'Group_Person_Id',joinTable:'PERSON_GROUP_ASSOCIATIONS'
   }       
}
class Person {
   …
   static mapping = {
       groups column:'Group_Group_Id',joinTable:'PERSON_GROUP_ASSOCIATIONS'
   }       
}

5.5.2.2 缓存策略

设置缓存

Hibernate 本身提供了自定义二级缓存的特性. 这就需要在 grails-app/conf/DataSource.groovy 文件中配置:

hibernate {
    cache.use_second_level_cache=true
    cache.use_query_cache=true
    cache.provider_class='org.hibernate.cache.EhCacheProvider'
}

当然,你也可以按你所需来定制设置,比如,你想使用分布式缓存机制.

想了解更多Hibernate的二级缓存,参考 Hibernate documentation 相关文档.

缓存实例

假如要在映射代码块中启用缺省的缓存,可以通过调用 cache 方法实现:

class Person {
  ..
  static mapping = {
      table 'people'
      cache true
  }
}

上面的例子中将配置一个读-写(read-write)缓存包括lazy和non-lazy属性.假如你想定制这些特性,你可以如下所示:

class Person {
  ..
  static mapping = {
      table 'people'
      cache usage:'read-only', include:'non-lazy'
  }
}

缓存关联对象

就像使用Hibernate的二级缓存来缓存实例一样,你也可以来缓存集合(关联),比如:

class Person {
  String firstName
  static hasMany = [addresses:Address]
  static mapping = {
      table 'people'
      version false
      addresses column:'Address', cache:true
  }
}
class Address {
   String number
   String postCode
}

上面的例子中,我们在 addresses 集合启用了一个读-写缓存,你也可以使用:

cache:'read-write' // or 'read-only' or 'transactional'

更多配置请参考缓存用法.

Caching Queries

You can cache queries such as dynamic finders and criteria. To do so using a dynamic finder you can pass the cache argument:

def person = Person.findByFirstName("Fred", cache:true)

Note that in order for the results of the query to be cached, you still need to enable caching in your mapping as discussed in the previous section.

You can also cache criteria queries:

def people = Person.withCriteria {
        like('firstName', 'Fr%')
        cache true
}

缓存用法

下面是不同缓存设置和他们的使用方法:

5.5.2.3 继承策略

默认情况下GORM 类使用 table-per-hierarchy 来映射继承的. 这就有一个缺点就是在数据库层面,列不能有 NOT-NULL 的约束。如果你更喜欢 table-per-subclass 你可以使用下面方法:

class Payment {
    Long id
    Long version
    Integer amount

static mapping = { tablePerHierarchy false } } class CreditCardPayment extends Payment { String cardNumber }

在祖先 Payment 类的映射设置中,指定了在所有的子类中,不使用 table-per-hierarchy 映射.

5.5.2.4 自定义数据库标识符

你可以通过DSL来定制GORM生成数据库标识,缺省情况下GORM将根据原生数据库机制来生成ids,这是迄今为止最好的方法,但是仍存在许多模式,不同的方法来生成标识。

为此,Hibernate特地定义了id生成器的概念,你可以自定义它要映射的id生成器和列,如下:

class Person {
  ..
  static mapping = {
      table 'people'
      version false
      id generator:'hilo', params:[table:'hi_value',column:'next_value',max_lo:100]
  }
}

在上面的例子中,我们使用了Hibernate内置的'hilo'生成器,此生成器通过一个独立的表来生成ids.

想了解更多不同的Hibernate生成器请参考 Hibernate文档

注意,如果你仅仅想定制列id,你可以这样:

class Person {
  ..
  static mapping = {
      table 'people'
      version false
      id column:'person_id'
  }
}

5.5.2.5 复合主键

GORM支持复合标识(复合主键--译者注). 概念(标识由两个或者更多属性组成,这不是我们建议的方法,但是如果你想这么做,这也是可能的:

class Person {
  String firstName
  String lastName

static mapping = { id composite:['firstName', 'lastName'] } }

上面的代码将通过Person类的 firstNamelastName 属性来创建一个复合id。当你后面需要通过id取一个实例时,你必须用这个对象的原型:

def p = Person.get(new Person(firstName:"Fred", lastName:"Flintstone"))
println p.firstName

5.5.2.6 数据库索引

To get the best performance out of your queries it is often necessary to tailor the table index definitions. How you tailor them is domain specific and a matter of monitoring usage patterns of your queries. 为得到最好的查询性能,通常你需要调整表的索引定义。如何调整它们是跟特定领域和要查询的用法模式相关的。使用GORM的DSL你可以指定那个列需要索引:

class Person {
  String firstName
  String address
  static mapping = {
      table 'people'
      version false
      id column:'person_id'
      firstName column:'First_Name', index:'Name_Idx'
      address column:'Address', index:'Name_Idx, Address_Index'
  }
}

5.5.2.7 乐观锁和版本定义

就像在 乐观锁和悲观锁 部分讨论的 , 默认情况下,GORM使用乐观锁和在每一个类中自动注入一个 version 属性,此属性将映射数据库中的一个 version 列.

如果你映射的是一个遗留数据库(已经存在的数据库--译者注), 这将是一个问题,因此可以通过如下方法来关闭这个功能:

class Person {
  ..
  static mapping = {
      table 'people'
      version false
  }
}

如果你关闭了乐观锁 你将自己负责并发更新并且存在用户丢失数据的风险 (due to data overriding) 除非你使用 悲观锁

5.5.2.8 立即加载和延迟加载

延迟加载集合

就像在 立即加载和延迟加载, 部分讨论的,默认情况下,GORM 集合使用延迟加载的并且可以通过 fetchMode 来配置, 但如果你更喜欢把你所有的映射都集中在 mappings 代码块中,你也可以使用ORM的DSL来配置获取模式:

class Person {
  String firstName
  static hasMany = [addresses:Address]
  static mapping = {
      addresses lazy:false
  }
}
class Address {
  String street
  String postCode
}

延迟加载单向关联

在GORM中,one-to-one和many-to-one关联缺省是非延迟加载的.这在有很多实体(数据库记录-译者注)的时候,会产生性能问题,尤其是关联查询是以新的SELECT语句执行的时候. 此时你应该将one-to-one和many-to-one关联的延迟加载象集合那样进行设置:

class Person {
        String firstName
        static belongsTo = [address:Address]
        static mapping = {
                address lazy:true // lazily fetch the address
        }
}
class Address {
        String street
        String postCode
}

这里我们设置 Personaddress属性为延迟加载.

5.5.2.9 自定义级联行为

正如 级联更新 这节描述的,控制更新和删除的主要机制是从关联一端到 belongsTo 静态属性的一端。

然而,通过cascade属性,ORM DSL可以让你访问Hibernate的 transitive persistence 能力。

有效级联属性的设置包括:

获得级联样式更好的理解和用法的介绍,请阅读Hibernate文档的transitive persistence章节

使用上述的值定义一个或多个级联属性(逗号分隔):

class Person {
  String firstName
  static hasMany = [addresses:Address]
  static mapping = {
      addresses cascade:"all,delete-orphan"
  }
}
class Address {
  String street
  String postCode
}

5.5.2.10 自定义Hibernate的类型

在较早的章节看到可以(通过 embedded 属性) 把一个表分成多个对象。 你也可以通过Hibernate的自定义用户类型实现相同的效果。这不是领域类本身,而是java或者groovy类。 所有这些类型都有一个继承自org.hibernate.usertype.UserType org.hibernate.usertype.UserType 的"meta-type"类。

Hibernate参考手册 有一些自定义类型资料,在这里我们将重点放在如何在Grails中映射。让我们看一个使用老式的(Java 1.5以前)枚举类型安全的领域类:

class Book {
  String title
  String author
  Rating rating

static mapping = { rating type: RatingUserType } }

我们所要做的是声明 rating 的枚举类型和在自定义映射UserType中设置属性的类型。这是你想使用自定义类型所必须做的。你也可以使用其他列的设置,比如使用"column"来改变列名和使用"index"把它添加到index。

自定义类型不局限于只是一个列,他们可以映射到多列。在这种情况下,你必须在映射中明确地定义那列使用, 因为Hibernate只能为一列使用属性名。 幸运的是,Grails可以为属性映射多列:

class Book {
  String title
  Name author
  Rating rating

static mapping = { name type: NameUserType, { column name: "first_name" column name: "last_name" } rating type: RatingUserType } }

上面的例子将为author属性创建"first_name"和"last_name"列。You'll be pleased to know that you can also use some of the normal column/property mapping attributes in the column definitions. For example:

column name: "first_name", index: "my_idx", unique: true

The column definitions do not support the following attributes: type, cascade, lazy, cache, and joinTable.

One thing to bear in mind with custom types is that they define the SQL types for the corresponding database columns. That helps take the burden of configuring them yourself, but what happens if you have a legacy database that uses a different SQL type for one of the columns? In that case, you need to override column's SQL type using the sqlType attribute:

class Book {
  String title
  Name author
  Rating rating

static mapping = { name type: NameUserType, { column name: "first_name", sqlType: "text" column name: "last_name", sqlType: "text" } rating type: RatingUserType, sqlType: "text" } }

Mind you, the SQL type you specify needs to still work with the custom type. So overriding a default of "varchar" with "text" is fine, but overriding "text" with "yes_no" isn't going to work.

5.5.3 缺省排序

你可以使用像 list 方法中的参数来排序对象:

def airports = Airport.list(sort:'name')

当然,你也可以定义一个排序的声明:

class Airport {
        …
        static mapping = {
                sort "name"
        }
}

必要的话你可以配置这个排序:

class Airport {
        …
        static mapping = {
                sort name:"desc"
        }
}

另外,您也可以在关联中配置排序:

class Airport {
        …
        static hasMany = [flights:Flight]
        static mapping = {
                flights sort:'number'
        }
}

5.6 事务编程

Grails是构建在Spring的基础上的,所以使用Spring的事务来抽象处理事务编程,但GORM类通过 withTransaction 方法使得处理更简单,方法的第一个参数是Spring的 TransactionStatus 对象.

典型的使用场景如下:

def transferFunds = {
        Account.withTransaction { status ->
                def source = Account.get(params.from)
                def dest = Account.get(params.to)

def amount = params.amount.toInteger() if(source.active) { source.balance -= amount if(dest.active) { dest.amount += amount } else { status.setRollbackOnly() } }

}

}

在上面的例子中,如果目的账户没有处于活动状态,系统将回滚事务,同时如果有任何异常抛出在事务的处理过程中也将会自动回滚。

假如你不想回滚整个事务,你也可以使用"save points"来回滚一个事务到一个特定的点。你可以通过使用Spring的 SavePointManager 接口来达到这个目的.

The withTransaction 方法为你处理begin/commit/rollback代码块作用域内的逻辑。

5.7 GORM和约束

尽管约束是 验证 章节的内容, 但是在此涉及到约束也是很重要的,因为一些约束会影响到数据库的生成。

Grails通过使用领域类的约束来影响数据库表字段(领域类所对于的属性)的生成,还是可行的。

考虑下面的例子,假如我们有一个域模型如下的属性.

String name
String description

默认情况下,在MySql数据库中,Grails将会定义这个列为...

column name | data type 
 description | varchar(255)

但是,在业务规则中,要求这个领域类的description属性能够容纳1000个字符,在这种情况下,如果我们是使用SQL脚本,那么我们定义的这个列可能是:

column name | data type 
 description | TEXT

现在我们又想要在基于应用程序的进行验证,_要求在持久化任何记录之前_,确保不能超过1000个字符。在Grails中,我们可以通过constraints. 来完成,我们将在领域类中新增如下的约束声明.

static constraints = {
        description(maxSize:1000)
}

这个约束条件将会提供我们所需的基于应用程序的验证并且也将生成上述示例所示的数据库信息。下面是影响数据库生成的其他约束的描述。

影响字符串类型属性的约束

如果 maxSize 或者 size 约束被定义, Grails将根据约束的值设置列的最大长度.

通常, 不建议在同一个的领域类中组合使用这些约束. 但是, 如果你非要同时定义 maxSizesize 约束的话, Grails将设置列的长度为 maxSize 约束和size上限约束的最少值. (Grails使用两者的最少值,因此任何超过最少值的长度将导致验证错误.)

如果定义了inList约束 (maxSizesize 未定义), 字段最大长度将取决于列表(list)中最长字符串的的长度. 以"Java"、"Groovy"和"C++"为例, Grails将设置字段的长度为6("Groovy"的最长含有6个字符).

影响数值类型属性的约束

如果定义了 maxmin 或者range约束, Grails将基于约束的值尝试着设置列的精度. (设置的结果很大程度上依赖于Hibernate跟底层数据库系统的交互程度.)

通常来说, 不建议在同一领域类的属性上组合成双的min/max和range约束,但是如果这些约束同时被定义了,那么Grails将使用约束值中的最少精度值. (Grails取两者的最少值,是因为任意超过最少精度的长度将会导致一个验证错误.)

如果定义了scale约束, 那么Grails会试图使用基于约束的值来设置列的 标度(scale) . 此规则仅仅应用于浮点数值 (比如,java.lang.Float,java.Lang.Double, java.lang.BigDecimal及其相关的子类). (设置的结果同样也是很大程度上依赖于Hibernate跟底层数据库系统的交互程度.)

约束定义着数值的最小/最大值, Grails使用数字的最大值来设置其精度. 切记仅仅指定min/max约束中的一个,是不会影响到数据库的生成的 (因为可能会是很大的负值,比如当max是100), ,除非指定的约束值要比Hibernate默认的精度(当前是19)更高.比如...

someFloatValue(max:1000000, scale:3)

将产生:

someFloatValue DECIMAL(19, 3) // precision is default

但是

someFloatValue(max:12345678901234567890, scale:5)

将产生:

someFloatValue DECIMAL(25, 5) // precision = digits in max + scale

someFloatValue(max:100, min:-100000)

将产生:

someFloatValue DECIMAL(8, 2) // precision = digits in min + default scale

6.Web层

6.1 控制器(Controllers)

一个控制器(Controllers)处理请求并创建或准备响应 ,是请求范围。 换句话说,会为每个 request 创建一个新的实体。 一个控制器(Controller)可以生成响应或委托给视图。 创建一个控制器(Controller)只需要创建一个以 Controller 结尾的类。并放置于 grails-app/controllers 目录下。

默认的 URL Mapping 设置确保控制器(Controllers)名字的第一个部分被映射到URI上 ,每个在控制器(Controllers)中定义的操作(Action)被映射到控制器(Controller)名字URI中的URI。

6.1.1 理解控制器(Controller)与操作(Action)

创建控制器(Controller)

可以通过 create-controller 创建控制器(Controllers)。例如,你可以在Grails项目的根目录尝试运行下面命令:

grails create-controller book

这条命令将会在grails-app/controllers/BookController.groovy路径下创建一个控制器(Controller):

class BookController { … }

BookController 默认被映射到 /book URI(相对于你应用程序根目录).

create-controller 命令只不过是方便的工具,你同样可以使用你喜欢的文本编辑器或IDE更容易的创建控制器(Controller)

创建操作(Action)

一个控制器(Controllers) 可以拥有多个属性,每个属性都可以被分配一个代码块。所有这样的属性都被映射到URI:

class BookController {
    def list = {

// do controller logic // create model

return model } }

默认情况下,由于上面示例属性名被命名为list所以被映射到/book/list URI。

默认Action

一个控制器(Controller)具有默认 URI概念,即被映射到 控制器(Controller)的根URI。默认情况下,默认的URI是/book。 默认的URI通过下面的规则来规定:

def defaultAction = "list"

6.1.2 控制器(Controller) 与作用域

可用的作用域

作用域本质上就是hash对象,它允许你存储变量。 下面的作用域在 控制器(Controller)中可以使用:

存取作用域

作用域可以通过上面的变量名与Groovy数组索引操作符结合来进行存取。甚至是Servlet API提供的类,像 HttpServletRequest:

class BookController {
    def find = {
        def findBy = params["findBy"]
        def appContext = request["foo"]
        def loggedUser = session["logged_user"]

} }

你设置可以使用.操作符来存取作用域中的变量,这是语法更加清楚:

class BookController {
    def find = {
        def findBy = params.findBy
        def appContext = request.foo
        def loggedUser = session.logged_user

} }

这是Grails统一存取不同作用域的一种方式。

使用Flash作用域

Grails 支持 flash作用域的概念,它只用于临时存储用于这个请求到下个请求的属性,然后,这个属性就会被清除 对于重定向前直接设置消息是非常有用的,例如:

def delete = {
    def b = Book.get( params.id )
    if(!b) {
        flash.message = "User not found for id ${params.id}"
        redirect(action:list)
    }
    … // remaining code
}

6.1.3 Models(模型)与Views(视图)

Returning the Model

一个model本质上就是一个map,在视图渲染时使用。map中的keys转化为变量名,用于视图的获取。 第一种方式是明确的return一个model:

def show = {
 	[ book : Book.get( params.id ) ]
}

如果没有明确的 model被return,控制器(Controller)的属性将会被视为 model。 所以允许你这样编写代码:

class BookController {
    List books
    List authors
    def list = {
           books = Book.list()
           authors = Author.list()
    }
}

这可能由于实际上控制器(Controller)是 prototype(原型)范围。换句话说,每个请求都会创建一个新的控制器(Controller)。 否则,像上面的代码,就不会是线程安全的。

上面示例中,booksauthors属性在视图中都是可用的。

一个更高级的方式就是 return一个 Spring ModelAndView 类的实体:

import org.springframework.web.servlet.ModelAndView

def index = { def favoriteBooks = … // get some books just for the index page, perhaps your favorites

// forward to the list view to show them return new ModelAndView("/book/list", [ bookList : favoriteBooks ]) }

选择View

在之前的2个示例中,都没有指定哪个 view 用于渲染。因此,Grails怎么知道哪个 view被选取?答案在于规约。对于action:

class BookController {
	def show = {
	 	[ book : Book.get( params.id ) ]
	}	
}

Grails 会自动查找位于 grails-app/views/book/show.gspview (事实上, Grails 会首先查找JSP,因为,Grails同样可以与 JSP一起使用).

假如,你想渲染其他view, render 方法在这里就能帮助你:

def show = {
  	def map = [ book : Book.get( params.id ) ]
    render(view:"display", model:map)
}

这种情况下,Grails将会尝试渲染位于 grails-app/views/book/display.gsp的view。注意,Grails自动描述位于book文件夹中的 grails-app/views路径位置的视图。很方便,但是,如果你拥有某些共享的视图需要存取,作为替代使用:

def show = {
  	def map = [ book : Book.get( params.id ) ]
    render(view:"/shared/display", model:map)
}

在这种情况下,Grails将尝试渲染grails-app/views/shared/display.gsp位置上的视图。

渲染响应

有时它很容易的渲染来自创建控制器小块文本或者代码的响应(通常使用Ajax应用程序)。因为,使用高度灵活的 render方法:

render "Hello World!"

上面的代码,在响应中写入 "Hello World!"文本, 其他的示例包括:

// write some markup
render {
   for(b in books) {
      div(id:b.id, b.title)
   }
}
// render a specific view
render(view:'show')
// render a template for each item in a collection
render(template:'book_template', collection:Book.list())
// render some text with encoding and content type
render(text:"<xml>some xml</xml>",contentType:"text/xml",encoding:"UTF-8")

如果,你打算使用Groovy的MarkupBuilder来产生html,可以使用render来避免html元素与Grails标签之间的命名冲突。例如:

def login = {
        StringWriter w = new StringWriter()
        def builder = new groovy.xml.MarkupBuilder(w)
        builder.html{
            head{
                title 'Log in'
            }
            body{
                h1 'Hello'
                form{

} } }

def html = w.toString() render html }

实际上调用 form标签 (将返回一些文本,而忽略MarkupBuilder). 为了正确的输出 <form>元素,使用下面这些:

def login = {
        // …
        body{
            h1 'Hello'
            builder.form{

} } // … }

6.1.4 重定向与链接

Redirects

使用redirect方法,Actions(操作)可在所有的控制器(Controller)中重定向:

class OverviewController {
                     def login = {}

def find = { if(!session.user) redirect(action:login) … } }

redirect 方法内部使用HttpServletResonse对象的sendRedirect方法。

redirect 方法可以选择如下用法之一:

// 调用同一个类的login action
                 redirect(action:login)

// 重定向到home 控制器(Controller)的index action
                 redirect(controller:'home',action:'index')

// 明确的重定向到URI
                 redirect(uri:"/login.html")

// 重定向到一个URL
 redirect(url:"http://grails.org")

使用方法的params 参数,参数可以选择性的从一个 action传递到下一个:

redirect(action:myaction, params:[myparam:"myvalue"])

通过 params动态属性,这些方法变得可用,同样也接受request参数。 如果指定一个名字与request参数的名字相同的参数,则 request参数被隐藏,控制器(Controller)参数被使用。

因为 params对象也是一个 map,可以使用它把当前的request参数,从一个 action传递到下一个:

redirect(action:"next", params:params)

最后,你也可以在一个目标URI上包含一个片段(fragment):

redirect(controller: "test", action: "show", fragment: "profile")

将(依靠 URL mappings) 导航到/myapp/test/show#profile"。

h4. 链接

Actions同样可以被链接。链接允许model在一个操作(Action)到下一个操作(Action)中保留。例如下面调用first action :

class ExampleChainController {
                     def first = {
                         chain(action:second,model:[one:1])
                     }
                     def second  = {
                         chain(action:third,model:[two:2])
                     }
                     def third = {
                          [three:3])
                     }
                 }

model的结果:

[one:1, two:2, three:3]

通过chainModel map,这个 model在chain中会被随后的 控制器(controller)操作(actions)存取. 这个动态属性只存在于随后调用chain方法的操作(actions)中:

class ChainController {

def nextInChain = { def model = chainModel.myModel … } }

Like the redirect method you can also pass parameters to the chain method:

chain(action:"action1", model:[one:1], params:[myparam:"param1"])

6.1.5 Controller(控制器) 拦截器

通常,它用于拦截基于每个request(请求),session(会话)或应用程序状态的数据处理,这可以通过 action(操作)拦截器来实现。 目前有两种拦截器类型: before 和 after.

假如你的拦截器可能被用于更多的controller(控制器), 几乎肯定会写一个更好的 Filter(过滤器). Filters(过滤器) 可以应用于多个controllers(控制器)或 URIs, 无需改变任何controller(控制器)逻辑.

Before 拦截器

beforeInterceptor在action (操作)被执行前进行数据处理拦截 . 假如它返回 false,那么 ,被拦截的action (操作)将不会被执行. 拦截器可以像下面这样被定义为拦截一个controller(控制器)中所有的action (操作):

def beforeInterceptor = {
       println "Tracing action ${actionUri}"
}

上面是在controller(控制器)定义主体内被声明. 它会在所有 action(操作)之前被执行,并且不会干扰数据处理. 一个普通的使用情形是为了验证:

def beforeInterceptor = [action:this.&auth,except:'login']
// defined as a regular method so its private
def auth() {
     if(!session.user) {
            redirect(action:'login')
            return false
     }
}
def login = {
     // display login page
}

上面的代码定义了一个名为auth的方法. 使用一个方法,是为了让它不会作为一个 action(操作)而暴露于外界(即. 它是private). 随后,beforeInterceptor 定义用于'except' login actions(操作)之外的所有 actions(操作)的拦截,并告知执行'auth' 方法. 'auth' 方法是使用Groovy的方法指针语法来引用 ,在方法内部,它自己会检测是否一个用户在session(会话)内,否则,重定向到 login action(操作) 并返回 false, 命令被拦截的actions(操作)不被执行 .

After 拦截器

为了定义一个在actions(操作)之后执行的拦截,可以使用afterInterceptor 属性:

def afterInterceptor = { model ->
       println "Tracing action ${actionUri}"
}

after 拦截器把结果 model作为参数,所以,可以执行model或response的post操作.

after 拦截器 也可以在渲染之前修改Spring MVC ModelAndView对象. 在这种情况下, 上面的示例变成:

def afterInterceptor = { model, modelAndView ->
       println "Current view is ${modelAndView.viewName}"
       if(model.someVar) modelAndView.viewName = "/mycontroller/someotherview"
       println "View is now ${modelAndView.viewName}"
}

通过当前action(操作),允许基于被返回的model改变视图. 注意,如果action(操作)被拦截调用redirect 或render, modelAndView 可能为null.

拦截条件

Rails 用户非常熟悉验证示例 ,以及如何在'except'条件的使用下执行拦截 (拦截器在Rails中被称为'过滤器', 这个术语与Java领域中的servlet 过滤器术语有冲突):

def beforeInterceptor = [action:this.&auth,except:'login']

除了被指定的actions(操作),它执行所有actions(操作)的拦截. 一组actions(操作)列表同样可以像下面这样被定义:

def beforeInterceptor = [action:this.&auth,except:['login','register']]

其他被支持的条件是'only', 它只对被指定的actions(操作)执行拦截:

def beforeInterceptor = [action:this.&auth,only:['secure']]

6.1.6 数据绑定

数据绑定是"绑定"进入的请求参数到一个对象的属性或者一个完整对象图的行为. 数据绑定将处理所有来自请求参数必要的类型装换,典型的传送通过表单提交 , 始终是字符串,尽管Groovy或Java对象的属性可能不一定是.

Grails使用 Spring's底层的数据绑定能力来完成数据绑定.

绑定Request数据到Model上

这里有2种方式来绑定请求参数到domain类的属性上. 第一种涉及使用domain类的隐式构造函数:

def save = {
  def b = new Book(params)
  b.save()
}

这里的数据绑定发生在代码new Book(params)内.通过传递 params 对象给domain类的构造函数, Grails 自动识别来自请求参数的绑定 . 因此,假如你有一个这样进入的请求 :

/book/save?title=The%20Stand&author=Stephen%20King

titleauthor 请求参数将会自动 被设置到domain类上. 假如,你需要在一个已存在的实体上执行数据绑定,那么你可以使用 properties 属性:

def save = {
  def b = Book.get(params.id)
  b.properties = params
  b.save()
}

这个和使用隐式构造函数是完全一样的效果.

数据绑定和单向关联

如果你有one-to-onemany-to-one 关联,你同样可以使用Grails的数据绑定能力更新这些关系. 例如,如果你有这样的请求参数:

/book/save?author.id=20

Grails 将自动检测请求参数上的 .id 后缀,并查找给定id的 Author实体 ,随后像这样进行数据绑定:

def b = new Book(params)

属于绑定与Many-ended关联

假如你有一个 one-to-many 或 many-to-many关联,依赖关联类型,有不同的方法用于数据绑定.

假如你有一个以Set基本的关联 (默认用于hasMany) ,那么简单的方式加入一个关联是简单的传送一组标识符列表. 考虑下面 <g:select> 示例的用法:

<g:select name="books"
          from="${Book.list()}"
          size="5" multiple="yes" optionKey="id"
          value="${author?.books}" />

它生成一个选择框 ,允许你选择多个值. 在这种情况下,如果你提交表单,Grails将自动利用来自选择框的标识符加入 books关联.

不过, 假如,你有一个更新关联对象的属性的方案,这个方法将不会工作. 作为替代,你需要使用下标操作符:

<g:textField name="books[0].title" value="the Stand" />
<g:textField name="books[1].title" value="the Shining" />

不过, 如果,你想要更新在相同顺序中的渲染标记,对于基于Set的关联是危险的 . 这是因为Set 没有顺序的概念, 所以,你引用的books0books1 不能确保关联的顺序在服务器端的正确性,除非你自己应用明确排序 .

如果你使用基于List的关联就不会存在这个问题 , 因为List 拥有确定的顺序并使供索引来引用. 这同样适用于基于 Map的关联.

还要注意 ,假如你绑定的关联长度为,你引用的元素超出了关联的长度:

<g:textField name="books[0].title" value="the Stand" />
<g:textField name="books[1].title" value="the Shining" />
<g:textField name="books[2].title" value="Red Madder" />

随后, Grails 在确定的位置自动为你创建一个实体. 如果你"跳过"中间的某些元素 :

<g:textField name="books[0].title" value="the Stand" />
<g:textField name="books[1].title" value="the Shining" />
<g:textField name="books[5].title" value="Red Madder" />

随后,Grails会自动在中间创建实体 . 例如,如果关联的长度为2,在上面的情况下,Grails会创建4 个额外的实体.

数据绑定多个domain类

它可能通过来自 params对象来绑定多个domain对象.

例如,你有一个进入的请求:

/book/save?book.title=The%20Stand&author.name=Stephen%20King

需要注意的是,上面请求不同之处在于拥有 author.前缀或 book前缀. 这是用于分离哪个参数属于哪个类型. Grails的params对象就像 多维 hash ,你可以索引来分离唯一的参数子集来绑定.

def b = new Book(params['book'])

注意,我们如何使用book.title的第一圆点前面的前缀参数来隔离唯一的参数绑定. 我们同样可以这样来使用Authordomain类 :

def a = new Author(params['author'])

数据绑定与类型转换错误

有时,当执行数据绑定时,它可能不会将一种指定的String转换为指定的目标类型. 你会得到类型转换错误. Grails 会保留类型转换错误在Grails domain 类的 errors 属性中 . 例如这里:

class Book {
    …
    URL publisherURL
}

这里,我们有一个Bookdomain 类 ,它使用Java的java.net.URL来表示 URLs.现在,我们有一个像这样的请求参数:

/book/save?publisherURL=a-bad-url

在这种情况下,它不可能将 字符串a-bad-url 绑定到 publisherURL 属性上,一个类型匹配错误会发生. 你可以像这样来检查它们:

def b = new Book(params)

if(b.hasErrors()) { println "The value ${b.errors.getFieldError('publisherURL').rejectedValue} is not a valid URL!" }

虽然,我们没有覆盖错误代码 (更多信息查看 Validation), 你需要的类型转换错误的错误消息在grails-app/i18n/messages.properties 内. 你可以使用像下面这样的普通错误消息来处理 :

typeMismatch.java.net.URL=The field {0} is not a valid URL

或更具体点:

typeMismatch.Book.publisherURL=The publisher URL you specified is not a valid URL

数据绑定与安全关系

当批量更新来自请求参数的属性,你必须小心,避免客户端绑定恶意数据到 domain 类上, 并持久化到数据库.你可以使用下标操作符限制捆绑在某个给定domain类的属性:

def p = Person.get(1)

p.properties['firstName','lastName'] = params

在这种情况下,只有firstNamelastName 属性将被捆绑.

另一种实现这个的方式是使用 domain类作为数据绑定目标,你可以使用Command Objects. 另外还有一个更加灵活bindData 方法.

The bindData 方法具有同样的数据绑定能力,但,是对于任意的对象:

def p = new Person()
bindData(p, params)

当然,bindData 方法同样允许你排除某些你不想更新的参数:

def p = new Person()
bindData(p, params, [exclude:'dateOfBirth'])

或只包含某些属性:

def p = new Person()
bindData(p, params, [include:['firstName','lastName]])

6.1.7 XML与JSON响应

使用render方法输出XML

Grails支持一些不同的方法来产生XML和JSON响应. 第一个是通过 render 方法.

render方法可以传递一个代码块来实现XML中的标记生成器:

def list = {
	def results = Book.list()
	render(contentType:"text/xml") {
		books {
			for(b in results) {
				book(title:b.title)
			}
		}	
	}
}

这段代码的结果会像这样:

<books>
	  <book title="The Stand" />
	  <book title="The Shining" />	
</books>

注意,你必须小心的是避免使用标记生成器带来的命名冲突. 例如,这段代码会产生一个错误:

def list = {
	def books = Book.list()  // naming conflict here
	render(contentType:"text/xml") {
		books {
			for(b in results) {
				book(title:b.title)
			}
		}	
	}
}

问题在于,这里的局部变量 books, Groovy会把它当做一个方法来调用.

使用render方法输出JSON

render 同样被用于输出JSON:

def list = {
	def results = Book.list()
	render(contentType:"text/json") {
		books {
			for(b in results) {
				book(title:b.title)
			}
		}	
	}
}

在这种情况下,结果大致相同:

[
	{title:"The Stand"}, 
	{title:"The Shining"}
]

同样的命名冲突危险适用于JSON生成器.

自动XML列集(Marshalling)

(译者注:在此附上对于列集(Marshalling)解释:对函数参数进行打包处理得过程,因为指针等数据,必须通过一定得转换,才能被另一组件所理解。可以说列集(Marshalling)是一种数据格式的转换方法。)

Grails同样支持自动列集(Marshalling) domain类 为XML,通过特定的转换器.

首先,导入grails.converters 类包到你的controller(控制器):

import grails.converters.*

现在,你可以使用下列高度易读的语法来自动转换domain类为XML:

render Book.list() as XML

输出结果看上去像下面这样:

<?xml version="1.0" encoding="ISO-8859-1"?>
<list>
  <book id="1">
    <author>Stephen King</author>
    <title>The Stand</title>
  </book>
  <book id="2">
    <author>Stephen King</author>
    <title>The Shining</title>
  </book>
</list>

一个使用转换器的替代方法是使用Grails的codecs 特性. codecs特性提供了 encodeAsXMLencodeAsJSON方法:

def xml = Book.list().encodeAsXML()
render xml

更多的XML 列集(Marshalling)信息见REST

自动JSON列集(Marshalling)

Grails同样支持自动列集(Marshalling)为JSON通过同样的机制. 简单替代XMLJSON:

render Book.list() as JSON

输出结果看上去像下面这样:

[
	{"id":1,
	 "class":"Book",
	 "author":"Stephen King",
	 "title":"The Stand"},
	{"id":2,
	 "class":"Book",
	 "author":"Stephen King",
	 "releaseDate":new Date(1194127343161),
	 "title":"The Shining"}
 ]

作为替代,你可以使用encodeAsJSON达到相同的效果.

6.1.8 文件上传

文件上传程序

Grails通过Spring的 MultipartHttpServletRequest 接口来支持文件上传. 上传文件的第一步就是像下面这样创建一个multipart form:

Upload Form: <br />
	<g:form action="upload" method="post" enctype="multipart/form-data">
		<input type="file" name="myFile" />
		<input type="submit" />
	</g:form>

这里有一些方法来处理文件上传. 第一种方法是直接与Spring的MultipartFile 实体:

def upload = {
    def f = request.getFile('myFile')
    if(!f.empty) {
      f.transferTo( new File('/some/local/dir/myfile.txt') )
      response.sendError(200,'Done');
    }    
    else {
       flash.message = 'file cannot be empty'
       render(view:'uploadForm')
    }
}

这显然很方便,通过MultipartFile 接口可以直接获得一个InputStream,用来转移到其他目的地和操纵文件等等.

通过数据绑定上传文件

文件上传同样可以通过数据绑定来完成。例如,假定你有一个像下面这样Image domain类:

class Image {
   byte[] myFile
}

现在,假如你创建一个image并像下面这个示例一样传入 params对象,Grails将自动把文件的内容当作一个byte绑定到myFile属性:

def img = new Image(params)

它同样可以设置文件的内容为一个string,通过改变image的myFile属性类型为一个String类型:

class Image {
   String myFile
}

6.1.9 命令对象

Grails控制器(controllers)支持命令对象概念.一个命令对象类似于Struts中的一个formbean,它们在当你想要写入属性子集来更新一个domain类情形时是非常有用的 . 或在没有domain类需要的相互作用,但必须使用 data bindingvalidation 特性 .

声明命令对象

命令对象通常作为一个控制器直接声明在控制器(controller)类定义下的同一个源文件中. 例如:

class UserController {
	…
}
class LoginCommand {
   String username
   String password
   static constraints = {
           username(blank:false, minSize:6)
           password(blank:false, minSize:6)
   }
}

上面的示例证明你可以提供 约束给命令对象,就象你在domain 类中的用法一样.

使用命令对象

为了使用命令对象,控制器可以随意指定任何数目的命令对象参数。必须提供参数的类型以至于Grails能知道什么样的对象被创建,写入和验证.

在控制器(controller)的操作被执行之前,Grails将自动创建一个命令对象类的实体,用相应名字的请求参数写入到命令对象属性, 并且命令对象将被验证,例如:

class LoginController {
  def login = { LoginCommand cmd ->
         if(cmd.hasErrors()) {
                redirect(action:'loginForm')
         }
         else {
            // do something else
        }
  }
}

命令对象与依赖注入

命令对象可以参与依赖注入。这有利于一些定制的验证逻辑与Grails的services的结合。 :

class LoginCommand {
    def loginService

String username String password

static constraints = { username(validator: { val, obj -> obj.loginService.canLogin(obj.username, obj.password) }) } }

上面示例,命令对象与一个来自Spring的 ApplicationContext注入名字bean结合.

6.1.10 处理重复的表单提交

Grails 已经内置支持处理重复表单提交, 通过使用"同步令牌模式". 首先,你得在 form 标签上定义一个令牌:

<g:form useToken="true" ...>

随后,在你的控制器(controller)代码中使用 withForm 方法来处理有效和无效的请求:

withForm {
   // good request
}.invalidToken {
   // bad request
}

如果你只提供了 withForm 方法而没有链接 invalidToken 方法,那么,默认情况下,Grails 将会无效的令牌存储在flash.invalidToken变量中 并导航请求回到原始页面. 这可以在页面中检测到:

<g:if test="${flash.invalidToken}">
  Don't click the button twice!
</g:if>

withForm 标签利用了session ,因此,如果在群集中使用,要求会话密切关联.

6.2 Groovy Server Pages

Groovy Servers Pages (或者简写为 GSP)Grails的视图技术。它被设计成像ASP和JSP这样被使用者熟悉的技术,但更加灵活和直观.

GSP存在于Grails的grails-app/views目录中,他们通常会自动渲染(通过规约),或者像这样通过render方法:

render(view:"index")

GSP使典型的混合标记和GSP标签,辅助页面渲染.

虽然,它可能会在你的GSP页面中内置Groovy逻辑,Although it is possible to have Groovy logic embedded in your GSP and doing this will be covered in this document the practice is strongly discouraged. Mixing mark-up and code is a bad thing and most GSP pages contain no code and needn't do so.

一个GPS通常拥有一个"model",它是变量集被用于视图渲染。通过一个控制器model被传递到GSP视图。例如,考虑下列控制器的操作:

def show = {
	[book: Book.get(params.id)]
}

这个操作将查找一个book实体,并创建一个包含关键字为Book的model,这个关键字可在随后的GSP视图中应用:

<%=book.title%>

6.2.1 GSP基础

在下一节,我们将通过GSP基础知识让你知道它能做什么。首先,我们将涵盖基础语法,对于JSP和ASP用户是非常熟悉的.

GSP支持使用<% %>来嵌入Groovy代码(这是不推荐的):

<html>
   <body>
     <% out << "Hello GSP!" %>
   </body>
</html>

同样,你可以使用<%= %>语法来输出值:

<html>
   <body>
     <%="Hello GSP!" %>
   </body>
</html>

GSP同样支持服务器端JSP样式注释,像下列示例显示的这样:

<html>
   <body>
	 <%-- This is my comment --%>
     <%="Hello GSP!" %>
   </body>
</html>

6.2.1.1 变量与作用域

<% %> 中你当然可以声明变量:

<% now = new Date() %>

然后,在页面中的之后部分可以重复使用 :

<%=now%>

然而, 在GSP中存在着一些预先定义的变量,包括:

6.2.1.2 逻辑和迭代

使用 <% %> 语法,你当然可以使用这样的语法进行嵌套循环等等操作:

<html>
   <body>
      <% [1,2,3,4].each { num -> %>
         <p><%="Hello ${num}!" %></p>
      <%}%>
   </body>
</html>

同样可以分支逻辑:

<html>
   <body>
      <% if(params.hello == 'true' )%>	
      <%="Hello!"%>
      <% else %>
      <%="Goodbye!"%>
   </body>
</html>

6.2.1.3 页面指令

GSP同样支持少许的JSP样式页面指令.

import指令允许在页面中导入类。然而,它却很少被使用,因为Groovy缺省导入和GSP 标签:

<%@ page import="java.awt.*" %>

GSP同样支持contentType@ 指令:

<%@ page contentType="text/json" %>

contentType@指令允许GSP使用其他的格式来渲染.

6.2.1.4 表达式

尽管GSP也支持 <%= %> 语法,而且很早就介绍过,但在实际当中却很少应用,因为此用法主要是为ASP和 、JSP开发者所保留的。 而GSP的表达式跟JSP EL表达式很相似的,跟Groovy GString的 ${expr} 用法也很像:

<html>
  <body>
    Hello ${params.name}
  </body>
</html>

尽管如此,跟JSP EL不同的是, 你可以在${..}括号中使用Groovy表达式.${..}中的变量缺省情况下是转义, 因此变量的任何HTML字符串内容被直接输出到页面,要减少这种Cross-site-scripting (XSS)攻击的风险, 你可以设置grails-app/conf/Config.groovy中的grails.views.default.codec为HTML转化方式:

grails.views.default.codec='html'

其他可选的值是'none' (缺省值)和'base64'.

6.2.2 GSP标签

现在,JSP遗传下来的缺点已经被取消,下面的章节将涵盖GSP的内置标签,它是定义GSP页面最有利的方法.

标签库 部分涵盖怎么添加你自己的定制标签库.

所有GSP内置标签以前缀g:开始。 不像JSP,你不需要指定任何标签库的导入.假如,一个标签以g:开始,它被自动认为是一个GSP标签.一个GPS标签的示例看起来像这样:

<g:example />

GSP标签同样可以拥有主体,像这样:

<g:example>
   Hello world
</g:example>

表达式被传递给GSP标签属性,假如没有使用表达式,将被认为是一个String值:

<g:example attr="${new Date()}">
   Hello world
</g:example>

Maps同样能被传递给GSP标签属性,通常使用一个命名参数样式语法:

<g:example attr="${new Date()}" attr2="[one:1, two:2, three:3]">
   Hello world
</g:example>

注意,对于String类型属性值,你必须使用单引号:

<g:example attr="${new Date()}" attr2="[one:'one', two:'two']">
   Hello world
</g:example>

在介绍完基本的语法之后,下面我们来讲解Grails中默认提供的标签.

6.2.2.1 变量与作用域

变量可以在GSP中使用 set 标签来定义:

<g:set var="now" value="${new Date()}" />

这里, 我们给GSP表达式结果赋予了一个名为now的变量 (简单的构建一个新的 java.util.Date 实体)。 你也可以在<g:set>主体中定义一个变量:

<g:set var="myHTML">
   Some re-usable code on: ${new Date()}
</g:set>

变量同样可以被放置于下列的范围内:

选择变量被放入的范围可以使用scope属性:

<g:set var="now" value="${new Date()}" scope="request" />

6.2.2.2 逻辑和迭代

GSP同样支持迭代逻辑标签,逻辑上通过使用 if , , elseelseif 来支持典型的分支情形。 :

<g:if test="${session.role == 'admin'}">
   <%-- show administrative functions --%>
</g:if>
<g:else>
   <%-- show basic functions --%>
</g:else>

GSP用each eachwhile标签来处理迭代:

<g:each in="${[1,2,3]}" var="num">
   <p>Number ${num}</p>
</g:each>

<g:set var="num" value="${1}" /> <g:while test="${num < 5 }"> <p>Number ${num++}</p> </g:while>

6.2.2.3 搜索和过滤

假如你拥有对象集合,你经常需要使用一些方法来排序和过滤他们。 GSP支持 findAllgrep 来做这些工作:

Stephen King's Books:
<g:findAll in="${books}" expr="it.author == 'Stephen King'">
     <p>Title: ${it.title}</p>
</g:findAll>

expr属性包含了一个Groovy表达式,它可以被当作一个过滤器来使用。 谈到过滤器,grep标签通过类来完成与过滤器类似的工作:

<g:grep in="${books}" filter="NonFictionBooks.class">
     <p>Title: ${it.title}</p>
</g:grep>

或者使用一个正则表达式:

<g:grep in="${books.title}" filter="~/.*?Groovy.*?/">
     <p>Title: ${it}</p>
</g:grep>

上面的示例同样有趣,因为它使用了GPath.Groovy的GPath等同与XPath语言。实际上books集合是books集合的实体。 不过,假设每个books拥有一个title,你可以使用表达式books.title来获取Book titles的list!

6.2.2.4 链接和资源

GSP还拥有特有的标签来帮助你管理连接到控制器和操作. link 标签允许你指定控制器和操作配对的名字,并基于 URL Mappings 映射来自动完成连接。即使你去改变!一些 link 的示例如下:

<g:link action="show" id="1">Book 1</g:link>
<g:link action="show" id="${currentBook.id}">${currentBook.name}</g:link>
<g:link controller="book">Book Home</g:link>
<g:link controller="book" action="list">Book List</g:link>
<g:link url="[action:'list',controller:'book']">Book List</g:link>
<g:link action="list" params="[sort:'title',order:'asc',author:currentBook.author]">
     Book List
</g:link>

6.2.2.5 表单和字段

表单基础

GSP支持许多不同标签来帮助处理HTML表单和字段,最基础的是form标签, form标签是一个控制器/操作所理解的正规的HTML表单标签版本。 url属性允许你指定映射到哪个控制器和操作:

<g:form name="myForm" url="[controller:'book',action:'list']">...</g:form>

我们创建个名为myForm的表单,它被提交到 BookControllerlist操作。除此之外,适用于所有不同的HTML属性.

表单字段

同构造简单的表单一样,GSP支持如下不同字段类型的定制:

上面的每一个都允许GSP表达式作为值:

<g:textField name="myField" value="${myValue}" />

GSP同样包含上面标签的扩张助手版本, 比如radioGroup (创建一组radio标签), localeSelect, currencySelecttimeZoneSelect(选择各自的地区区域, 货币 和时间区域). .

多样的提交按钮

处理多样的提交按钮这样由来已久的问题,同样可以通过Grails的actionSubmit 标签优雅的处理。它就像一个正规提交,但是,允许你指定一个可选的操作来提交:

<g:actionSubmit value="Some update label" action="update" />

6.2.2.6 标签作为方法调用

GSP标签和其他标签技术一个主要不同在于,来自 controllers(控制器) , 标签库 或者GSP 视图中的GPS标签可以被当作任意的正规标签或者当作方法被调用.

来自GSPs中的标签当作方法调用

当作为方法被调用时,标签的返回值被当作String实体直接被写入响应中。 因此,示例中的createLinkTo能等同的看做方法调用:

Static Resource: ${createLinkTo(dir:"images", file:"logo.jpg")}

当你必须在一个属性内使用一个标签时是特别有用的:

<img src="${createLinkTo(dir:'images', file:'logo.jpg')}" />

I在视图技术中,标签内嵌套标签的特性是不被支持的,这样变得十分混乱,往往使得像Dreamweaver这样WYSWIG的工具产生不利的效果以至于在渲染标签时:

<img src="<g:createLinkTo dir="images" file="logo.jpg" />" />

来自控制器(Controllers)和标签库的标签作为方法调用

你同样可以调用来自控制器和标签库的标签。标签可以不需要内部默认的g:namespace前缀来调用,并返回String结果:

def imageLocation = createLinkTo(dir:"images", file:"logo.jpg")

然而,你同样可以用命名空间前缀来避免命名冲突:

def imageLocation = g.createLinkTo(dir:"images", file:"logo.jpg")

假如你有一个自定义命名空间,,你可以使用它的前缀来替换(例如,使用 FCK Editor plugin:

def editor = fck.editor()

6.2.3 视图(View)与模板(Templates)

除了views之外, Grails还有模板的概念. 模板有利于分隔出你的视图在可维护的块中,并与 Layouts 结合提供一个高度可重用机制来构建视图.

模板基础

Grails使用在一个视图名字前放置一个下划线来标识为一个模板的规约。 例如,你可能有个位于grails-app/views/book/_bookTemplate.gsp的模板处理渲染Books:

<div class="book" id="${book?.id}">
   <div>Title: ${book?.title}</div>
   <div>Author: ${book?.author?.name}</div>
</div>

为了渲染来自grails-app/views/book视图中的一个模板,你可以使用render标签:

<g:render template="bookTemplate" model="[book:myBook]" />

注意,我们是怎么样使用render标签的model属性来使用传入的一个model。 假如,你有多个Book实体,你同样可以使用render标签为每个Book渲染模板 :

<g:render template="bookTemplate" var="book" collection="${bookList}" />

共享模板

在早先的示例中,我们有一个特定于BookController模板,它的视图位于grails-app/views/book.然而,你可能想横跨你的应用来共享模板。

在这种情况下,你可以把他们放置于grails-app/views视图根目录或者位于这个位置的任何子目录,然后在模板属性在模板名字之前使用一个 /来指明相对模板路径 .例如,假如你有个名为grails-app/views/shared/_mySharedTemplate.gsp模板, 你可以像下面这样引用它:

<g:render template="/shared/mySharedTemplate" />

你也可以使用这个技术从任何视图或控制器(Controllers)来引用任何目录下的模板:

<g:render template="/book/bookTemplate" model="[book:myBook]" />

模板命名空间

因为模板使用如此频繁,它有一个模板命名空间, 名为tmpl, 他使模板的使用变得容易. 考虑下面例子的使用模式:

<g:render template="bookTemplate" model="[book:myBook]" />

这个想下面这样通过tmpl命名空间表示 :

<tmpl:bookTemplate book="${myBook}" />

在控制器(Controllers)和标签库中的模板

你同样可以使用控制器 render方法渲染模板控制器中,它对Ajax引用很有用:

def show = {
    def b = Book.get(params.id)
	render(template:"bookTemplate", model:[book:b])
}

在控制器(controller)中的render 方法最普通的行为是直接写入响应。 假如,你需要获得模板作为一个String的结果作为替代,你可以使用render标签:

def show = {
    def b = Book.get(params.id)
	String content = g.render(template:"bookTemplate", model:[book:b])
	render content
}

注意, g. 命名空间的用法,它告诉Grails我们想使用标签作为方法调用来代替render 方法.

6.2.4 使用Sitemesh布局

创建布局

Grails利用了Sitemesh,一个装饰引擎,来支持视图布局。 布局位于grails-app/views/layouts 目录中。一个典型的布局如下:

<html>
      <head>
          <title><g:layoutTitle default="An example decorator" /></title>
          <g:layoutHead />
      </head>
      <body onload="${pageProperty(name:'body.onload')}">
            <div class="menu"><!--my common menu goes here--></menu>
                 <div class="body">
                      <g:layoutBody />
                 </div>
            </div>
      </body>
</html>

关键的元素是layoutHead, layoutTitlelayoutBody标签的用法,这里是他们所做的:

上面的示例同样表明pageProperty tag 可被用于检查和返回目标页面的外观.

启用布局

这里有一些方法来启用一个布局.简单的在视图中添加meta标签:

<html>
    <head>
	    <title>An Example Page</title>
        <meta name="layout" content="main"></meta>
    </head>
    <body>This is my content!</body>
</html>

在这种情况下,一个名为grails-app/views/layouts/main.gsp将被用于布局这个页面。假如,我们使用来自早前部分的布局,输出看上去像下列这样:

<html>
      <head>
          <title>An Example Page</title>
      </head>
      <body onload="">
        <div class="menu"><!--my common menu goes here--></div>
                 <div class="body">
					This is my content!
                 </div>
      </body>
</html>

在控制器(Controller)中指定布局

另一种用于指定布局的方式是通过在控制器(controller)中为 "layout"属性指定布局的名字, 假如你有个这样的控制器(controller):

class BookController {
    static layout = 'customer'

def list = { … } }

你可以创建一个grails-app/views/layouts/customer.gsp布局,应用于所有 BookController中委派的视图 . "layout"属性值可能包含相对于grails-app/views/layouts/目录的路径结构 . 例如:

class BookController {
    static layout = 'custom/customer'

def list = { … } }

视图的显然可通过 grails-app/views/layouts/custom/customer.gsp 模板.

布局规约

第二种关联布局的方法是使用"布局规约",假如你有个这样的控制器:

class BookController {
    def list = {  … }
}

你可以创建一个名为grails-app/views/layouts/book.gsp的布局,根据规约,它将被应用于BookController的所有视图中。

换句话说,你可以创建一个名为grails-app/views/layouts/book/list.gsp的布局,它将只被应用于BookController中的list操作,

如果你同时使用了以上提到的两种布局的话,那当list操作被执行的时候,那么操作将根据优先级的顺序来使用布局.

内联布局

通过applyLayout标签Grails同样支持Sitemesh的内联布局概念。 applyLayout标签可以被用于应用一个布局到一个模板,URL或者内容的任意部分。 事实上,通过"decorating"你的模板允许你更进一步的积木化你的视图结构.

一些使用示例如下:

<g:applyLayout name="myLayout" template="bookTemplate" collection="${books}" />

<g:applyLayout name="myLayout" url="http://www.google.com" />

<g:applyLayout name="myLayout"> The content to apply a layout to </g:applyLayout>

Server-Side包含

applyLayout标签被以用于引用布局外内容 applying layouts to , 假如你想简单的在当前页面包含外部内容,你可以使用 include:

<g:include controller="book" action="list"></g:include>

你甚至可以结合 include 标签和 applyLayout 标签 来添加灵活性:

<g:applyLayout name="myLayout">
   <g:include controller="book" action="list"></g:include>
</g:applyLayout>

最后,你也可以在控制器(controller)或标签库把include标签作为方法调用 :

def content = include(controller:"book", action:"list")

最后的内容有 include标签的返回值提供 .

6.2.5 Sitemesh内容块

虽然,这对于装饰全部页面非常有用,有时,你需要装饰站点的部分独自的页面。为了实现这个可以使用内容块. 在开始时,你需要使用 <content>标签分隔装饰页面 :

<content tag="navbar">
… draw the navbar here…
</content>
<content tag="header">
… draw the header here…
</content>
<content tag="footer">
… draw the footer here…
</content>
<content tag="body">
… draw the body here…
</content>

随后,在布局内部,你可以引用这些组件并为每个引用单个布局:

<html>
	<body>
		<div id="header">
			<g:applyLayout name="headerLayout"><g:pageProperty name="page.header"></g:applyLayout>
		</div>
		<div id="nav">
			<g:applyLayout name="navLayout"><g:pageProperty name="page.navbar"></g:applyLayout>
		</div>
		<div id="body">
			<g:applyLayout name="bodyLayout"><g:pageProperty name="page.body"></g:applyLayout>
		</div>
		<div id="footer">
			<g:applyLayout name="footerLayout"><g:pageProperty name="page.footer"></g:applyLayout>			
		</div>
	</body>
</html>

6.3 标签库

Java Server Pages JSP) 一样,GSP支持定制tag库的概念.不同于JSP,Grails标签库机制是简单的,优雅的,在运行时完全可重载的.

创建一个标签库是相当简单的,创建一个以规约TagLib结尾的一个Groovy类,并把它放置于grails-app/taglib目录里:

class SimpleTagLib {

}

现在,为了创建一个标签,简单的创建属性并赋值一个带有两个参数的代码块:标签属性和主体内容:

class SimpleTagLib {
	def simple = { attrs, body ->

} }

attrs属性是一个简单的标签属性map,同时body是另一可调用的代码块,它返回主体内容:

class SimpleTagLib {
	def emoticon = { attrs, body ->
	   out << body() << attrs.happy == 'true' ? " :-)" : " :-("	
    }
}

正如以上所显示的,这里有个隐式的out变量,它引用了输出Writer,可以用来附加内容到响应中. 然后,你可以在你的GSP内简单的引用这个标签而不需要任何导入:

<g:emoticon happy="true">Hi John</g:emoticon>

6.3.1 变量与作用域

在标签库的作用域中包含了一些预先定义好的变量:

6.3.2 简单标签

作为演示,早先的示例只不过是写了个没有主体只有输出内容的简单标签。另一个示例是一个 dateFormat 样式标签:

def dateFormat = { attrs, body ->
	out << new java.text.SimpleDateFormat(attrs.format).format(attrs.date)
}

上面使用了Java的SimpleDateFormat类来格式化一个date,然后把它写入响应。随后,这个标签能像下列这样在GSP中使用:

<g:dateFormat format="dd-MM-yyyy" date="${new Date()}" />

有时。你需要用简单的标签把HTML标签(mark-up)写入到响应中。一个方法是直接嵌套内容:

def formatBook = { attrs, body ->
    out << "<div id="${attrs.book.id}">"	
    out << "Title : ${attrs.book.title}"	
	out << "</div>"
}

虽然,这个方法可能很诱人,但不是非常的简洁。一个更好的方法将是复用render标签:

def formatBook = { attrs, body ->
    out << render(template:"bookTemplate", model:[book:attrs.book])	
}

然后,这个单独的GSP模板做了实际的渲染工作.

6.3.3 逻辑标签

一旦一组条件满足,你同样可以在标签的主体中创建仅仅用来输出的逻辑标签。一个这样的例子可能是一组安全标签:

def isAdmin = { attrs, body ->
     def user = attrs['user']
     if(user != null && checkUserPrivs(user)) {
           out << body()
     }
}

上面的标签检查用户是否为管理人员,如果他/她有正确设置的访问权限只输出主体内容:

<g:isAdmin user="${myUser}">
    // some restricted content
</g:isAdmin>

6.3.4 迭代标签

迭代标签同样普通,因为你可以多次调用主体:

def repeat = { attrs, body ->
    attrs.times?.toInteger().times { num ->
        out << body(num)
    }
}

在这个示例中,我们检查一个times属性,假如存在,把它转换为一个数字,然后使用Groovy的times方法:

<g:repeat times="3">
<p>Repeat this 3 times! Current repeat = ${it}</p>
</g:repeat>

注意,我们是怎么样在这个示例中使用隐式的it变量来引用当前的数字。这个过程是因为在迭代内部我们调用了传递进入当前值的主体:

out << body(num)

那个值然后被作为默认的it变量传递给标签,然而,假如你有嵌套标签便会导致冲突,因此,你将可能替换主体使用的变量名:

def repeat = { attrs, body ->
	def var = attrs.var ? attrs.var : "num"
    attrs.times?.toInteger().times { num ->
        out << body((var):num)
    }
}

这里,我们检查是否存在一个var属性,如果存在的话,将其作为body调用的参数:

out << body((var):num)

注意,变量名围绕的圆括号的使用.假如你省略,Groovy会认为你使用了一个String关键字,而不是引用这个变量它自己.

现在,我们可以改变这个标签的使用方法,如下:

<g:repeat times="3" var="j">
<p>Repeat this 3 times! Current repeat = ${j}</p>
</g:repeat>

注意,我们是怎么样使用var属性来定义j变量名,随后,我们可能在标签主体类引用这个变量.

6.3.5 标签命名空间

默认情况下,标签被添加到默认的Grails命名空间,并在GSP页面中和 g: 前缀一起使用。然而,你可以指定一个不同的命名空间,通过在你的 TagLib 类中添加一个静态属性:

class SimpleTagLib {
    static namespace = "my"

def example = { attrs -> … } }

这里,我们指定了一个命名空间my,因此,稍后在GPS页面中标签库中的标签引用会像这样:

<my:example name="..." />

前缀和静态的命名空间属性值一样.命名空间对于插件特别有用.

命名空间内的标签可以作为方法调用,使用命名空间作为前缀来执行方法调用:

out << my.example(name:"foo")

可用于GSP,控制器或者标签库.

6.3.6 使用JSP 标签库

除了GSP提供的简单标签库机制, 你也可以在GSP中使用JSP标签.通过taglib指令来简单声明你需要的JSP标签:

<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

随后,你可以想任何其他标签一样来使用它:

<fmt:formatNumber value="${10}" pattern=".00"/>

额外的好处是,你可以把JSP标签当方法调用 :

${fmt.formatNumber(value:10, pattern:".00")}

6.4 URL映射

到目前为止,贯穿整个文档用于URLs的规约默认为 /controller/action/id . 然而,这个规约不是硬性的写入Grails中,实际上,它是通过一个位于 grails-app/conf/UrlMappings.groovy 的URL映射类所控制.

UrlMappings类包含一个名为mappings单一属性,并被赋予一个代码块:

class UrlMappings {
    static mappings = {
    }	
}

6.4.1 映射到控制器和操作

为了创建简单的映射,只需简单的使用相对URL作为方法名,并指定控制器和操作的命名参数来映射:

"/product"(controller:"product", action:"list")

在这种情况下,我们建立URL/productProductControllerlist操作的映射。 你当然可以省略操作定义,来映射控制器默认的操作:

"/product"(controller:"product")

一个可选的语法是把在块中被赋值的控制器和操作传递给方法:

"/product" {
	controller = "product"
	action = "list"
}

你使用哪一个句法很大程度上依赖于个人偏好.

6.4.2 嵌入式变量

简单变量

早前的部分说明,怎样使用具体的"标记"来映射普通的URLs。在URL映射里讲过,标记是在每个斜线(/)字符之间的顺序字符。 一个具体的标记就像/product这样被良好定义。 然而,很多情况下,标记的值直到运行时才知道是什么。在这种情况下,你可以在URL中使用变量占位符,例如:

static mappings = {
  "/product/$id"(controller:"product")
}

在这种情况下,通过嵌入一个$id变量作为第2个标记,Grails将自动映射第2个标记到一个名为id的参数(通过params对象得到). 例如给定的URL/product/MacBook,下面的代码将渲染"MacBook"到响应中:

class ProductController {
     def index = { render params.id }
}

当然你可以构建更多复杂的映射示例。例如传统的blog URL格式将被映射成下面这样:

static mappings = {
   "/$blog/$year/$month/$day/$id"(controller:"blog", action:"show")
}

上面的映射允许你这样:

/graemerocher/2007/01/10/my_funky_blog_entry

在URL里单独的标记将再次被映射到带有year, month, day, id等等可用值的 params 对象中.

动态控制器(Controller)和操作(Action)名

变量同样可以被用于动态构造控制器和操作名。实际上,默认的Grails URL映射使用这样的技术:

static mappings = {
    "/$controller/$action?/$id?"()
}

这里,控制器(controller)名,操作(action)名和id名,隐式的从嵌入在URL中的controller, actionid中获得:

static mappings = {
    "/$controller" {
	   action = { params.goHere }
    }
}

可选的变量

默认映射另一个特性就是能够在一个变量的末尾附加一个? ,使它成为一个可选的标记。这个技术更进一步的示例能够运用于blog URL映射,使它具有更灵活性的连接 :

static mappings = {
   "/$blog/$year?/$month?/$day?/$id?"(controller:"blog", action:"show")
}

下列URLs的所有映射将与放置于params对象中的唯一关联的参数匹配:

/graemerocher/2007/01/10/my_funky_blog_entry
/graemerocher/2007/01/10
/graemerocher/2007/01
/graemerocher/2007
/graemerocher

任意变量

你同样可以传递来自于URL映射的任意参数给控制器,把他们设置在块内传递给这个映射:

"/holiday/win" {
     id = "Marrakech"
     year = 2007
}

在这个params对象得到的这个变量将被传递给这个控制器.

动态解析变量

硬编码任意变量是有用的,但是,有时你需要基于运行时因素来计算变量名。这个同样可能通过给变量名分配一个块:

"/holiday/win" {
     id = { params.id } 
     isEligible = { session.user != null } // must be logged in
}

上述情况,当URL实际被匹配,块中的代码将被解析,因此可以被用于结合所有种类的逻辑处理.

6.4.3 映射到视图

如果你想决定一个URL一个view,而无需涉及一个控制器或者操作,你也可以这样做。 例如,如果你想映射根URL / 到一个位于 grails-app/views/index.gsp 的GSP,你可以这样使用:

static mappings = {
      "/"(view:"/index")  // map the root URL
}

换句话说,假如你需要一个具体给定的控制器(Controller)中的一个视图,你可以这样使用:

static mappings = {
   "/help"(controller:"site",view:"help") // to a view for a controller
}

6.4.4 映射到响应代码

Grails同样允许你映射一个HTTP响应代码到控制器,操作或视图。所有你需要做的是使用一个方法名来匹配你所感兴趣的响应代码:

static mappings = {
   "500"(controller:"errors", action:"serverError")
   "404"(controller:"errors", action:"notFound")
   "403"(controller:"errors", action:"forbidden")
}

或者换句话说,假如你只不过想提供定制的错误页面:

static mappings = {
   "500"(view:"/errors/serverError")
   "404"(view:"/errors/notFound")
   "403"(view:"/errors/forbidden")
}

6.4.5 映射到HTTP方法

URL映射同样可以配置成基于HTTP 方法 (GET, POST, PUT or DELETE)的map。这个对于RESTful APIs和基于HTTP方法的约束映射是非常有用的.

作为一个示例,下面的映射为ProductControllerURL提供一个RESTful API URL映射:

static mappings = {
   "/product/$id"(controller:"product"){
       action = [GET:"show", PUT:"update", DELETE:"delete", POST:"save"]
   }	
}

6.4.6 映射通配符

Grails的URL映射机制同样支持通配符映射。例如,考虑下面的映射:

static mappings = {
	"/images/*.jpg"(controller:"image")
}

这个映射将匹配所有images路径下像/image/logo.jpg这样的jpg。当然你可以通过一个变量来达到同样的效果:

static mappings = {
	"/images/$name.jpg"(controller:"image")
}

然而,你可以使用双通配符来匹配多于一个层次之外的:

static mappings = {
	"/images/**.jpg"(controller:"image")
}

这样的话,这个映射将不但匹配/image/logo.jpg而且匹配/image/other/logo.jpg。更好的是你可以使用一个双通配符变量:

static mappings = {
	// will match /image/logo.jpg and /image/other/logo.jpg 
	"/images/$name**.jpg"(controller:"image")
}

这样的话,它将储存路径,从params 对象获得命名参数里的name通配符 :

def name = params.name
println name // prints "logo" or "other/logo"

如果你使用通配符URL mappings,那么你可以排除某些来自Grails的URL mapping进程 URIs. 实现这个你可以在UrlMappings.groovy类中设置excludes :

class UrlMappings = {
	static excludes = ["/images/**", "/css/**"]
	static mappings = {
		…
	}
}

这样,Grails不为匹配任何以 /images/css开头的URLs.

6.4.7 自动重写链接

URL映射另一个重要的特性是自动定制 link 标签的行为。以便改变这个映射而不需要改变所有的连接.

通过一个URL重写技术做到这点,从URL映射反转连接设计:

static mappings = {
   "/$blog/$year?/$month?/$day?/$id?"(controller:"blog", action:"show")
}

如果,你像下列一样使用连接标签:

<g:link controller="blog" action="show" params="[blog:'fred', year:2007]">My Blog</g:link>
<g:link controller="blog" action="show" params="[blog:'fred', year:2007, month:10]">My Blog - October 2007 Posts</g:link>

Grails将自动重写URL通过适当的格式:

<a href="/fred/2007">My Blog</a>
<a href="/fred/2007/10">My Blog - October 2007 Posts</a>

6.4.8 应用约束

URL映射同样支持Grails统一 验证规约 机制, 它允许你更进一步"约束"一个URL是怎么被匹配的。例如,如果我们回到早前的blog示例代码,这个映射当前看上去会像这样 :

static mappings = {
   "/$blog/$year?/$month?/$day?/$id?"(controller:"blog", action:"show")
}

允许URLs像这样:

/graemerocher/2007/01/10/my_funky_blog_entry

不过,它也允许这样:

/graemerocher/not_a_year/not_a_month/not_a_day/my_funky_blog_entry

当它强迫你在控制器代码中做一些聪明的语法分析时会有问题。幸运的是,URL映射能进一步的约束验证URL标记:

"/$blog/$year?/$month?/$day?/$id?" {
     controller = "blog"
     action = "show"
     constraints {
          year(matches:/d{4}/)
          month(matches:/d{2}/)
          day(matches:/d{2}/)
     }
}

在这种情况下,约束能确保 year, monthday参数匹配一个具体有效的模式,从而在稍后来减轻你的负担 .

6.5 Web流(Flow)

概述

Grails基于Spring Web Flow项目来支持创建Web流(Flow)。一个Web流(Flow)就是一个会话,它跨越多个请求并保持着流(Flow)作用域的状态。 一个Web流(Flow)也定义了开始和结束状态。 .

Web流(Flow)无需HTTP session,但作为替代,它将状态存储在序列化表单中,然后通过Grails来回传递的request参数中的执行流中的key进行还原。 这相比其他使用HttpSession来保存状态的应用来说更具有可扩展性,尤其是在内存和集群方面.

Web流(Flow)本质是高级的状态机,它管理着一个状态到下个状态"流"的执行。因为为你管理着状态,你就勿需担心用户在进入多步骤流(Flow)的操作(action) ,因为Web流(Flow)已经帮你管理了,因此Web流(Flow)在处理象网上购物、宾馆预定及任何多页面的工作流的应用具有出乎意料的简单.

创建流

创建一个流(Flow)只需简单的创建一个普通的Grails控制器(controller),然后添加一个以规约Flow结尾的操作。例如:

class BookController {
   def index = {
      redirect(action:"shoppingCart")
   }
   def shoppingCartFlow = {
        …
   }
}

注意,当重定向或引用流(Flow)时,可以把它当做一个操作(action)而省略掉流(Flow)前缀。换句话说, 上面流的操作(action)名为shoppingCart.

6.5.1 开始与结束状态

如上所述,一个流(Flow)定义了开始和结束状态。一个开始状态是当用户第一次开始一个会话(或流(Flow))。Grails的开始流(Flow)是第一个带有代码块的方法调用。例如:

class BookController {
   …
   def shoppingCartFlow = {
       showCart {
           on("checkout").to "enterPersonalDetails"           
           on("continueShopping").to "displayCatalogue"
       }
       …
       displayCatalogue {
            redirect(controller:"catalogue", action:"show")
       }
       displayInvoice()
   }
}

这里,showCart节点是这个流的开始状态。 因为这个showCart状态并没有定义一个操作(action)或重定向,只被视为是一个视图状态。 通过规约,指向grails-app/views/book/shoppingCart/showCart.gsp视图 .

注意,这不像正规的控制器(controller)操作(action),这个视图被存储于与其流名字匹配的grails-app/views/book/shoppingCart目录中 .

shoppingCart流(Flow)也可能拥有两个结束状态。第一个是displayCatalogue, 执行外部重定向到另一个控制器(controller)和操作(action),从而结束流(Flow)。第二个是displayInvoice是一个最终状态,因为它根本没有任何事件, 只是简单的渲染一个名为grails-app/views/book/shoppingCart/displayInvoice.gsp的视图,并在同一时间终止流(Flow).

一旦一个流(Flow)结束,它只能从开始状态重新开始,对于showCart不会来自任何其他状态.

6.5.2 操作(Action)状态和视图状态

视图状态

视图状态没有定义操作(action)redirect。下面是一个视图状态示例:

enterPersonalDetails {
   on("submit").to "enterShipping"
   on("return").to "showCart"
}

它默认查找一个名为grails-app/views/book/shoppingCart/enterPersonalDetails.gsp的视图。 注意,enterPersonalDetails定义了两个事件:submitreturn。视图负责触发(triggering)这些事件。假如你想让视图用于渲染,使用render方法来完成:

enterPersonalDetails {
   render(view:"enterDetailsView")
   on("submit").to "enterShipping"
   on("return").to "showCart"
}

现在,它将查找grails-app/views/book/shoppingCart/enterDetailsView.gsp。假如使用共享视图,视图参数以/ 开头:

enterPersonalDetails {
   render(view:"/shared/enterDetailsView")
   on("submit").to "enterShipping"
   on("return").to "showCart"
}

现在,它将查找 grails-app/views/shared/enterDetailsView.gsp

操作(Action)状态

操作(Action)状态只执行代码但不渲染任何视图。操作(Action)的结果被用于控制流(Flow)的切换。为了创建一个操作操作(Action)状态,你需要定义一个被用于执行的操作。 这通过调用action方法实现并传递它的一个代码块来执行:

listBooks {
   action { 
	  [ bookList:Book.list() ]
   }
   on("success").to "showCatalogue"
   on(Exception).to "handleError"
}

正如你看到的,一个操作看上去非常类似于一个控制器(controller)操作(action),实际上,假如你需要可以重用控制器(controller)操作(action)。 假如这个操作没有错误成功返回,success事件将被触发。 在这里,返回一个map,它被视为"model"看待,并自动放置于流(flow)作用域.

此外,在上面的示例中也使用了下面的异常处理程序来处理错误:

on(Exception).to "handleError"

这使当流(Flow)切换到状态出现异常的情况下调用handleError.

你可以编写与流(flow)请求上下文相互作用更复杂的操作(action):

processPurchaseOrder  {
     action {
         def a =  flow.address
         def p = flow.person
         def pd = flow.paymentDetails
         def cartItems = flow.cartItems
         flow.clear()

def o = new Order(person:p, shippingAddress:a, paymentDetails:pd) o.invoiceNumber = new Random().nextInt(9999999) cartItems.each { o.addToItems(it) } o.save() [order:o] } on("error").to "confirmPurchase" on(Exception).to "confirmPurchase" on("success").to "displayInvoice" }

这是一个更复杂的操作(action),用于收集所有来自流(flow)作用域信息,并创建一个Order对象。 然后,把Order作为模型返回。这里值得注意的重要事情是与请求上下文和 "流(flow)作用域"的相互作用.

切换操作

另一种形式的操作(action)被称之为切换操作(action)。一旦一个event被触发,切换操作优先于状态切换被直接执行。普通的切换操作如下 :

enterPersonalDetails {
   on("submit") {
       log.trace "Going to enter shipping"	
   }.to "enterShipping"
   on("return").to "showCart"
}

注意,我们是怎样传递一个代码块给submit事件,它只是简单的记录这个切换。切换状态对于数据绑定与验证是非常有用的,将在后面部分涵盖.

6.5.3 流(Flow)执行事件

为了执行流流从一个状态到下一个状态的 切换 ,你需要一些方法来触发一个 event ,指出流流下一步该做什么。事件的触发可以来自于任何视图状态和操作状态.

来自于一个视图状态的触发事件

正如之前所讨论的,在早前代码列表内流的开始状态可能处理两个事件。一个checkout和一个continueShopping事件:

def shoppingCartFlow = {
    showCart {
        on("checkout").to "enterPersonalDetails"           
        on("continueShopping").to "displayCatalogue"
    }
    …
}

因为showCart事件是一个视图状态,它会渲染 grails-app/book/shoppingCart/showCart.gsp视图. 在视图内部,你需要拥有一个用于触发流(Flow)执行的组件.在一个表单中,这可使用submitButton标签:

<g:form action="shoppingCart">
    <g:submitButton name="continueShopping" value="Continue Shopping"></g:submitButton>
    <g:submitButton name="checkout" value="Checkout"></g:submitButton>
</g:form>

这个表格必须提交返回shoppingCart流流。每个submitButton标签的name属性标示哪个事件将被触发。 假如,你没有表格,你同样可以用link标签来触发一个事件,如下:

<g:link action="shoppingCart" event="checkout" />

来自于一个操作(Action)的触发事件

为了触发来自于一个操作(action)的一个事件,你需要调用一个方法。例如,这里内置的error()success()方法。 下面的示例在切换操作中验证失败后触发error()事件:

enterPersonalDetails {
   on("submit") {
         def p = new Person(params)
         flow.person = p
         if(!p.validate())return error()
   }.to "enterShipping"
   on("return").to "showCart"
}

在这种情况下,因为错误,切换操作将使流回到enterPersonalDetails状态.

有了一种操作状态,你也能触发事件来重定向流:

shippingNeeded {
   action {
       if(params.shippingRequired) yes()
       else no()
   }
   on("yes").to "enterShipping"
   on("no").to "enterPayment"
}

6.5.4 流(Flow)的作用域

作用域基础

在以前的示例中,你可能会注意到我们在“流作用域(flow scope)”中已经使用了一个特殊的流(flow)来存储对象,在Grails中共有5种不同的作用域可供你使用 :

Grails的service类可以自动的定位web flow的作用域,详细请参考Services .

此外从一个action中返回的模型映射(model map)将会自动设置成flow范围,比如在一个转换(transition)的操作中,你可以象下面这样使用流(flow)作用域 :

enterPersonalDetails {
   on("submit") {
         [person:new Person(params)]
   }.to "enterShipping"
   on("return").to "showCart"
}

要知道每一个状态总是创建一个新的请求,因此保存在request作用域中的对象在其随后的视图状态中不再有效,要想在状态之间传递对象 ,需要使用除了request之外的其他作用域。此外还有注意,Web流(Flow)将 :

  1. 在状态转换的时候,会将对象从flash作用域移动到request作用域;
  2. 在渲染以前,将会合并flow和conversation作用域的对象到视图模型中(因此你不需要在视图中引用这些对象的时候,再包含一个作用域前缀了).

流(Flow)的作用域和序列化

当你将对象放到 flash, flowconversation 作用域中的时候,要确保对象已经实现了java.io.Serializable接口,否则将会报错。 这在domain类尤为显著,因为领域类通常在视图中渲染的时候被放到相应的作用域中,比如下面的领域类示例 :

class Book {
	String title
}

为了能够让Book类的实例可以放到流(flow)作用域中,你需要修改如下:

class Book implements Serializable {
	String title
}

这也会影响到领域类中的关联和闭包,看下面示例:

class Book implements Serializable {
	String title
	Author author
}

此处如果Author关联没有实现Serializable,你同样也会得到一个错误。 此外在GORM events中使用的闭包比如onLoad, onSave等也会受到影响, 下例的领域类如果放到flow作用域中,将会产生一个错误:

class Book implements Serializable {
	String title
	def onLoad = {
		println "I'm loading"
	}
}

这是因为onLoad事件中的代码块必能被序列化,要想避免这种错误,需要将所有的事件声明为transient :

class Book implements Serializable {
	String title
	transient onLoad = {
		println "I'm loading"
	}
}

6.5.5 数据绑定和验证

开始和结束状态 部分, 开始状态的第一个示例触发一个切换到 enterPersonalDetails 状态。这个状态渲染一个视图,并等待用户键入请求信息 :

enterPersonalDetails {
   on("submit").to "enterShipping"
   on("return").to "showCart"
}

一个视图包含一个带有两个提交按钮的表格,每个都触发提交事件或返回事件:

<g:form action="shoppingCart">
    <!-- Other fields -->
    <g:submitButton name="submit" value="Continue"></g:submitButton>
    <g:submitButton name="return" value="Back"></g:submitButton>
</g:form>

然而,怎么样捕捉被表格提交的信息?为了捕捉表格信息我们可以使用流切换操作:

enterPersonalDetails {
   on("submit") {
         flow.person = new Person(params)
         !flow.person.validate() ? error() : success()
   }.to "enterShipping"
   on("return").to "showCart"
}

注意,我们是怎样执行来自请求参数的绑定,把Person实体放置于流(flow)作用域中。同样有趣的是,我们执行 验证,并在验证失败是调用error()方法 .这个流(flow)的动机即停止切换并返回 enterPersonalDetails 视图,因此,有效的项通过user进入,否则,切换继续并转到enterShipping state.

就像正规操作(action),流(flow)操作(action)也支持 命令对象概念,通过定义闭包的第一个参数 :

enterPersonalDetails {
   on("submit") { PersonDetailsCommand cmd ->	     
          flow.personDetails = cmd
         !flow.personDetails.validate() ? error() : success()
   }.to "enterShipping"
   on("return").to "showCart"
}

6.5.6 子流程和会话

Grails的Web Flow集成同样支持子流(subflows)。一个子流在一个流中就像一个流。拿下面search流作为示例:

def searchFlow = {
            displaySearchForm {
                on("submit").to "executeSearch"
            }
            executeSearch {
                action {
                    [results:searchService.executeSearch(params.q)]
                }
                on("success").to "displayResults"
                on("error").to "displaySearchForm"
            }
            displayResults {
                on("searchDeeper").to "extendedSearch"
                on("searchAgain").to "displaySearchForm"
            }
            extendedSearch {
                subflow(extendedSearchFlow)   // <--- extended search subflow
                on("moreResults").to "displayMoreResults"
                on("noResults").to "displayNoMoreResults"
            }
            displayMoreResults()
            displayNoMoreResults()
}

它在extendedSearch状态中引用了一个子流。子流完全是另一个流 :

def extendedSearchFlow = {
       startExtendedSearch {
           on("findMore").to "searchMore"
           on("searchAgain").to "noResults"
       }
       searchMore {
           action {
              def results = searchService.deepSearch(ctx.conversation.query)
              if(!results)return error()
              conversation.extendedResults = results
           }
           on("success").to "moreResults"
           on("error").to "noResults"
       }
       moreResults()
       noResults()
}

注意,它是怎样把extendedResults放置于会话范围的。这个范围不同于流范围,因为它允许你横跨整个会话而不只是这个流。 同样注意结束状态(每个子流的 moreResultsnoResults在主流中触发事件 :

extendedSearch {
         subflow(extendedSearchFlow)   // <--- extended search subflow
         on("moreResults").to "displayMoreResults"
         on("noResults").to "displayNoMoreResults"
}

6.6 过滤器

尽管Grails支持良好的细粒度控制器(controller),但只对少数控制器(controller)的应用时非常有用,当管理大型应用时就会变得很困难。 另一方面,过滤器能横跨一群控制器(controller),一个URI空间或一个具体的操作(action)。 过滤器对插件更容易并能保证彻底的分离主要控制器(controller)逻辑,有利于所有像安全,日志等等这样的横切关注点 .

6.6.1 应用过滤器

为了创建一个过滤器,可在 grails-app/conf 下创建一个以规约 Filters 结尾的类。在这个类中,定义一个名为 filters 的代码块,它包含了过滤器的定义 :

class ExampleFilters {
   def filters = {
        // your filters here
   }
}

每个在filters块中定义的过滤器(Filters)拥有一个名字和一个 作用域。名字是方法的名字,作用域使用命名参数来定义。例如,假如你需要定义一个应用于所有控制器(controller)和操作(action)的过滤器(Filters)可以使用通配符 :

sampleFilter(controller:'*', action:'*') {
  // interceptor definitions
}

过滤器的作用域可以是下面之一:

过滤器的一些示例包括:

all(controller:'*', action:'*') {

}

justBook(controller:'book', action:'*') {

}

someURIs(uri:'/book/**') {

}

allURIs(uri:'/**') {

}

另外,这个次序决定了你所定义的过滤器的执行次序.

6.6.2 过滤器(Filters)类型

在过滤器的主体内,你可以定义下列过滤器(Filters)的拦截器类型之一:

例如,为实现普通身份验证,可以定义如下过滤器(Filters):

class SecurityFilters {
   def filters = {
       loginCheck(controller:'*', action:'*') {
           before = {
              if(!session.user && !actionName.equals('login')) {
                  redirect(action:'login')
                  return false
               }
           }

} } }

这里的loginCheck过滤器(Filters)使用一个before拦截器来执行代码块, 检查是否一个用户在session内,假如不是,重定向到login操作(action)。注意,如何返回false确保操作(action)本身不被执行 .

6.6.3 变量与作用域

过滤器支持所有在 控制器(controllers)标签库 中可用的属性,附加application context :

不过,过滤器只支持用于控制器(controller)和标签库方法的子集 。这些包括:

6.7 Ajax

Ajax代表异步Javascript与XML,它是转向富web应用程序的驱动力. 这些类型的应用程序,通常更适合于像RubyGroovy语言所写的敏捷,动态框架,Grails通过它的Ajax标签库提供支持构建Ajax应用程序. 它们完整的列表可以参看标签库参考.

6.7.1 用Prototype实现Ajax

Grails默认装载Prototype 库,但通过Plug-in 系统,可以提供对Dojo, Yahoo UIGoogle Web Toolkit 等其他框架的支持.

这部分涵盖Grails对Prototype的支持。你需要在页面的<head>标签内添加这样一行就可以开始了 :

<g:javascript library="prototype" />

这里使用javascript标签自动插入Prototype正确位置的引用。假如你同样需要Scriptaculous ,你可以如下这样做为替换 :

<g:javascript library="scriptaculous" />

6.7.1.1 远程链接

远程内容可以通过多种方式加载,最常使用的方法是通过 remoteLink 标签。 这个标签允许创建的HTML锚标记执行一个异步请求,并在一个元素中随意设置响应。用这个简单方式创建的远程链接就像这样 :

<g:remoteLink action="delete" id="1">Delete Book</g:remoteLink>

上面的连接发送一个异步请求给当前id为1的控制器的delete操作 .

6.7.1.2 内容更新

这真是太棒了,但通常你想提供一些事情发生的反馈信息给用户:

def delete = {
      def b = Book.get( params.id )
      b.delete()
      render "Book ${b.id} was deleted"
}

GSP代码:

<div id="message"></div>
<g:remoteLink action="delete" id="1" update="message">Delete Book</g:remoteLink>

上面的示例将调用这个操作并设置message div的响应内容为"Book 1 was deleted"。这通过标签上的update属性来完成,它同样可以获取一个map来指出在失败时什么被更新 :

<div id="message"></div>
<div id="error"></div>
<g:remoteLink action="delete" id="1"
              update="[success:'message',failure:'error']">Delete Book</g:remoteLink>

这里,error div在请求失败时被更新.

6.7.1.3 远程表单提交

,一个HTML form也可以异步被提交通过以下两种方式之一。第一个,使用 formRemote 标签,它和 remoteLink 标签有类似的属性 :

<g:formRemote url="[controller:'book',action:'delete']" update="[success:'message',failure:'error']">
       <input type="hidden" name="id" value="1" />
       <input type="submit" value="Delete Book!" />
</g:formRemote >

或者作为选择可以使用submitToRemote来创建一个提交按钮。它允许一些按钮远程提交而一些不依赖操作 :

<form action="delete">
       <input type="hidden" name="id" value="1" />
       <g:submitToRemote action="delete" update="[success:'message',failure:'error']" />
</form>

6.7.1.4 Ajax事件

某些事件的发生会调用特定的javascript。所有以"on"开头的事件,在适当的时候允许你反馈信息给用户,或采取其他行为:

<g:remoteLink action="show" 
              id="1" 
              update="success" 
              onLoading="showProgress()" 
              onComplete="hideProgress()">Show Book 1</g:remoteLink>

上述代码将执行"showProgress()"函数来显示一个进度条或者其他适当的展示,其他的事件还包括 :

假如你需要引用XmlHttpRequest对象,你可以使用隐式的event参数e获取它 :

<g:javascript>
   function fireMe(e) {
	   alert("XmlHttpRequest = " + e)
   }
}
</g:javascript>
<g:remoteLink action="example" 
              update="success" 
              onSuccess="fireMe(e)">Ajax Link</g:remoteLink>

6.7.2 用Dojo实现Ajax

Grails把 Dojo 作为一种外部插件来支持Grails的特性。在终端窗口,进入你项目的根目录键入下列命令来安装插件 :

grails install-plugin dojo

将下载Dojo最新的支持版本,并安装到你的Grails项目中。完成上面的步骤后,你可以在你页面的顶部添加下列引用:

<g:javascript library="dojo" />

现在,所有像remoteLink, formRemotesubmitToRemote标签都可以和Dojo进行远程处理工作 .

6.7.3 用GWT实现Ajax

Grails同样支持 Google Web Toolkit 特性,插件的全面 文档 可以在Grails wiki中找到 .

6.7.4 服务端的Ajax

虽然Ajax特性X为XML,但通常可以分解成许多不同方式执行Ajax:

Ajax部分中的更多的示例涵盖了内容为中心的 Ajax在什么地方更新页面,但同样你可能使用数据为中心的Ajax或脚本为中心的 Ajax。这份指南涵盖了不同风格的Ajax .

内容为中心的Ajax

作为概括,内容为中心的 Ajax涉及从服务器端发送一些HTML返回和通过使用render方法来渲染模板 :

def showBook = {
	def b = Book.get(params.id)

render(template:"bookTemplate", model:[book:b]) }

在客户端调用这个会涉及到remoteLink标签的使用 :

<g:remoteLink action="showBook" id="${book.id}" update="book${book.id}">Update Book</g:remoteLink>
<div id="book${book.id}">
   <!--existing book mark-up -->
</div>

数据为中心的Ajax与JSON

数据为中心的Ajax通常涉及到客户端响应的赋值和编程化更新。Grails中的JSON响应,通常使用Grails的JSON marshaling能力 :

import grails.converters.*

def showBook = { def b = Book.get(params.id)

render b as JSON }

然后,在客户端使用一个Ajax事件处理解析这个进入的JSON请求:

<g:javascript>
function updateBook(e) {
	var book = eval("("+e.responseText+")") // evaluate the JSON
	$("book"+book.id+"_title").innerHTML = book.title
}
<g:javascript>
<g:remoteLink action="test" update="foo" onSuccess="updateBook(e)">Update Book</g:remoteLink>
<g:set var="bookId">book${book.id}</g:set>
<div id="${bookId}">
	<div id="${bookId}_title">The Stand</div>
</div>

数据为中心的Ajax与XML

在服务器端使用XML同样普遍:

import grails.converters.*

def showBook = { def b = Book.get(params.id)

render b as XML }

不过,因为涉及到DOM,客户变得更复杂:

<g:javascript>
function updateBook(e) {
	var xml = e.responseXML
	var id = xml.getElementsByTagName("book").getAttribute("id")
	$("book"+id+"_title")=xml.getElementsByTagName("title")[0].textContent
}
<g:javascript>
<g:remoteLink action="test" update="foo" onSuccess="updateBook(e)">Update Book</g:remoteLink>
<g:set var="bookId">book${book.id}</g:set>
<div id="${bookId}">
	<div id="${bookId}_title">The Stand</div>
</div>

脚本为中心的Ajax与JavaScript

脚本为中心的 Ajax涉及实际返回的Javascript在客户端被赋值。这样的示例见下表:

def showBook = {
	def b = Book.get(params.id)

response.contentType = "text/javascript" String title = b.title.encodeAsJavascript() render "$('book${b.id}_title')='${title}'" }

要记住的重要事情是,设置contentTypetext/javascript。如果在客户端使用Prototype,由于设置了contentType, 返回的Javascript将自动被赋值.

很明显,在这种情况下,它是关键性的,你有一个一致的client-sideAPI, 因此,你不想客户端的改变破坏服务器端。这就是Rails有些像RJS的理由之一。 虽然,Grails当前没有像RJS的一个特性,但动态Dynamic JavaScript Plug-in插件提供了类似的能力.

6.8 内容协商

Grails已经内置支持内容协商通过使用任意HTTP Accept 报头 ,一种明确格式请求参数或URI映射的扩展.

配置Mime类型

在你开始处理内容协商之前,你必须告诉Grails希望支持什么样的内容类型。 默认情况下,grails-app/conf/Config.groovy内使用 grails.mime.types设置来配置若干不同的内容类型 :

grails.mime.types = [ xml: ['text/xml', 'application/xml'],
                      text: 'text-plain',
                      js: 'text/javascript',
                      rss: 'application/rss+xml',
                      atom: 'application/atom+xml',
                      css: 'text/css',
                      cvs: 'text/csv',
                      all: '*/*',
                      json: 'text/json',
                      html: ['text/html','application/xhtml+xml']
                    ]

上面的小块配置,允许Grails检查把包含 'text/xml' 或 'application/xml' 媒体类型的一个请求的格式只当做 'xml'看待,你可以添加你自己的类型通过简单的添加条目到 map中.

内容协商使用Accept报头

每个进入的HTTP请求都有个指定的Accept报头,它定义了什么样的媒体类型(或 mime 类型)客户端能"接受"。在老式浏览器中通常是 :

*/*

这意味着任何事物.不过在新生浏览器中,所有东西一起像这样发送更有用(一个FirefoxAccept报头示例) :

text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5

Grails解析这个进入的格式,并添加一个propertyrequest对象,用于描叙首选的请求格式。对于上述示例下列的断言会通过 :

assert 'html' == request.format

为什么?这个text/html媒体类型拥有最高"质量"等级0.9,因此,具有最高优先权。如前所述,假如你有一老式浏览器结果会稍微不同 :

assert 'all' == request.format

在这种情况下,'all'可能的格式会被客户端接受。为了处理来自控制器(Controllers)不同类型的请求,你可以使用withFormat方法,它的行为被当作switch表达式 :

import grails.converters.*

class BookController { def books def list = { this.books = Book.list() withFormat { html bookList:books js { render "alert('hello')" } xml { render books as XML } } } }

当Grails只执行 html()调用并且首选的格式是html时会发生什么。它只是让 Grails寻找每个名为grails-app/views/books/list.html.gspgrails-app/views/books/list.gsp的视图。 如果格式是xml,那么,闭包会被调用,XML响应会被渲染 .

我们怎样处理'all'格式?只需在withFormat代码块中简单指定content-types,以便,无论你想要的哪个都会被首先执行。 因此,在上面示例中的"all" 将触发html处理 .

当使用withFormat时确保它在控制器(controller)操作(action)中最后一个被调用,因为withFormat方法的返回值用来决定操作(action)下一步做什么.

内容协商与格式化请求参数

如果请求头的内容跟你的不一致,通过指定一个format的请求参数覆盖这个格式 :

/book/list?format=xml

你同样可以在URL Mappings定义中定义这个参数 :

"/book/list"(controller:"book", action:"list") {
	format = "xml"
}

内容协商与URI扩展

Grails同样可以通过URI扩展支持内容协商。例如,给定下列URI:

/book/list.xml

Grails将剔除扩展并映射到/book/list作为替代,同时,基于这个扩展把内容格式化为xml。 这个行为是默认允许的,那么,假如你希望关闭它, 你必须把grails-app/conf/Config.groovy下的grails.mime.file.extensions属性设置为false :

grails.mime.file.extensions = false

测试内容协商

为了在一个综合测试中测试内容协商(参见 测试部分)你可以操作每个进入的请求包头 :

void testJavascriptOutput() {
	def controller = new TestController()
	controller.request.addHeader "Accept", "text/javascript, text/html, application/xml, text/xml, */*"

controller.testAction() assertEquals "alert('hello')", controller.response.contentAsString }

或者你可以设置格式化参数来实现类似的效果:

void testJavascriptOutput() {
	def controller = new TestController()
	controller.params.format = 'js'

controller.testAction() assertEquals "alert('hello')", controller.response.contentAsString }

7. 验证

Grails的验证功能基于Spring's Validator API和数据绑定功能。 不过,Grails利用这些特性,通过它的"constraints(约束)"机制, 提供了一个统一的定义验证约束方式。

Grails中的Constraints(约束)是用声明式指定效验规则的方式。常用于domain 类,不过 URL MappingsCommand 对象同样支持Constraints(约束)。

7.1 声明 Constraints(约束)

在一个domain类中,constraints(约束) 是通过给constraints属性赋值代码块的形式来定义的:

class User {
    String login
    String password
    String email
    Integer age

static constraints = { … } }

然后,通过与属性名匹配的方法调用,并结合命名参数来指定constraints(约束)

class User {
    ...

static constraints = { login(size:5..15, blank:false, unique:true) password(size:5..15, blank:false) email(email:true, blank:false) age(min:18, nullable:false) } }

在这个示例中,我们声明login属性必须在5-15个字符长度之间,不能为空,并且必须是唯一的。 我们还可以为password,emailage属性运用其他的约束.

现有约束的完整参考可以在参考指南中找到

7.2 验证约束

验证基础

你可以在任何实体中调用validate方法验证domain:

def user =  new User(params)

if(user.validate()) { // do something with user } else { user.errors.allErrors.each { println it } }

domain的errors 属性是一个Spring Errors 接口实例. Errors 提供用于导航验证错误以及取回原始值的方法。

验证阶段

Grails中本质上有2个验证阶段,第一个阶段是 data binding ,当你把请求参数绑定到实体上发生,例如:

def user = new User(params)

这时,因为类型转换(如String转换为Dates),在errors属性可能已经出现错误。 你可以检查它们并通过使用ErrorsAPI获得原始输入值:

if(user.hasErrors()) {
	if(user.errors.hasFieldErrors("login")) {
		println user.errors.getFieldError("login").rejectedValue
	}
}

验证的第2阶段发生在当你调用 validatesave时。 这时,Grails将会验证你在constraints 定义的约束值。比如,默认的持久方法save会在执行之前调用 validate 。因此,允许你像下面这样编码:

if(user.save()) {
    return user
}
else {
    user.errors.allErrors.each {
        println it
    }
}

7.3 客户端验证

显示错误

通常,当你得到一个验证错误后,你会重定向回页面渲染这些错误。这时,你就需要一些渲染错误的方法。 Grails 提供了一组丰富的标签,处理错误渲染。 如果只是想简单的渲染错误列表,可以使用renderErrors:

<g:renderErrors bean="${user}" />

假如,你需要更多的控制,可以使用 hasErrorseachError:

<g:hasErrors bean="${user}">
  <ul>
   <g:eachError var="err" bean="${user}">
       <li>${err}</li> 
   </g:eachError>
  </ul>
</g:hasErrors>

高亮错误

当一个字段存在错误的输入时,一个红色块和一些提示符,对于高亮错误非常有用。 这时通过把 hasErrors 当做方法调用来做到。 比如:

<div class='value ${hasErrors(bean:user,field:'login','errors')}'>
   <input type="text" name="login" value="${fieldValue(bean:user,field:'login')}"/>
</div>

上面的代码做了什么?它会检查userlogin字段是否存在任何错误,如果存在,就给 div添加一个errors CSS class,这样就可以让你使用CSS来高亮div.

取回输入值

任何错误实际上是Spring中FieldError 类的实体,它会在内部保存原始输入值。通过fieldValue标签获取错误对象的原始输入值:

<input type="text" name="login" value="${fieldValue(bean:user,field:'login')}"/>

这段代码会查看,在User bean中是否存在一个 FieldError,如果是,就获取 login 字段的原始输入值。

7.4 验证与国际化

Grails中另一个关于errors值得注意的重要事情是:错误消息的显示,无需任何的硬编码。Spring中的 FieldError类使用Grails的 i18n 支持,基本上解决了来自消息绑定的消息。

规约与 Message 编码

编码它们自己通过规约来规定,例如,考虑早前看到约束:

package com.mycompany.myapp

class User { ...

static constraints = { login(size:5..15, blank:false, unique:true) password(size:5..15, blank:false) email(email:true, blank:false) age(min:18, nullable:false) } }

如果 blank约束不合法 , Grails将在form中通过规约查找消息编码:

[Class Name].[Property Name].[Constraint Code]

在这种情况下, blank约束就会是 user.login.blank 因此,你需要在 grails-app/i18n/messages.properties文件中包含下面这样的消息:

user.login.blank=Your login name must be specified!

它会查找带package或不带package的类名,带有package的将会优先。作为示例,com.mycompany.myapp.User.login.blank 将先于 user.login.blank使用。 当你domain 类的消息编码与插件产生冲突时,可以这样使用。

每个规约的编码参考可以参考参考指南 constraints refer to the reference guide for each constraint.

显示消息

当你使用message标签时, renderErrors 标签将自动处理查找消息。 不过,假如你想获得更多的渲染控制,你需要自己编写代码:

<g:hasErrors bean="${user}">
  <ul>
   <g:eachError var="err" bean="${user}">
       <li><g:message error="${err}" /></li> 
   </g:eachError>
  </ul>
</g:hasErrors>

这个示例中, eachError 标签主体内,我们使用了 message 标签的 error 参数来读取给定的错误 。

7.5 验证非Domain 与命令行对象

Domain 类command objects(命令行对象)默认支持验证。其他类也可以在类中定义静态constraints 属性获得验证(如上所述),然后把它们告诉框架。 当应用程序在框架中注册验证类 是非常重要的 。简单定义constraints属性是不够的。

Validateable 注解

任何定义了静态constraints属性和标有 @Validateable 接口的类可以在框架中被验证。 考虑下面示例:

// src/groovy/com/mycompany/myapp/User.groovy
package com.mycompany.myapp

import org.codehaus.groovy.grails.validation.Validateable

@Validateable class User { ...

static constraints = { login(size:5..15, blank:false, unique:true) password(size:5..15, blank:false) email(email:true, blank:false) age(min:18, nullable:false) } }

默认情况下,框架会搜索所有带有@Validateable 注解的类。 你可以指定框架只搜索某个packages,通过给Config.groovy中的 grails.validateable.packages属性赋值一列字符串。

// grails-app/conf/Config.groovy

...

grails.validateable.packages = ['com.mycompany.dto', 'com.mycompany.util']

...

假如grails.validateable.packages属性被设置,框架只会在这些packages中搜索 (和它们的子 packages) 标有@Validateable的类.

注册Validateable类

假如一个类没有被标为@Validateable,它仍然可能通过框架验证。 那就是必须在类中定义静态constraints 属性 (如上所述) , 然后,通过在Config.groovy中为grails.validateable.classes属性设置值来告诉框架。

// grails-app/conf/Config.groovy

...

grails.validateable.classes = [com.mycompany.myapp.User, com.mycompany.dto.Account]

...

8. Service 层

除了 Web 层 之外, Grails 还定义了service 层的概念。Grails 团队不赞成在controllers中嵌入核心的应用程序逻辑,因为这样并没有提升重用和清楚的关注点分离。

Grails中的Services在应用程序中被视为放置多数逻辑的地方 。 从controllers脱离,负责处理通过重定向的请求流等等。

创建Service

你可以在终端窗口的项目根目录下运行 create-service 创建Service:

grails create-service simple

上面的示例将在grails-app/services/SimpleService.groovy位置创建一个Service。 service的名字按规约以 Service结尾。 除此之外,service就是个普通的Groovy类:

class SimpleService {	
}

8.1 声明式事务处理

Services一般涉及协调 domain 类之间的逻辑, , 因此常常涉及大范围的持久化操作。 因为services性质,它们常常需要事物状态。你可以使用 withTransaction方法来编程事物,不过,这是重复性的,没有充分利用Spring强大的潜在事物抽象

Services允许启用事物,本质上是以声明的方式来声明service中的所有方法必须用于事物。默认情况下,所有services的事物都是可用的——禁用它,只需设置 transactional属性为 false:

class CountryService {
    static transactional = false
}

你也可以默认设置这个属性为 true 在以后改变它,或者清楚的表明这个服务是有意地用于事物。

警告: 依赖注入唯一 声明事物工作的方式。你不能使用new操作符,像这样new BookService()获取事物服务

其结果是,所有的方法都被包含在事物中,当方法体中抛出异常时,自动回滚。事物的传播级别被默认设置为 PROPAGATION_REQUIRED.

8.2 服务作用域

默认情况下,存取服务方法是非同步的,所以无法阻止同步执行这些函数。事实上,因为服务是单例的,可以被同时使用,你必须非常小心服务中存储状态。或者采用容易(和更好的)途径并不在y service中存储状态。

你可以通过把service放置于特定的作用域来改变这样的行为:

假如你的service为flash, flowconversation 作用域,它需要实现 java.io.Serializable 并只用于 Web Flow上下文

为了启用一个作用域,在你的类中添加一个静态scope属性,其值为上面所述的作用域之一:

static scope = "flow"

8.3 依赖注入与服务

依赖注入基础

Grails服务的一个重要方面是,有能力利用Spring 框架的依赖注入能力。 Grails支持 "依赖注入通过规约". 换句话说,你可以使用一个属性名表示的一个服务的类名,自动把他们注入到 controllers, tag libraries,等等。

作为示例,给定的服务名为BookService, 如果你像下面这样在controller中放置一个名为bookService 的属性:

class BookController {
   def bookService
   …
}

在这种情况下,Spring 容器将自动注入一个基于它自己配置作用域的服务实体。 所有的依赖注入是通过名字的; Grails 不支持类型注入。 你也可以像下面这样指定类型:

class AuthorService {
	BookService bookService
}

不过, 存在副作用,即在开发模式下BookService的改变会在加载时抛出一个错误。

依赖注入与服务

你可以使用相同的技术在一个服务中注入另一个服务。 如果说,你的AuthorService需要一个 BookService, 可以像下面这样声明 AuthorService:

class AuthorService {
	def bookService
}

依赖注入与Domain类

你甚至可以在domain类中注入服务, 这可以帮助开发出各种丰富的domain:

class Book {	
	…
	def bookService
	def buyBook() {
		bookService.buyBook(this)
	}
}

8.4 Using Services from Java

服务的强大在于它包含了可重用的逻辑,你可以使用来自其他类的服务,包括Java类。这里有一些方法让你重用来自Java的服务。 简单的方法是把你的服务移动到grails-app/services目录下的一个包里。 这是关键步骤,因为你不可能在Java中导入一个默认package 。作为示例, BookService 就是因为上面的原因,在下面Java中不能使用:

class BookService {
	void buyBook(Book book) {
		// logic
	}
}

不过, 把这个类放入一个package中便可修复,, 把这个类移动到 grails-app/services/bookstore子目录, 然后,修改package 声明:

package bookstore
class BookService {
	void buyBook(Book book) {
		// logic
	}
}

package的替代是,定义个需要服务实现的接口:

package bookstore;
interface BookStore {
	void buyBook(Book book);
}

然后,服务:

class BookService implements bookstore.BookStore {
	void buyBook(Book b) {
		// logic
	}
}

后一种方法更熟悉, 在Java端,只需要接口的引用,而不需要实现类。 无论哪种方式,这个练习的目的是,在编译时,让Java能够静态解决类(或接口)的使用。现在,这样便可在 src/java包内创建一个Java类,并提供了一个setter,在Spring中使用bean的类型和它的名字:

package bookstore;
// note: this is Java class
public class BookConsumer {
	private BookStore store;

public void setBookStore(BookStore storeInstance) { this.store = storeInstance; } … }

这样一来,你可以在 grails-app/conf/spring/resources.xml中把这个Java当做Spring bean来配置 (更多详情查看 Grails and Spring):

<bean id="bookConsumer" class="bookstore.BookConsumer">
	<property name="bookStore" ref="bookService" />
</bean>

9. 测试

自动化测试被看成是Grails中一个重要部分,以 Groovy Tests 为基础执行测试。因此,Grails提供了许多方法,使不管是简单的单元测试,还是高难度的方法测试都能更容易执行。这个章节详细描述了Grails给出的各种不同的测试方法。

你要明白的第一件事是,所有create-*命令,实际上Grails最后都会自动帮它们创建集成好的全部测试实例。比如你运行下方的create-controller 命令:

grails create-controller simple

Grails不仅在grails-app/controllers/目录下创建了SimpleController.groovy,而且在test/integration/目录下创建了对它的集成测试实例SimpleControllerTests.groovy。,然而Grails不会在这个测试实例里自动生成逻辑代码,这部分需要你自己写。

当你完成这部分逻辑代码,就可以使用test-app执行所有测试实例:

grails test-app

上面的这个命令将输出如下内容:

-------------------------------------------------------
Running Unit Tests…
Running test FooTests...FAILURE
Unit Tests Completed in 464ms …
-------------------------------------------------------

Tests failed: 0 errors, 1 failures

同时运行结果放在test/reports目录下。你也可以指定名字单独运行一个测试,不需要测试后缀参数:

grails test-app SimpleController

除此之外,你可以以空格隔开同时运行多个实例:

grails test-app SimpleController BookController

9.1 单元测试

单元测试是对单元块代码的测试。换句话说你在分别测试各个方法或代码段时,不需要考虑它们外层周围代码结构。在Grails框架中,你要特别注意单元测试和集成测试之间的一个不同点,因为在单元测试中,Grails在集成测试和测试运行时,不会注入任何被调用的动态方法。

这样做是有意义的,假如你考虑到,在Grails中各个数据库注入的各自方法(通过使用GORM),和潜在使用的Servlet引擎(通过控制器)。例如,你在 BookController调用如下的一个服务应用:

class MyService {
    def otherService

String createSomething() { def stringId = otherService.newIdentifier() def item = new Item(code: stringId, name: "Bangle") item.save() return stringId }

int countItems(String name) { def items = Item.findAllByName(name) return items.size() } }

正如你看到的,这个应用调用了GORM,那么你用在单元测试中怎样处理如上这段代码呢?答案在Grails测试支持类中可以找到。

测试框架

Grails测试插件最核心部分是grails.test.GrailsUnitTestCase类。它是 GroovyTestCase子类,为Grails应用和组件提供测试工具。这个类为模拟特殊类型提供了若干方法,并且提供了按Groovy的MockFor和StubFor方式模拟的支持。

正常来说你在看之前所示的MyService例子和它对另外一个应用服务的依赖,以及例子中使用到的动态域类方法会有一点痛苦。你可以在这个例子中使用元类编程和“map as object”规则,但是很快你会发现使用这些方法会变得很糟糕,那我们要怎么用GrailsUnitTestCase写它的测试呢?

import grails.test.GrailsUnitTestCase

class MyServiceTests extends GrailsUnitTestCase { void testCreateSomething() { // Mock the domain class. def testInstances = [] mockDomain(Item, testInstances)

// Mock the "other" service. String testId = "NH-12347686" def otherControl = mockFor(OtherService) otherControl.demand.newIdentifier(1..1) {-> return testId }

// Initialise the service and test the target method. def testService = new MyService() testService.otherService = otherControl.createMock()

def retval = testService.createSomething()

// Check that the method returns the identifier returned by the // mock "other" service and also that a new Item instance has // been saved. assertEquals testId, retval assertEquals 1, testInstances assertTrue testInstances[0] instanceof Item }

void testCountItems() { // Mock the domain class, this time providing a list of test // Item instances that can be searched. def testInstances = [ new Item(code: "NH-4273997", name: "Laptop"), new Item(code: "EC-4395734", name: "Lamp"), new Item(code: "TF-4927324", name: "Laptop") ] mockDomain(Item, testInstances)

// Initialise the service and test the target method. def testService = new MyService()

assertEquals 2, testService.countItems("Laptop") assertEquals 1, testService.countItems("Lamp") assertEquals 0, testService.countItems("Chair") } }

上面代码出现了一些新的方法,但是一旦对它们进一步解释,你应该很快懂得要使用这些方法是多么容易。首先看testCreateSomething()测试方法里调用的mockDomain()方法,这是GrailsUnitTestCase类提供的其中一个方法:

def testInstances = [] 
mockDomain(Item, testInstances)

这个方法可以给给定的类添加所有共同域的方法(实例和静态),这样任何使用它的代码段都可以把它当作一个真正全面的domain 类。 举个例子,一旦Item类被模拟了,我们就可以在实例它的时候放心得调用save();那么这时,如果我们调用一个被模拟的domain类的这个方法,要怎么做?很简单,在testInstances数组列表里添加新的实例,这个数组被当成参数传进mockDomain()方法。

下面我们将重点讲解mockFor方法:

def otherControl = mockFor(OtherService) 
otherControl.demand.newIdentifier(1..1) {-> return testId }

这段代码功能与Groovy中的MockFor类和StubFor类非常接近,你可以用这个方法模拟任何类。事实上,上述demand语法跟MockFor和StubFor使用的语法一样,所以你在用它时应该不会觉得有差别,当然你要需要频繁注入一个mock实例作为关联,但是你可以简单得调用上述的mock控制对象的createMock()方法,很容易实现。对那些熟悉EasyMock用法的人,它们知道这是otherControl强调了mockFor()返回的对象角色,它是一个控制对象而非mock对象。

testCreateSomething()方法中剩余部分应该很熟悉了,特别是你现在已经知道了save()模拟方法是往testInstances数组里添加实例。我们能确定newIdentifier()模拟方法被调用,因为它返回的值对createSomething()方法返回结果产生直接的影响。那假如情况不是这样呢?我们怎么知道它是否被调用? 在MockFor类和StubFor类中,use()方法最后会做这个检查,但是testCreateSomething()方法中没有这个方法,然而你可以调用控制对象的verify()方法。在这个例子中,otherControl对象可以实现。这个方法会执行检查,在newIdentifier()应该被调用但没有被调用的情况下抛出诊断结果。

最后,这个例子中的testCountItems()向我们展示了mockDomain()方法的另外一个特性:

def testInstances = [ new Item(code: "NH-4273997", name: "Laptop"), 
                                          new Item(code: "EC-4395734", name: "Lamp"), 
                                          new Item(code: "TF-4927324", name: "Laptop") ] 
mockDomain(Item, testInstances)

通常手工模拟动态遍历器比较烦人,而且你经常不得不为每次执行设置不同的数组;除了这个之外,假如你决定使用一个不同的遍历器,你就不得不更新测试实例去测试新的方法。感谢mockDomain()方法为一组域实例的动态遍历器提供了一个轻量级的执行实现,把测试数据简单得作为这个方法的第二个参数,模拟遍历器就会工作了。

GrailsUnitTestCase - 模拟方法

你已经看过了一些介绍GrailsUnitTestCase中mock..()方法的例子。在这部分我们将详细地介绍所有GrailsUnitTestCase中提供的方法,首先以通用的mockFor()开始。在开始之前,有一个很重要的说明先说一下,使用这些方法可以保证对所给的类做出的任何改变都不会让其他测试实例受影响。这里有个普遍出现且严重的问题,当你尝试通过meta-class编程方法对它自身进行模拟,但是只要你对每个想模拟的类使用任何一个mock..()方法,这个问题就会消失了。

mockFor(class, loose = false)

万能的mockFor方法允许你对你一个类设置strict或loose请求。

这个方法很容易使用,默认情况下它会创建一个strict模式的mock控制对象,它的方法调用顺序非常重要,你可以使用这个对象详细定义各种需求:

def strictControl = mockFor(MyService)
strictControl.demand.someMethod(0..2) { String arg1, int arg2 -> … }
strictControl.demand.static.aStaticMethod {-> … }

注意你可以在demand后简单得使用static属性,就可以mock静态方法,然后定义你想mock的方法名字,一个可选的range范围作为它的参数。这个范围决定这个方法会被调用了多少次,所以假如这个方法的执行次数超过了这个范围,偏小或偏大,一个诊断异常就会被抛出。假如这个范围没有定义,默认的是使用“1..1”范围,比如上面定义的那个方法就只能被调用一次。

demand的最后一部分是closure,它代表了这个mock方法的实现部分。closure的参数列表应该与被mock方法的数量和类型相匹配,但是同时你可以随意在closure主体里添加你想要的代码。

像之前提到的,假如你想生成一个你正在模拟类的能用mock实例,你就需要调用mockControl.createMock()。事实上,你可以调用这个方法生成你想要的任何数量的mock实例。一旦执行了test方法,你就可以调用 mockControl.verify()方法检查你想要执行的方法执行了没。

最后,如下这个调用:

def looseControl = mockFor(MyService, true)

将生成一个含有loose特性的mock控制对象,比如方法调用的顺序不重要。

mockDomain(class, testInstances = )

这个方法选一个类作为它的参数,让所有domain类的非静态方法和静态方法的mock实现都可以在这个类调用到。

使用测试插件模拟domain类是其中的一个优势。手工模拟无论如何都是很麻烦的,所以mockDomain()方法帮你减轻这个负担是多么美妙。

实际上,mockDomain()方法提供了domain类的轻量级实现,database只是存储在内存里的一组domain实例。 所有的mock方法,save(),get(),findBy*()等都可以按你的期望在这组实例里运行。除了这些功能之外,save()和validate()模拟方法会执行真正的检查确认,包括对唯一的限制条件支持,它们会对相应的domain实例产生一个错误对象。

这里没什么其他要说了,除了插件不支持标准查询语句和HQL查询语句模拟。假如你想使用其中的一个,你可以简单得手工mock相应的方法,比如用mockFor()方法,或用真实的数据测试一个集成实例。

mockForConstraintsTests(class, testInstances = )

这个方法可以对domain类和command对象进行非常详细地模拟设置,它允许你确认各种约束是否按你想要的方式执行。

你测试domain约束了?如果没有,为什么没有?如果你的回答是它们不需要测试,请你三思。你的各种约束包含逻辑部分,这部分逻辑很容易产生bug,而这类bug很容易被捕捉到,特别的是save()允许失败也不会抛出异常。而如果你的回答是太难或太烦,现在这已经不再是借口了,可以用mockForConstraintsTests()解决这个问题。

这个方法就像mockDomain()方法的简化版本,简单得对所给的domain类添加一个validate()方法。你所要做的就是mock这个类,创建带有属性值的实例,然后调用validate()方法。你可以查看domain实例的errors属性判断这个确认方法是否失败。所以假如所有我们正在做的是模拟validate()方法,那么可选的测试实例数组参数呢?这就是我们为什么可以测试唯一约束的原因,你很快就可以看见了。

那么假设我们拥有如下的一个简单domain类:

class Book {
    String title
    String author

static constraints = { title(blank: false, unique: true) author(blank: false, minSize: 5) } }

不要担心这些约束是否合理,它们在这仅仅是示范作用。为了测试这些约束,我们可以按下面方法来做:

class BookTests extends GrailsUnitTestCase {
    void testConstraints() {
        def existingBook = new Book(title: "Misery", author: "Stephen King")
        mockForConstraintsTests(Book, [ existingBook ])

// Validation should fail if both properties are null. def book = new Book() assertFalse book.validate() assertEquals "nullable", book.errors["title"] assertEquals "nullable", book.errors["author"]

// So let's demonstrate the unique and minSize constraints. book = new Book(title: "Misery", author: "JK") assertFalse book.validate() assertEquals "unique", book.errors["title"] assertEquals "minSize", book.errors["author"]

// Validation should pass! book = new Book(title: "The Shining", author: "Stephen King") assertTrue book.validate() } }

你可以在没有进一步解释的情况下,阅读上面这些代码,思考它们正在做什么事情。我们会解释的唯一一件事是errors属性使用的方式。第一,它返回了真实的Spring Errors实例,所以你可以得到你通常期望的所有属性和方法。第二,这个特殊的Errors对象也可以用如上map/property方式使用。简单地读取你感兴趣的属性名字,map/property接口会返回被确认的约束名字。注意它是约束的名字,不是你所期望的信息内容。

这是测试约束讲解部分。我们要讲的最后一件事是用这种方式测试约束会捕捉一个共同的错误:typos in the "constraints" property。正常情况下这是目前最难捕捉的一个bug,还没有一个约束单元测试可以直接简单得发现这个问题。

mockLogging(class, enableDebug = false)

这个方法可以给一个类增加一个mock的log属性,任何传递给mock的logger的信息都会输出到控制台的。

mockController(class)

此方法可以为指定类添加mock版本的动态控制器属性和方法,通常它和ControllerUnitTestCase一起连用。

mockTagLib(class)

此方法可以为指定类添加mock版本的动态tablib属性和方法,通常它和TagLibUnitTestCase一起连用。

9.2 集成测试

集成测试与单元测试不同的是在测试实例内你拥有使用Grails环境的全部权限。Grails将使用一个内存内的HSQLDB数据库作为集成测试,清理每个测试之间的数据库的数据。

测试控制器

测试控制器之前你首先要了解Spring Mock Library。

实质上,Grails自动用 MockHttpServletRequestMockHttpServletResponse,和 MockHttpSession 配置每个测试实例,你可以使用它们执行你的测试用例。比如你可以考虑如下controller:

class FooController {

def text = { render "bar" }

def someRedirect = { redirect(action:"bar") } }

它的测试用例如下:

class FooControllerTests extends GroovyTestCase {

void testText() { def fc = new FooController() fc.text() assertEquals "bar", fc.response.contentAsString }

void testSomeRedirect() {

def fc = new FooController() fc.someRedirect() assertEquals "/foo/bar", fc.response.redirectedUrl } }

在上面的实例中,返回对象是一个MockHttpServletResponse实例,你可以使用这个实例获取写进返回对象的contentAsString值,或是跳转的URL。这些Servlet API的模拟版本全部都很更改,不像模拟之前那样子,因此你可以对请求对象设置属性,比如contextPath等。

Grails在集成测试期间调用actions不会自动执行interceptors,你要单独测试拦截器,必要的话通过functional testing测试。

用应用测试控制器

假如你的控制器引用了一个应用服务,你必须在测试实例里显示初始化这个应用。

举个使用应用的控制器例子:

class FilmStarsController {
    def popularityService

def update = { // do something with popularityService } }

相应的测试实例:

class FilmStarsTests extends GroovyTestCase {
    def popularityService

public void testInjectedServiceInController () { def fsc = new FilmStarsController() fsc.popularityService = popularityService fsc.update() } }

测试控制器command对象

使用command对象,你可以给请求对象request提供参数,当你调用没有带参数的action处理对象时,它会自动为你做command对象工作。

举个带有command对象的控制器例子:

class AuthenticationController {
    def signup = { SignupForm form ->
        …
    }
}

你可以如下对它进行测试:

def controller = new AuthenticationController()
controller.params.login = "marcpalmer"
controller.params.password = "secret"
controller.params.passwordConfirm = "secret"
controller.signup()

Grails把signup()的调用自动当作对处理对象的调用,利用模拟请求参数生成command对象。在控制器测试期间,params通过Grails的模拟请求对象是可更改的。

测试控制器和render方法

render方法允许你在一个action主体内的任何一个地方显示一个定制的视图。例如,考虑如下的例子:

def save = {
        def book = Book(params)
        if(book.save()) {
                // handle
        }
        else {
                render(view:"create", model:[book:book])
        }
}

上面举的这个例子中,处理对象用返回值作这个模型的结果是不可行的,相反结果保存在控制对象的modelAndView属性当中。modelAndView属性是Spring MVC ModelAndView类的一个实例,你可以用它测试一个action处理后的结果:

def bookController = new BookController()
bookController.save()
def model = bookController.modelAndView.model.book

模拟生成请求数据

如果你测试一个action请求处理对象需要类似REST web应用的请求参数,你可以使用Spring MockHttpServletRequest对象实现。例如,考虑如下这个action,它执行一个进来请求的数据邦定:

def create = {
        [book: new Book(params['book']) ] 
}

假如你想把book参数模拟成一个XML请求对象,你可以按如下方法做:

void testCreateWithXML() {
        def controller = new BookController()
        controller.request.contentType = 'text/xml'
        controller.request.contents = '''<?xml version="1.0" encoding="ISO-8859-1"?>
        <book>
                <title>The Stand</title>
                …
        </book> 
        '''.getBytes() // note we need the bytes

def model = controller.create() assert model.book assertEquals "The Stand", model.book.title }

同样你可以通过JSON对象达到这个目的:

void testCreateWithJSON() {
        def controller = new BookController()     
        controller.request.contentType = "text/json"
        controller.request.content = '{"id":1,"class":"Book","title":"The Stand"}'.getBytes()

def model = controller.create() assert model.book assertEquals "The Stand", model.book.title

}

使用JSON,也不要忘记对class属性指定名字,绑定的目标类型。在XML里,在book节点内这些设置隐含的,但是使用JSON你需要这个属性作为JSON包的一部分。

更多关于REST web应用的信息,可以参考REST章节。

测试Web Flows

测试Web Flows需要一个特殊的测试工具grails.test.WebFlowTestCase,它继承Spring Web Flow的AbstractFlowExecutionTests 类。Testing Web Flows requires a special test harness called grails.test.WebFlowTestCase which sub classes Spring Web Flow's AbstractFlowExecutionTests class.

WebFlowTestCase子类必须是集成测试实例Subclasses of WebFlowTestCase must be integration tests

例如在下面的这个小flow情况下:

class ExampleController {
        def exampleFlow = {
                start {
                        on("go") {
                                flow.hello = "world"
                        }.to "next"
                }
                next {
                        on("back").to "start"
                        on("go").to "end"
                }
                end()
        }  
}

接着你需要让测试工具知道使用什么样的flow定义。通过重载getFlow抽象方法可以实现:

class ExampleFlowTests extends grails.test.WebFlowTestCase {
        def getFlow() { new ExampleController().exampleFlow }
        …
}

假如你需要指定一个flow标识,你可以通过重载getFlowId方法实现,同时默认情况下是一个测试实例:

class ExampleFlowTests extends grails.test.WebFlowTestCase {
        String getFlowId() { "example" }
        …
}

一旦这在你的测试实例里实现了,你需要用startFlow方法开始启动这个flow,这个方法会返回ViewSelection对象:

void testExampleFlow() {
        def viewSelection = startFlow()

assertEquals "start", viewSelection.viewName … }

如上所示,你可以通过使用ViewSelection对象的viewName属性,检查你是否是正确的。触发事件你需要使用signalEvent方法:

void testExampleFlow() {
        …
        viewSelection = signalEvent("go")
        assertEquals "next", viewSelection.viewName
        assertEquals "world", viewSelection.model.hello
}

这里我们可以给flow发送信号执行go事件,这导致了到next状态的转变。在上面的这个例子中转变的结果把一个hello变量放进flow范围。我们可以检查如上ViewSelection的model属性测试这个变量的值。

测试标签库

其实测试标签库是一件很容易的事,因为当一个标签被当作一个方法执行时,它会返回一个字符串值。所以例如你拥有如下的一个标签库:

class FooTagLib {
   def bar =  { attrs, body ->
           out << "<p>Hello World!</p>"
   }

def bodyTag = { attrs, body -> out << "<${attrs.name}>" out << body() out << "</${attrs.name}>" } }

相应的测试如下:

class FooTagLibTests extends GroovyTestCase {

void testBarTag() { assertEquals "<p>Hello World!</p>", new FooTagLib().bar(null,null) }

void testBodyTag() { assertEquals "<p>Hello World!</p>", new FooTagLib().bodyTag(name:"p") { "Hello World!" } } }

注意在第二个例子的testBodyTag中,我们传递了返回标签主体内容的代码块作为内容,把标签主体内容作为字符串比较方便。

使用GroovyPagesTestCase测试标签库

除了上述简单的标签库测试方法之外,你也可以使用grails.test.GroovyPagesTestCase类测试标签库。

GroovyPagesTestCase类是常见GroovyTestCase的子类,它为GSP显示输出提供实用方法。

GroovyPagesTestCase类只能在集成测试中使用。

举个时间格式化标签库的例子,如下:

class FormatTagLib {
        def dateFormat = { attrs, body -> 
                out << new java.text.SimpleDateFormat(attrs.format) << attrs.date
        }
}

可以按如下方法进行测试:

class FormatTagLibTests extends GroovyPagesTestCase {
        void testDateFormat() {
                def template = '<g:dateFormat format="dd-MM-yyyy" date="${myDate}" />'

def testDate = … // create the date assertOutputEquals( '01-01-2008', template, [myDate:testDate] ) } }

你也可以使用GroovyPagesTestCase的applyTemplate方法获取GSP的输出结果:

class FormatTagLibTests extends GroovyPagesTestCase {
        void testDateFormat() {
                def template = '<g:dateFormat format="dd-MM-yyyy" date="${myDate}" />'

def testDate = … // create the date def result = applyTemplate( template, [myDate:testDate] )

assertEquals '01-01-2008', result } }

测试Domain类

用GORM API测试domain类是一件很简单的事情,然而你还要注意一些事项。第一,假如你在测试查询语句,你将经常需要flush以便保证正确的状态持久保存到数据库。比如下面的一个例子:

void testQuery() {
        def books = [ new Book(title:"The Stand"), new Book(title:"The Shining")]
        books*.save()

assertEquals 2, Book.list().size() }

这个测试实际上会失败,因为调用save方法的时候,save方法不会真的持久保存book实例。调用save方法仅仅是向Hibernate暗示在将来的某个时候这些实例应该会被保存。假如你希望立即提交这些改变,你需要flush它们:

void testQuery() {
        def books = [ new Book(title:"The Stand"), new Book(title:"The Shining")]
        books*.save(flush:true)

assertEquals 2, Book.list().size() }

在这个案例中我们传递了flush的true值作为参数,更新将马上被保存,因此对此后的查询语句也有效。

9.3 功能测试

功能测试是测试正在运行的应用,经常自动化较难实现。Grails没有发布任何功能测试开箱即用支持,但是通过插件实现了对 Canoo WebTest 的支持。

首先按如下的命令按照Web Test:

grails install-plugin webtest

参考reference on the wiki ,它里面了解释怎么使用Web Test和Grails。

10. 国际化

通过潜在支持Spring MVC国际化标准,Grails支持国际化标准(i18n)开箱即用。在Grails中,你能够根据用户属地定制该地区语言的文字。引用Java语言中的 Locale 文档定义:
Locale 对象描述了特定的地理、政治和文化地区。需要 Locale来执行其任务的操作称为本地化操作,它使用 Locale为用户量身定制信息。例如,显示一个数值就是本地化操作,应该根据用户本国家、地区或文化的风俗/传统来格式化该数值。

一个Locale对象由language codecountry code组成。比如,en_US是美国英语的代码,而en_GB是英国英语的代码。

10.1 理解消息绑定

现 在你知道了本地化,为了在Grails中使用它们,你不得不创建你想显示的不同语言信息资源。Grails中的信息资源以简单的java属性文件格式放置 在grails-app/i18n目录下。

每个资源束根据规则,以messages名字开始和locale结束。Grails在grails-app/i18n下发布了一串不同语言范围内的内置信息,例如:

messages.properties 
messages_de.properties
messages_es.properties
etc.

默 认情况Grails会在messages.properties文件中检索信息,除非用户已经指定了一个自定义本地化文件。通过创建一个新的以 locale标签结尾的属性文件,你可以创建你感兴趣的属于自己的信息资源。比如属于英式英语范畴的 messages_en_GB.properties。

10.2 修改本地化

默认情况用户地区从传进来的 Accept-Language头部得知。然而 通过简单得给Grails传进lang参数作为请求参数,用户就可以更改地区了:
/book/list?lang=es

Grails 会自动更改用户地区,并把这个值存放在cookie里,随后的各种请求会有个新的头部。

10.3 读取信息

视图中读取信息

你 通常最需要信息的地方是在视图内。要在视图内读取信息,使用message标签就可以了,如下:

<g:message code="my.localized.content" />

只要你在带有合适locale后缀的messages.properties文件有个key键,比如下面这种格式,Grails就会找到相对应的信息:

my.localized.content=Hola, Me llamo John. Hoy es domingo.

注 意有时候你需要向相应的信息传递参数。参考下面这个message标签:

<g:message code="my.localized.content" args="${ ['Juan', 'lunes'] }" />

还有可能在信息中使用定位参数:

my.localized.content=Hola, Me llamo {0}. Hoy es {1}.

控制器和标签库中读取信息

因 为你可以在controllers中像方法一样使用标签,所以在controllers中读取信息也很经常,如下:

def show = { 
def msg = message(code:"my.localized.content", args:['Juan', 'lunes'])
}

tag libraries中使用的方法一样,但是注意如果你的标签库使用了不同的namespace,你需要使用g.前缀:

def myTag = { attrs, body -> 
def msg = g.message(code:"my.localized.content", args:['Juan', 'lunes'])
}

10.4 脚手架和i18n

Grails 没有发布可以生成控制器和视图的i18n特性脚手架模板。然而i18n模板插件可以提供i18n特性脚手架模板,这些模板与默认的脚手架模板一样,除了它 们为标签,按钮等定义信息时使用message标签外。

首先用下面的这个命令安装i18n模板:

grails install-plugin i18n-templates

参考reference on the wiki,它里面了解释怎么使用i18n模板。

11. 安全性

Grails差不多和Java Servlets一样可靠。然而由于JVM运行代码的特性,Java servlets对一般的缓冲区溢出和恶意URL使用是极为安全和免疫的。

Web安全问题通常由于开发人员的无知过错造成的,Grails提供了一些帮助,可以避免常出现的错误,使安全应用更加容易编写。

Grails可以自动做什么

Grails拥有一些默认的内置安全机制

  1. 所有通过GORM域对象访问标准数据库可以自动避免SQL语句以防止SQL注入攻击。
  2. 默认scaffolding模板HTML文件当打开时所有数据域不显示。
  3. 所有Grails的链接创建标签(link, form, createLink, createLinkTo 等)都使用适当的转义机制以防止代码注入。
  4. Grails提供codecs,运行你在显示HTML,JavaScript和URLs时,转义数据以避免在数据里注入攻击。

11.1 防止攻击

SQL注入

Hibernate是实现GORM域类的基础技术,当提交数据库时会自动转义数据,所以这个没什么问题。然而编写使用未检查的请求参数的脏动态HQL代码,仍然会有问题可能存在。比如如下的这种做法就很容易受HQL注入攻击:

def vulnerable = {
	def books = Book.find("from Book as b where b.title ='" + params.title + "'")
}

千万别这样做。假如你想传递参数,用命名参数和定位参数代替:

def safe = {
	def books = Book.find("from Book as b where b.title =?", [params.title])
}

钓鱼式攻击

这是一个公关关系问题,涉及到避免你的品牌化过程和与顾客设定的沟通手段遭到黑客攻击。顾客需要知道怎么确认收到的emails是真的。

XSS-跨站脚本攻击

你的应用要尽可能多得检验进来的请求是从你的应用里发出的,而不是其它网站。标签和页面流系统能做到这点,Grails对Spring Web Flow的支持也默认包含了这个安全特性。

确保所有呈现到视图的数据值都被转义过也是非常重要的。例如当呈现HTML文件或XHTML文件时,你必须对每个对象调用encodeAsHTML,以便保证用户不会向其他人读取的数据和标签恶意注入JavaScript代码或其他HTML代码。Grails为此目的提供了若干个动态编码方法,因此假如你的输出转义格式没有现成的,你可以很容易得编写自己的编码器。

你也必须避免使用请求参数和数据域来决定用户转向的下一个链接。假如你使用一个successURL参数,在你成功登入之后,用来指示用户的转向;这时攻击者可以通过你的网站模拟登入程序,然后一旦登入就把用户转向到他们的网站,这样就潜在允许JS代码使用该网站的登入帐号。

HTML/URL注入

HTML和URL注入提供有害的数据,之后被用来在页面生成一个链接,点击它不会产生期望的行为,可能会转向另外一个网站或更改请求参数。

Grails提供的codecs可以很容易得处理HTML/URL注入,Grails提供的标签库在适用的地方全都使用encodeAsURL。如果你自己创建能生成链接的标签,你在做的时候要小心。

拒绝服务DoS

负载均衡器和其他应用在这里可能会起到用处,但是还存在其他问题,比如过度查询,攻击者创建一个链接设置结果集的最大值,导致一个查询超过服务器的最大内存限制或拖慢系统运行。解决办法是在请求参数传进动态遍历器或其他GORM查询方法之前,给这些请求参数“消毒”:

def safeMax = Math.max(params.max?.toInteger(), 100) // never let more than 100 results be returned
return Book.list(max:safeMax)

可推测ID号

许多应用把URL的最后一部分当作从GORM或者其他地方获取的某个对象的id。特别是当发生在GORM中时,这些id号是很容易猜测的,因为这些id号通常是一串数字。

因此你必须假定请求用户在请求返回时用请求id号可以看见相对应的对象。

不这样做是隐藏式安全,这样做毫无疑问是非法的,像有letmein的默认密码等等这些情况。

你必须假设每个未受保护URL都可以公共访问。

11.2 编码和解码对象

Grails支持动态编码/解码方法概念。Grails捆绑了一些标准的编解码器,Grails也为开发人员提供了一个贡献自己编解码器的简单机制,这些编解码器在运行时可以被识别。

编解码器类

一个Grails编解码器是个包含一个编码闭包,一个解码闭包或两者皆有。当一个Grails应用启用了,Grails框架会动态从grails-app/utils/目录加载编解码器。

Grails框架将在 grails-app/utils/目录下查找以Codec结尾命名的类名。例如Grails捆绑的其中一个标准编解码器就是HTMLCodec。

假如一个编解码器包含一个encode属性,该属性被赋予一个代码块,Grails会创建一个动态的encode方法,并把该方法添加到Object类,方法名表示了定义encode闭包的编解码器。例如,HTMLCodec类定义了一个编码器代码块,因此Grails会把该闭包与名为encodeAsHTML的Object类相关联。

HTMLCodec类和URLCodec类也定义了解码块,所以Grails会把这些闭包与decodeHTML和decodeURL相关联的。动态编解码器能在Grails应用的任何一个地方执行。例如,考虑一下这种情况,一个报告文件含有一个叫description的属性,该属性包含了需要被转义显示在HTML文档的特殊字符。GSP文档里,一种处理方法就是用如下的动态编码器编码description属性:

${report.description.encodeAsHTML()}

执行解码使用value.decodeHTML()语句。

标准的编解码器

HTMLCodec

编解码器执行HTML转义过程和反转义过程,所以你提供的数值在没有创建任何HTML标签或破坏页面布局下可以被安全得显示出来。例如,给个"Don't you know that 2 > 1?"字符串,你就不能在HTML页面中安全得显示出来,因为大于符号>看起来像要关闭一个标签,特别是你在某个属性内显示这个字符串,情况会更糟糕,像输入框的value属性 。

使用例子如下:

<input name="comment.message" value="${comment.message.encodeAsHTML()}"/>

注意HTML编码不会重新编码单引号或双引号,你必须对属性值只用两个重复引号避免含有引号的正文毁坏你的页面。

URLCodec

当在生成跳转链接,形体处理(form actions)链接,或者任何时候需要数据生成链接时,URL编码是必需的。URL编码可以阻止非法字符串进入链接改变它跳转的目的地,例如"Apple & Blackberry"不能作为get请求中的一个参数,因为&符号为破坏参数解析过程。

使用例子如下:

<a href="/mycontroller/find?searchKey=${lastSearch.encodeAsURL()}">Repeat last search</a>

Base64Codec

执行Base64编码/解码函数,使用例子如下:

Your registration code is: ${user.registrationCode.encodeAsBase64()}

JavaScriptCodec

JavaScriptCodec会转义字符串成为合法的JavaScript字符串,使用例子如下:

Element.update('${elementId}', '${render(template: "/common/message").encodeAsJavaScript()}')

HexCodec

HexCodec会把字节数组或数字数列编码为小写十六进制字符串,可以把十六进制字符串编码为字节数组,使用例子如下:

Selected colour: #${[255,127,255].encodeAsHex()}

MD5Codec

MD5Codec使用MD5算法摘要字节数组,数字数列或默认系统编码的字符串字节数组,得到一格小写十六进制字符串,使用例子如下:

Your API Key: ${user.uniqueID.encodeAsMD5()}

MD5BytesCodec

MD5BytesCodec使用MD5算法摘要字节数组,数字数列或默认系统编码的字符串字节数组,得到一个字节数组,使用例子如下:

byte[] passwordHash = params.password.encodeAsMD5Bytes()

SHA1Codec

SHA1Codec使用SHA1算法摘要字节数组,数字数列或默认系统编码的字符串字节数组,得到一格小写十六进制字符串,使用例子如下:

Your API Key: ${user.uniqueID.encodeAsSHA1()}

SHA1BytesCodec

SHA1BytesCodec使用SHA1算法摘要字节数组,数字数列或默认系统编码的字符串字节数组,得到一个字节数组,使用例子如下:

byte[] passwordHash = params.password.encodeAsSHA1Bytes()

SHA256Codec

SHA256Codec使用SHA256算法摘要字节数组,数字数列或默认系统编码的字符串字节数组,得到一格小写十六进制字符串,使用例子如下:

Your API Key: ${user.uniqueID.encodeAsSHA256()}

SHA256BytesCodec

SHA256BytesCodec使用SHA1算法摘要字节数组,数字数列或默认系统编码的字符串字节数组,得到一个字节数组,使用例子如下:

byte[] passwordHash = params.password.encodeAsSHA256Bytes()

定制编解码器Custom Codecs

许多应用可能定制属于自己的编解码器,Grails在装载标准编解码器时把它们一起装载。定制编解码器类必须在grails-app/utils/目录下定义,而且类名必须以Codec结尾。定制编解码器可能含有一个静态encode块,一个静态decode块或两者皆有。这些编解码代码块需要一个单一参数,当作动态方法操作对象,如下:

class PigLatinCodec {
  static encode = { str ->
    // convert the string to piglatin and return the result
  }
}

在适当的地方,一个应用可以使用上方定义的编解码器做如下的工作:

${lastName.encodeAsPigLatin()}

11.3 认证

尽管现在认证没有默认机制,实际上有上千种方法可以执行认证。然而,用 interceptorsfilters 实施一个简单的认证机制是没意义的。

过滤器运行你对所有的控制器或URI空间应用认证。比如你可以在grails-app/conf/SecurityFilters.groovy类中创建一组新过滤器如下:

class SecurityFilters {
   def filters = {
       loginCheck(controller:'*', action:'*') {
           before = {
              if(!session.user && actionName != "login") {
                  redirect(controller:"user",action:"login")
                  return false					
	           }
           }

} } }

在请求处理执行之前,上述类中的loginCheck过滤器将拦截该执行动作。假如session里没有一个用户而且请求被执行,该login请求处理不是自己转向到自己。

Login请求处理也是很小的:

def login = {
	if(request.get) render(view:"login")
	else {
		def u = User.findByLogin(params.login)
		if(u) {
			if(u.password == params.password) {
				session.user = u
				redirect(action:"home")
			}
			else {
				render(view:"login", model:[message:"Password incorrect"])							
			}
		}
		else {
			render(view:"login", model:[message:"User not found"])			
		}
	}
}

11.4 安全插件

如果你需要比简单认证更高级的功能,诸如授权(authorization),角色(roles)等,那么你可能要考虑使用一个可用的安全插件。

11.4.1 Acegi

Acegi插件是建立在 Spring Acegi 项目上,该项目为建立各种认证和授权架构提供了一个灵活,易扩展的框架。

Acegi插件需要你在URI和角色之间制定个详细的映射,为规范人,权威专家和请求maps提供一个默认的领域模型domain model。点击documentation on the wiki,查看更多信息。

11.4.2 JSecurity

JSecurity 是另外一个面向Java POJO的安全框架,它也可以提供一个规范领域,用户,角色和权限的默认领域模型。使用JSecurity,你必须让每个你想保护的controller类继承一个controller基类,然后提供一个建立角色的 accessControl代码块。例子如下:

class ExampleController extends JsecAuthBase {
    static accessControl = {
        // All actions require the 'Observer' role.
        role(name: 'Observer')

// The 'edit' action requires the 'Administrator' role. role(name: 'Administrator', action: 'edit')

// Alternatively, several actions can be specified. role(name: 'Administrator', only: [ 'create', 'edit', 'save', 'update' ]) }

… }

更多关于JSecurity的信息,参考JSecurity Quick Start

12. 插件

Grails提供了许多扩展点来满足你的扩展,包括从命令行接口到运行时配置引擎。以下章节详细说明了该如何着手来做这些扩展。

12.1 创建和安装插件

创建插件

创建一个Grails插件,只需要运行如下命令即可:

grails create-plugin [PLUGIN NAME]

根据你输入的名字将产生一插件工程。比如你输入 grails create-plugin example. 系统将创建一个名为 example的插件工程.

除了插件的根目录有一个所谓的“插件描述”的Groovy文件外,其他的跟一般的Grails工程结构完全一样.

将插件作为一个常规的Grails工程是有好处的,比如你可以马上用以下命令来测试你的插件:

grails run-app

由于你创建插件默认是没有 URL 映射的,因此控制器并不会马上有效.如果你的插件需要控制器,那要创建 grails-app/conf/MyUrlMappings.groovy 文件,并且在起始位置增加缺省的映射 "/$controller/$action?/$id?"().

插件描述文件本身需要符合以 GrailsPlugin 结尾的惯例并且将位于插件工程的根目录中。比如:

class ExampleGrailsPlugin {
   def version = 0.1

… }

所有插件的根目录下边都必须有此类并且还要有效,此类中定义了插件的版本和其他各式各样的可选的插件扩展点的钩子(hooks)--即插件预留的可以扩展的接口.

通过以下特殊的属性,你还可以提供插件的一些额外的信息:

Quartz Grails plugin为例:

class QuartzGrailsPlugin {
    def version = "0.1"
    def author = "Sergey Nebolsin"
    def authorEmail = "nebolsin@gmail.com"
    def title = "This plugin adds Quartz job scheduling features to Grails application."
    def description = '''
Quartz plugin allows your Grails application to schedule jobs to be
executed using a specified interval or cron expression. The underlying
system uses the Quartz Enterprise Job Scheduler configured via Spring,
but is made simpler by the coding by convention paradigm.
'''
    def documentation = "http://grails.org/Quartz+plugin"

… }

插件的安装和发布

要发布插件,你需要一个命令行窗口,并且进入到插件的根目录,输入:

grails package-plugin

这将创建一个 grails- +插件名称+版本的zip文件. 以先前的example插件为例,这个文件名是 grails-example-0.1.zip. package-plugin 命令还将生成 plugin.xml f在此文件中包含机器可读的插件信息,比如插件的名称、版本、作者等等。

产生了可以发布的插件文件以后(zip文件),进入到你自己的Grails工程的根目录,输入:

grails install-plugin /path/to/plugin/grails-example-0.1.zip

如果你的插件放在远程的Http服务器上,你也可以这样:

grails install-plugin http://myserver.com/plugins/grails-example-0.1.zip

注意被排除的组件

尽管 create-plugin 命令为您创建某些文件,以便插件能做为Grails应用运行,但是当打包插件的时候不是所有的文件都会在含在里面. 以下是通过package-plugin创建时,不包含的文件和目录:

如果你希望创建包含 WEB-INF 目录的组建,那么建议你使用 _Install.groovy 脚本文件 (covered later),这个脚本文件之后会解释;当安装一个插件提供这些组件时,这个脚本文件会被执行。 此外,除了用 UrlMappings.groovy之外,也允许你使用包括 UrlMappings 名字来定义不同的名称,例如 FooUrlMappings.groovy

12.2 插件仓库

在Grails插件的存储仓库(Repository)发布插件

更好的发布插件的方式是将其发布到Grails插件的存储仓库. 这样通过 list-plugins 命令就可以看到你的插件了:

grails list-plugins

此命令将列出Grails插件存储库的所有插件,当然了也可以用 plugin-info 来查看指定插件的信息:

grails plugin-info [plugin-name]

这将输出更多的详细信息,这些信息都是维护在插件描述文件中的。

如果你创建了一个Grails插件,你可以访问 创建插件,这里详细说明了如何在容器中发布你的插件。

当你有访问Grails插件仓库的权限时,要发行你的插件,只需要简单执行 release-plugin 即可:

grails release-plugin

这将自动地将改动提交到SVN和创建标签(svn的tagging),并且通过 list-plugins 命令你可以看到这些改动.

配置附加库

默认情况下,您使用的 list-plugins, install-plugin and release-plugin 命令都指向 http://plugins.grails.org。

然而, 要配置多个插件仓库,您可以使用grails-app/conf/BuildSettings.groovy 文件:

grails.plugin.repos.discovery.myRepository="http://svn.codehaus.org/grails/trunk/grails-test-plugin-repo"
grails.plugin.repos.distribution.myRepository="https://svn.codehaus.org/grails/trunk/grails-test-plugin-repo"

Repositories are split into those used for discovery over HTTP and those used for distribution, typically over HTTPS. 如果你想在多个项目中使用相同的设置,你可以把这些配置到 USER_HOME/.grails/settings.groovy

一旦使用了 list-plugins, install-plugin and plugin-info 命令将会自动处理最新配置的插件库。如果你只想把插件库中的插件列表列出来,你可以使用别名:

grails list-plugins -repository=myRepository

此外,如果你想和配置好的插件包一起发布插件,你可以用 release-plugin 命令:

grails release-plugin -repository=myRepository

12.3 理解插件的结构

如前所提到的,一个插件除了包含一个插件描述文件外,几乎就是一个常规的Grails应用。尽管如此,当安装以后,插件的结构还是有些许的差别。比如一个插件目录的结构如下:

+ grails-app
     + controllers
     + domain
     + taglib
     etc.
 + lib
 + src
     + java
     + groovy
 + web-app
     + js
     + css

从本质上讲,当一个插件被安装到Grails工程以后, grails-app 下边的内容将被拷贝到以 plugins/example-1.0/grails-app(以example为例)目录中. 这些内容 不会 被拷贝到工程的源文件主目录,即插件永远不会跟工程的主目录树有任何接口上的关系。.

然而,那些在特定插件目录中 web-app 目录下的静态资源将会被拷贝到主工程的 plugins 目录下. 比如 web-app/plugins/example-1.0/js.

因此,要从正确的地方引用这些静态资源也就成为插件的责任。比如,你要在GSP中引用一个JavaScript文件,你可以这样:

<g:createLinkTo dir="/plugins/example/js" file="mycode.js" />

这样做当然可以,但是当你开发插件并且单独运行插件的时候,将产生相对链接(link)的问题.

为了应对这种变化即不管插件是单独运行还是在Grails应用中运行,特地新增一个特别的 pluginContextPath 变量,用法如下:

<g:createLinkTo dir="${pluginContextPath}/js" file="mycode.js" />

这样在运行期间 pluginContextPath 变量将会等价于/ 或 /plugins/example 这取决于插件是单独运行还是被安装在Grails应用中

在lib和 src/java 以及 src/groovy 下的Java、Groovy代码将被编译到当前工程的 web-app/WEB-INF/classes 下边,因此在运行时也不会出现类找不到的问题.

12.4 提供基础的工件

增加新的脚本

在插件的scripts目录下可以增加新的Gant相关的脚本:

+ MyPlugin.groovy
   + scripts     <-- additional scripts here
   + grails-app
        + controllers
        + services
        + etc.
    + lib

增加新的控制器,标签库或者服务

grails-app 相关的目录树下,可以增加新的控制器、标签库、服务等,不过要注意:当插件被安装后将从其被安装的地方加载,而不是被拷贝到当前主应用工程的相应目录。.

+ ExamplePlugin.groovy
   + scripts
   + grails-app
        + controllers  <-- additional controllers here
        + services <-- additional services here
        + etc.  <-- additional XXX here
    + lib

Providing Views, Templates and View resolution

提供控制器的插件也会提供默认的视图。通过插件模块化您的应用是个很好的途径。Grails视图处理机制的工作原理是首先查看应用中被安装的视图,如果失败将视图查找插件中的视图。

比如有一个 AmazonGrailsPlugin 插件提供一个叫 BookController 的控制器,如果执行了 list 将会首先查找 grails-app/views/book/list.gsp 这个视图,如果失败,将会在插件里查找相同名称的视图。

但是,如果视图使用了模板,同时插件也提供了这个视图,那么必须使用以下的语法:

<g:render template="fooTemplate" contextPath="${pluginContextPath}"/>

注意 pluginContextPath 变量做为 contextPath 属性值的用法。如果没有指定这个属性,Grails将在应用中的模板中查找。

Excluded Artefacts

默认的,when packaging a plug-in,当打包一个插件时,Grails 的插件包中将不包含以下文件:

如果你的插件需要 web-app/WEB-INF 目录下的文件,那么建议你修改插件的 scripts/_Install.groovy Gant 脚本文件把项目的目标目录安装到插件包中。

此外, UrlMappings.groovy 文件默认不会避免命名冲突,你可以使用在默认名字前加增加 前缀。比如叫做 grails-app/conf/BlogUrlMappings.groovy

12.5 评估规约

在得以继续查看基于规约所能提供的运行时配置以前,有必要了解一下怎样来评估插件的这些基本规约。本质上,每一个插件都有一个隐含的 GrailsApplication接口的实例变量:application

GrailsApplication 提供了在工程内评估这些规约的方法并且保存着所有类的相互引用,这些类都实现了 GrailsClass 接口.

一个 GrailsClass 代表着一个物理的Grails资源,比如一个控制器或者一个标签库。如果要获取所有 GrailsClass 实例,你可以这样:

application.allClasses.each { println it.name }

GrailsApplication 实例中有一些特殊的属性可以方便的操作你感兴趣的人工制品(artefact)类型,比如你要获取所有控制器的类,可以如此:

application.controllerClasses.each { println it.name }

这些动态方法的规约如下:

GrailsClass 接口本身也提供了很多有用的方法以允许你进一步的评估和了解这些规约,他们包括:

完整的索引请参考 javadoc API.

12.6 参与构建事件

安装后进行配置和参与升级操作

Grails插件可以在安装完后进行配置并且可以参与应用的升级过程(通过 upgrade命令),这是由scripts目录下两个特定名称的脚本来完成的: - _Install.groovy_Upgrade.groovy.

_Install.groovy 是在插件安装完成后被执行的,而 _Upgrade.groovy 是用户每次通过 upgrade 命令来升级他的应用时被执行的.

这些是一个普通的 Gant 脚本,因此你完全可以使用Gant的强大特性。另外 pluginBasedir 被加入到Gant的标准变量中,其指向安装插件的根目录。

以下的 _Install.groovy 示例脚本将在 grails-app 目录下创建一个新的目录,并且安装一个配置模板,如下:

Ant.mkdir(dir:"${basedir}/grails-app/jobs")
Ant.copy(file:"${pluginBasedir}/src/samples/SamplePluginConfiguration.groovy",
         todir:"${basedir}/grails-app/conf")

// To access Grails home you can use following code: // Ant.property(environment:"env") // grailsHome = Ant.antProject.properties."env.GRAILS_HOME"

脚本事件

将插件和命令行的脚本事件关联起来还是有可能的,这些事件在执行Grails的任务和插件事件的时候被触发。

比如你希望在更新的时候,显示更新状态(如"Tests passed", "Server running"),并且创建文件或者人工制品。

一个插件只能通过 Events.groovy 脚本来监听那些必要的事件。 更多详细信息请参考 Hooking into Events.

12.7 运行时配置中的钩子Hooking into Runtime Configuration

Grails提供了很多的钩子函数来处理系统的不同部分,并且通过惯例的形式来执行运行时配置。

跟Grails的Spring配置进行交互

首先你可以使用 doWithSpring 闭包来跟Grails运行时的配置进行交互,例如下面的代码片段是取自于Grails核心插件 i18n的一部分:

import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;

class I18nGrailsPlugin {

def version = 0.1

def doWithSpring = { messageSource(ReloadableResourceBundleMessageSource) { basename = "WEB-INF/grails-app/i18n/messages" } localeChangeInterceptor(LocaleChangeInterceptor) { paramName = "lang" } localeResolver(CookieLocaleResolver) } }

这个插件建立起了Grails messageSource bean和一对其他beans以管理Locale解释和更改。它使用 Spring Bean Builder 语法。

参与web.xml的生成

Grails是在加载的时候生成 WEB-INF/web.xml 文件,因此插件不能直接修改此文件,但他们可以参与此文件的生成。 本质上一个插件可以通过 doWithWebDescriptor 闭包来完成此功能,此闭包的参数是 web.xml 是作为 XmlSlurper GPathResult类型传入的.

考虑如下来自 ControllersPlugin的示例:

def doWithWebDescriptor = { webXml ->
        def mappingElement = webXml.'servlet-mapping'
        mappingElement + {
                'servlet-mapping' {
                        'servlet-name'("grails")
                        'url-pattern'("*.dispatch")
                }
        }
}

此处插件得到最后一个 <servlet-mapping>元素的引用, 并且在其后添加Grails' servlet,这得益于XmlSlurper可以通过闭包以编程的方式修改XML的能力。

在初始化完毕后进行配置

有时候在Spring的 ApplicationContext 被创建以后做一些运行时配置是有意义的,这种情况下,你可以定义 doWithApplicationContext 闭包,如下例:

class SimplePlugin {
     def name="simple"
     def version = 1.1

def doWithApplicationContext = { appCtx -> SessionFactory sf = appCtx.getBean("sessionFactory") // do something here with session factory } }

12.8 运行时添加动态方法

基础知识

Grails插件允许你在运行时注册Grails管辖类或者其他类的动态方法,但新的方法只能通过 doWithDynamicMethods 闭包来增加。

对Grails管辖类来说,比如controllers、tag libraries等等,你可以增加方法,构造函数等,这是通过 ExpandoMetaClass 机制做到的,比如访问每个控制器的 MetaClass的代码如下所示:

class ExamplePlugin {
  def doWithDynamicMethods = { applicationContext ->
        application.controllerClasses.each { controllerClass ->
             controllerClass.metaClass.myNewMethod = {-> println "hello world" }
        }
  }
}

此处我们通过隐含的application对象来获取所有控制器类的MetaClass实例,并且为每一个控制器增加一个 myNewMethod 的方法。 或者,你已经知道要处理的类的类型了,那你只需要在此类的 metaClass 属性上增加一个方法即可,代码如下:

class ExamplePlugin {

def doWithDynamicMethods = { applicationContext -> String.metaClass.swapCase = {-> def sb = new StringBuffer() delegate.each { sb << (Character.isUpperCase(it as char) ? Character.toLowerCase(it as char) : Character.toUpperCase(it as char)) } sb.toString() }

assert "UpAndDown" == "uPaNDdOWN".swapCase() } }

此例中,我们直接在 java.lang.StringmetaClass 上增加一个新的 swapCase 方法.

跟ApplicationContext交互

doWithDynamicMethods 闭包的参数是Spring的 ApplicationContext 实例,这点非常有用,因为这允许你和该应用上下文实例中的对象进行交互。比如你打算实现一个跟Hibernate交互的方法,那你可以联合着 HibernateTemplate来使用SessionFactory 例,代码如下:

import org.springframework.orm.hibernate3.HibernateTemplate

class ExampleHibernatePlugin {

def doWithDynamicMethods = { applicationContext ->

application.domainClasses.each { domainClass ->

domainClass.metaClass.static.load = { Long id-> def sf = applicationContext.sessionFactory def template = new HibernateTemplate(sf) template.load(delegate, id) } } } }

另外因为Spring容器具有自动装配和依赖注入的能力,你可以在运行时实现更强大的动态构造器,此构造器使用applicationContext来装配你的对象及其依赖:

class MyConstructorPlugin {

def doWithDynamicMethods = { applicationContext -> application.domainClasses.each { domainClass -> domainClass.metaClass.constructor = {-> return applicationContext.getBean(domainClass.name) } }

} }

这里我们实际做的是通过查找Spring的原型beans(prototyped beans)来替代缺省的构造器。

12.9 参与自动重载

监控资源的改变

通常来讲,当资源发生改变的时候,监控并且重新加载这些变化是非常有意义的。这也是Grails为什么要在运行时实现复杂的应用程序重新加载。查看如下Grails的 ServicesPlugin的一段简单的代码片段:

class ServicesGrailsPlugin {
    …
    def watchedResources = "file:./grails-app/services/*Service.groovy"

… def onChange = { event -> if(event.source) { def serviceClass = application.addServiceClass(event.source) def serviceName = "${serviceClass.propertyName}" def beans = beans { "$serviceName"(serviceClass.getClazz()) { bean -> bean.autowire = true } } if(event.ctx) { event.ctx.registerBeanDefinition(serviceName, beans.getBeanDefinition(serviceName)) } } } }

首先定义了 watchedResources 集合,此集合可能是String或者String的List,包含着要监控的资源的引用或者模式。 如果要监控的资源是Groovy文件,那当它被改变的时候,此文件将会自动被重新加载,而且被传给 onChange 闭包的参数 event .

event 对象定义了一些有益的属性:

通过这些对象,你可以评估这些惯例,而且基于这些惯例你可以将这些变化适当的应用到 ApplicationContext 中, 在上述的"Services"示例中,当一个service类变化时,一个新的service类被重新注册到 ApplicationContext 中.

影响其他插件

当一个插件变化时,插件不但要有相应地反应,而且有时还会“影响”另外的插件。

以Services 和 Controllers插件为例. 当一个service被重新加载的时候,除非你也重新加载controllers,否则你将加载过的service自动装配到旧的controller类的时候,将会发生问题。.

为了避免这种情况发生,你可以指定将要受到“影响”的另外一个插件,这意味着当一个插件监测到改变的时候,它将先重新加载自身,然后重新加载它所影响到的所有插件。看 ServicesGrailsPlugin的代码片段:

def influences = ['controllers']

观察其他插件

如果你想观察一个特殊的插件的变化但又不需要监视插件的资源,那你可以使用"observe"属性:

def observe = ["hibernate"]

在此示例中,当一个Hibernate的领域类变化的时候,你将收到从hibernate插件传递过来的事件。 你也可以使用一个通配符查看所有加载的插件:

def observe = ["*"]

Logging plugin不仅如此,当应用运行时它都能添加 log 属性到 任何 插件库。

12.10 理解插件加载的顺序

Controlling Plug-in Dependencies

插件经常依赖于其他已经存在的插件,并且也能调整这种依赖. 为了做到这点,一个插件可以定义两个属性,首先是 dependsOn.让我们看看Grails Hibernate插件的代码片段:

class HibernateGrailsPlugin {
        def version = 1.0
        def dependsOn = [dataSource:1.0,
                         domainClass:1.0,
                         i18n:1.0,
                         core: 1.0]

}

如上述示例所演示的,Hibernate插件依赖于4个插件: dataSource , domainClass, i18ncore.

根本上讲,这些被依赖的插件将先被加载,接着才是Hibernate插件,如果这些被依赖的插件没有加载,那么Hibernate也不会加载。

dependsOn属性也支持一个小型的表达语言指定版本范围。以下是一些简单的语法例子:

def dependsOn = [foo:"* > 1.0"]
def dependsOn = [foo:"1.0 > 1.1"]
def dependsOn = [foo:"1.0 > *"]

当使用*通配符的时候,它表示"任何"版本。 The expression syntax also excludes any suffixes such as -BETA, -ALPHA etc. so for example the expression "1.0 > 1.1" would match any of the following versions:

Controlling Load Order

如果所依赖的插件不能被解析的话,则依赖于此的插件将被放弃并且不会被加载,这就是所谓的“强”依赖。 然而我们可以通过使用 loadAfter来定义一个“弱”依赖,示例如下:

def loadAfter = ['controllers']

此处如果 controllers 插件存在的话,插件将在controllers之后被加载,否则的话将被单独加载. 插件也可以适应于其他已存在的插件,以Hibernate插件的 doWithSpring闭包代码为例:

if(manager?.hasGrailsPlugin("controllers")) {
        openSessionInViewInterceptor(OpenSessionInViewInterceptor) {
                flushMode = HibernateAccessor.FLUSH_MANUAL
                sessionFactory = sessionFactory
        }
        grailsUrlHandlerMapping.interceptors << openSessionInViewInterceptor
  }

这里,controllers插件如果被加载的话,Hibernate插件仅仅注册一个OpenSessionInViewInterceptor 变量manager是 GrailsPluginManager interface 接口的一个实例,并且提供同其他插件交互的方法,而且 GrailsPluginManager 本身存在与任何一个插件中。

13. Web服务

Web服务就是让你的web应用提供一套web API,通常用SOAPREST来实现。 .

13.1 REST

REST就本身而言不是一种技术,而是一种架构模式。is not really a technology in itself, but more an architectural pattern. REST非常简单,以普通XML或JSON作为通信机制,结合可以表现底层系统状态的URL形式和 HTTP方法如 GET, PUT, POST和 DELETE.

每一个HTTP方法映射到一个action,如用GET方法获取数据,用PUT方法创建数据,用POST更新数据等等。在这个意义上 REST非常适合 CRUD.

URL形式

要用Grails实现REST,第一步就是提供REST形式的URL映射 URL映射:

static mappings = {
   "/product/$id?"(resource:"product")
}

这就将URI /product 映射到 ProductController. 在controller内部每个HTTP方法,GET,PUT,POST和DELETE都映射到一个action上,如下表所示:

方法 Action
GET show
PUT update
POST save
DELETE delete

可以通过URL映射机制修改HTTP方法和URL的映射关系:

"/product/$id"(controller:"product"){
    action = [GET:"show", PUT:"update", DELETE:"delete", POST:"save"]
}

但是在这个例子中,Grails并不像前面使用过的resource 参数那样自动提供XML或JSON序列化,除非提供在URL映射中提供parseRequest 参数:

"/product/$id"(controller:"product", parseRequest:true){
    action = [GET:"show", PUT:"update", DELETE:"delete", POST:"save"]
}

XML序列化 - 读取

controller可通过Grails提供的 XML序列化机制 来实现GET方法:

import grails.converters.*
class ProductController {
        def show = {
                if(params.id && Product.exists(params.id)) {
                        def p = Product.findByName(params.id)
                        render p as XML
                }
                else {
                        def all = Product.list()
                        render all as XML
                }
        }
        ..
}

这里,如果参数中指定id,通过id 搜索Product 如果指定id的Product存在,则返回该Product,否则返回所有Product. 这样,如果访问 /products我们会得到所有的Product,如果访问/product/MacBook,我们只获取到一个MacBook记录.

XML序列化 - 更新

为支持PUTPOST 你可以使用 params 对象。Grails中params对象具有读取XML数据包的能力。如下面的XML数据包:

<?xml version="1.0" encoding="ISO-8859-1"?>
<product>
        <name>MacBook</name>
        <vendor id="12">
                <name>Apple</name>
     </vender>
</product>

你可以通过在 数据绑定章节描述过的同样的方法,通过 params 对象来读取XML数据:

def save = {
        def p = new Product(params['product'])

if(p.save()) { render p as XML } else { render p.errors } }

在这个例子中,通过提取 params 对象中的 'product'对应的值,我们可以通过Product的构建器自动创建和绑定XML数据 。 注意这一行:

def p = new Product(params['product'])
这里我们不需要修改任何代码就可以以处理XML数据请求的方法处理表单提交。同样的方法也可以用来处理JSON请求.

如果需要对不同的客户端(REST,HTML等)提供不同的响应,你可以使用 content negotation

The Product object is then saved and rendered as XML, otherwise an error message is produced using Grails' validation capabilities in the form:

<error>
   <message>The property 'title' of class 'Person' must be specified</message>
</error>

13.2 SOAP

Grails通过 XFire 插件来支持SOAP。XFire插件使用流行的XFire SOAP协议栈,它允许你通过特定的expose属性将Grails的 services 作为SOAP服务提供:

class BookService {

static expose=['xfire']

Book[] getBooks(){ Book.list() as Book[] } }

WSDL文件可通过: http://127.0.0.1:8080/your_grails_app/services/book?wsdl获取

更多信息参考XFire插件的wiki 文档

13.3 RSS和Atom

Grails没有直接提供对RSS和Atom的支持. You could construct RSS or ATOM feeds with the render method's XML capability. 可以通过Grails Feeds插件来构建RSS和Atom。改插件使用流行的 ROME 库. 下面是简单使用这个插件的例子:

def feed = {
    render(feedType:"rss", feedVersion:"2.0") {
        title = "My test feed"
        link = "http://your.test.server/yourController/feed"

Article.list().each() { entry(it.title) { link = "http://your.test.server/article/${it.id}" it.content // return the content } } } }

14. Grails和 Spring

这一节适合于高级用户, Spring 框架,和想通过 插件开发来配置Grails的开发人员。

14.1 Grails内部实现

实际上Grails是变相的 Spring MVC 应用. Spring MVC是Spring框架内置的 MVC web开发框架.虽然从易用性来说Spring MVC比不上Struts这样的框架,但它的设计和架构都非常优秀,正适合在其基础之上构建另一个像Grails这样的框架。

Grails在以下方面利用了Spring MVC:

也就是说Grails内嵌Spring并在框架的各个环节上使用Spring.

Grails ApplicationContext

Spring开发人员经常热衷于想知道Grails中的ApplicationContext实例是怎么创建的.基本过程如下:

配置Spring Beans

大部分Grails的配置都是在运行时进行. 每个 插件 都可以配置在上面创建的ApplicationContext对象中注册过的Spring bean. For a reference as to which beans are configured refer to the reference guide which describes each of the Grails plug-ins and which beans they configure.

14.2 配置其他Bean

使用XML

Beans可用过grails-app/conf/spring/resources.xml 来配置. 这个文件是一个标准的Spring配置文件,在Spring Spring参考文档中对如何配置Spring Beans有详细描述。下面是一个简单的例子:

<bean id="myBean" class="my.company.MyBeanImpl"></bean>

配置完毕后, myBean就可以在Grails 控制器,标签库,服务等很多地方引用:

class ExampleController {

def myBean }

引用现有的Spring bean

resources.xml中声明的bean也可以通过约定来引用Grails类 . 比如, 如果你想在你的bean中引用 BookService这样一个service,你可以用如下的代码:

<bean id="myBean" class="my.company.MyBeanImpl">
        <property name="bookService" ref="bookService" />        
</bean>

这个bean本身需要一个 public setter方法,在Groovy中这样定义:

package my.company
class MyBeanImpl {
        BookService bookService
}

或在Java中:

package my.company;
class MyBeanImpl {
        private BookService bookService;
        public void setBookService(BookService theBookService) {
                this.bookService = theBookService;
        }
}

既然大部分Grails配置都是在运行时通过约定机制来完成,大部分bean并不需要声明, 但仍然可以在Spring应用中进行引用. 如你需要引用一个Grails DataSource 你可以这样:

<bean id="myBean" class="my.company.MyBeanImpl">
        <property name="bookService" ref="bookService" />        
        <property name="dataSource" ref="dataSource" />
</bean>

或者你需要引用Hibernate SessionFactory:

<bean id="myBean" class="my.company.MyBeanImpl">
        <property name="bookService" ref="bookService" />        
        <property name="sessionFactory" ref="sessionFactory" />
</bean>

所有提供的bean既说明可参考插件开发文档.

使用Spring DSL

如果你想使用Grails提供的 Spring DSL ,你必须创建grails-app/conf/spring/resources.groovy 文件,定义一个 beans属性块:

beans = {
        // 定义的beans
}

同样在的配置可以应用于XML例子:

beans = {
        myBean(my.company.MyBeanImpl) {
                bookService = ref("bookService")
        }  
}

这样做最大的好处是你能够在bean的定义中混合各种逻辑,如基于 environment:

import grails.util.*
beans = {
        switch(GrailsUtil.environment) {
                case "production":
                        myBean(my.company.MyBeanImpl) {
                                bookService = ref("bookService")
                        }

break case "development": myBean(my.company.mock.MockImpl) { bookService = ref("bookService") } break } }

14.3 运行时Spring与Beans DSL

Grails提供BeanBuilder的目的是提供一种简化的方法来关联使用Spring的各中依赖关系.

这是因为Spring的常规配置方法(通过XML)在本质上是静态的,除了通过程序方式来动态产生XML配置文件外,很难 在运行时修改和添加程序配置。而且这种方法非常繁琐,也容易出错. Grails的BeanBuilder 改变了这一点,它可以让你在运行时通过系统属性和环境属性来动态改变程序逻辑.

这使得程序代码动态适配它的环境,避免不必要的重复代码(如在Spring中为测试环境,开发环境和生产环境做不同的配置)

BeanBuilder 类

Grails提供了 grails.spring.BeanBuilder 类使用动态Groovy来创建bean的声明. 基本点如下:

import org.apache.commons.dbcp.BasicDataSource
import org.codehaus.groovy.grails.orm.hibernate.ConfigurableLocalSessionFactoryBean;
import org.springframework.context.ApplicationContext;

def bb = new grails.spring.BeanBuilder()

bb.beans { dataSource(BasicDataSource) { driverClassName = "org.hsqldb.jdbcDriver" url = "jdbc:hsqldb:mem:grailsDB" username = "sa" password = "" } sessionFactory(ConfigurableLocalSessionFactoryBean) { dataSource = dataSource hibernateProperties = [ "hibernate.hbm2ddl.auto":"create-drop", "hibernate.show_sql":true ] } }

ApplicationContext appContext = bb.createApplicationContext()

插件grails-app/conf/spring/resources.groovy 文件中你不需要创建一个BeanBuilder实例, 它在 doWithSpringbeans块中都隐式存在.

上面这个例子说明了如果使用 BeanBuilder类来配置某个特定的Hibernate数据源。

实际上,每个方法调用( dataSourcesessionFactory 调用) 都映射到Spring中的bean的名字. 方法的第一个参数是bean的class名字, 最后一个参数是一个块(block). 在块内部可以用标准的Groovy语法设置bean的属性。

通过bean的名字自动查找bean的引用. 通过上面的sessionFactory bean解析dataSource可以看点这一点。

也可以通过builder设置一些与bean管理相关的特殊的bean属性,如:

sessionFactory(ConfigurableLocalSessionFactoryBean) { bean ->
    bean.autowire = 'byName'       // Autowiring behaviour. The other option is 'byType'. [autowire]
    bean.initMethod = 'init'       // Sets the initialisation method to 'init'. [init-method]
    bean.destroyMethod = 'destroy' // Sets the destruction method to 'destroy'. [destroy-method]
    bean.scope = 'request'         // Sets the scope of the bean. [scope]
    dataSource = dataSource
    hibernateProperties = [ "hibernate.hbm2ddl.auto":"create-drop",
                            "hibernate.show_sql":true  ]
}

括号中的字符串对应于Spring XML定义中相应的bean 属性名。

在Spring MVC中使用BeanBuilder

如果想在Spring MVC中使用BeanBuilder,你必须确保grails-spring-<version>.jar 包含在classpath中. 还要在/WEB-INF/web.xml文件中做如下设置:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext.groovy</param-value>
</context-param>
<context-param>
    <param-name>contextClass</param-name>
    <param-value>org.codehaus.groovy.grails.commons.spring.GrailsWebApplicationContext</param-value>
</context-param>

然后在创建 /WEB-INF/applicationContext.groovy文件并配置如下:

beans {
        dataSource(org.apache.commons.dbcp.BasicDataSource) {
        driverClassName = "org.hsqldb.jdbcDriver"
        url = "jdbc:hsqldb:mem:grailsDB"
        username = "sa"
        password = ""
    }
}

从文件系统中加载bean定义

你可以使用BeanBuilder并使用下面的语法 来加载在外部Groovy脚本中定义的bean:

def bb = new BeanBuilder()
bb.loadBeans("classpath:*SpringBeans.groovy")

def applicationContext = bb.createApplicationContext()

这里BeanBuilder 将加载在classpath中以SpringBeans.groovy结尾的Groovy文件并将它们 解析成bean的定义.这里是一个范例脚本文件:

beans {
    dataSource(BasicDataSource) {
        driverClassName = "org.hsqldb.jdbcDriver"
        url = "jdbc:hsqldb:mem:grailsDB"
        username = "sa"
        password = ""
    }
    sessionFactory(ConfigurableLocalSessionFactoryBean) {
        dataSource = dataSource
        hibernateProperties = [ "hibernate.hbm2ddl.auto":"create-drop",
                                "hibernate.show_sql":true  ]
    }
}

绑定变量

如果从脚本中加载bean,可以通过创建Groovy Binding对象来实现绑定:

def binding = new Binding()
binding.foo = "bar"

def bb = new BeanBuilder() bb.binding = binding bb.loadBeans("classpath:*SpringBeans.groovy")

def ctx = bb.createApplicationContext()

14.4 BeanBuilder DSL

使用构建器参数

可以通过在bean的class和最后一个closure之间定义的方法来定义构建器参数:

bb.beans {
    exampleBean(MyExampleBean, "firstArgument", 2) {
        someProperty = [1,2,3]
    }
}

配置 BeanDefinition (使用工厂方法)

传给closure的第一个参数是一个bean配置对象引用,你可以使用它来配置工厂方法,调用 AbstractBeanDefinition 的方法:

bb.beans {
    exampleBean(MyExampleBean) { bean ->
        bean.factoryMethod = "getInstance"
        bean.singleton = false
        someProperty = [1,2,3]
    }
}

你也可以通过bean 定义方法的返回值来配置bean:

bb.beans {
    def example = exampleBean(MyExampleBean) {
        someProperty = [1,2,3]
    }
    example.factoryMethod = "getInstance"
}

使用工厂bean(Factory beans)

Spring提供了工厂bean的概念,即bean不是从class创建,而是由这些工厂创建 defines the concept of factory beans and often a bean is created not from a class, but from one of these factories. 在这种情况下bean没有class,你必须将工厂bean的名字传给定义的bean:

bb.beans {
    myFactory(ExampleFactoryBean) {
        someProperty = [1,2,3]
    }
    myBean(myFactory) {
        name = "blah"
    }
}

注意:上面的例子中我们传递的是 myFactory bean而不是一个clas. 另一个常见的需求是提供调用工厂bean的工厂方法名,可以用下面的Groovy语法做到这一点:

bb.beans {
    myFactory(ExampleFactoryBean) {
        someProperty = [1,2,3]
    }
    myBean(myFactory:"getInstance") {
        name = "blah"
    }
}

这里 ExampleFactoryBeangetInstance会被调用来创建myBean bean.

运行时创建 bean的引用

有时只有在运行是才知道需要创建的bean的名字. 在这种情况情况下你可以使用字符串替换来实现动态调用:

def beanName = "example"
bb.beans {
    "${beanName}Bean"(MyExampleBean) {
        someProperty = [1,2,3]
    }
}

在这个例子中,使用早先定义的 beanName 变量来调用bean.

另外, 可使用ref来动态引用在运行时才知道的bean的名字,如下面的代码:

def beanName = "example"
bb.beans {
    "${beanName}Bean"(MyExampleBean) {
        someProperty = [1,2,3]
    }
    anotherBean(AnotherBean) {
        example = ref("${beanName}Bean")
    }
}

这里AnotherBean属性通过运行时对 exampleBean的引用来设置 . 也可以通过 ref 来引用在父 ApplicationContext定义的bean, ApplicationContextBeanBuilder的构建器中提供:

ApplicationContext parent = ...//
der bb = new BeanBuilder(parent)
bb.beans {
    anotherBean(AnotherBean) {
        example = ref("${beanName}Bean", true)
    }
}

这里第二个参数 true 指定了在父ApplicationContext中查找bean的引用.

使用匿名内部bean

你可以通过将属性块付给bean的一个属性来使用匿名内部bean,这个属性块提供一个bean的类型参数:

bb.beans {
    marge(Person.class) {
        name = "marge"
        husband =  { Person p ->
            name = "homer"
            age = 45
            props = [overweight:true, height:"1.8m"]
        }
        children = [bart, lisa]
    }
    bart(Person) {
        name = "Bart"
        age = 11
    }
    lisa(Person) {
        name = "Lisa"
        age = 9
    }
}

在上面的例子中我们将marge bean的husband属性 赋值一个属性块(参数类型是Person)的方式创建一个内部bean引用. 如果你有一个工厂bean你也可以忽略类型参数,直接使用 传进进来的bean的定义:

bb.beans {
    personFactory(PersonFactory.class)
    marge(Person.class) {
        name = "marge"
        husband =  { bean ->
            bean.factoryBean = "personFactory"
            bean.factoryMethod = "newInstance"
            name = "homer"
            age = 45
            props = [overweight:true, height:"1.8m"]
        }
        children = [bart, lisa]
    }
}

抽象bean和父子bean定义

要创建一个抽象bean,定义一个没有class 的bean:

class HolyGrailQuest {
    def start() { println "lets begin" }
}
class KnightOfTheRoundTable {
    String name
    String leader
    KnightOfTheRoundTable(String n) {
        this.name = n
    }
    HolyGrailQuest quest

def embarkOnQuest() { quest.start() } }

def bb = new grails.spring.BeanBuilder() bb.beans { abstractBean { leader = "Lancelot" } … }

这里定义了一个抽象bean,这个bean有一个属性 leader,属性值为 "Lancelot". 要使用抽象bean,只要将它设为要定义的bean的父即可:

bb.beans {
    …
    quest(HolyGrailQuest)
    knights(KnightOfTheRoundTable, "Camelot") { bean ->
        bean.parent = abstractBean
        quest = quest
    }
}

当使用父bean时,你必须在设置其他属性前设置parent属性!

如果你要定义一个具有class的抽象bean,可以这样:

def bb = new grails.spring.BeanBuilder()
bb.beans {
    abstractBean(KnightOfTheRoundTable) { bean ->
        bean.'abstract' = true
        leader = "Lancelot"
    }
    quest(HolyGrailQuest)
    knights("Camelot") { bean ->
        bean.parent = abstractBean
        quest = quest
    }
}

上面例子中我们创建了抽象 KnightOfTheRoundTable 并将它的参数设为abstract. 接下来我们定义了一个knights bean,没有 定义它的class,而是继承父bean中定义的class。

使用 Spring命名空间

从Spring 2.0开始,通过XML命名空间可以更方便的使用Spring的各种特性. 如果使用 BeanBuilder, 你可以先声明所要使用的Spring命名空间:

xmlns context:"http://www.springframework.org/schema/context"

然后调用与命名空间名称和属性匹配的方法:

context.'component-scan'( 'base-package' :"my.company.domain" )

通过Spring的命名空间可以做很多有用的事,比如查找JNDI资源:

xmlns jee:"http://www.springframework.org/schema/jee"

jee.'jndi-lookup'(id:"dataSource", 'jndi-name':"java:comp/env/myDataSource")

上面的例子通过查找JNDI创建一个 dataSourcebean对象. 通过Spring命名空间,你可以在BeanBuilder中直接访问Spring AOP功能 比如下面的代码:

class Person {
 int age;
 String name;

void birthday() { ++age; } } class BirthdayCardSender { List peopleSentCards = [] public void onBirthday(Person person) { peopleSentCards << person } }

你可以定义一个AOP aspect pointcut来监测对 birthday() 方法的所有调用:

xmlns aop:"http://www.springframework.org/schema/aop"
fred(Person) { 
 name = "Fred" 
 age = 45 
}

birthdayCardSenderAspect(BirthdayCardSender)

aop { config("proxy-target-class":true) { aspect( id:"sendBirthdayCard",ref:"birthdayCardSenderAspect" ) { after method:"onBirthday", pointcut: "execution(void ..Person.birthday()) and this(person)" } } }

14.5 属性占位符配置

通过扩展的Spring的PropertyPlaceholderConfigurer,Grails支持属性占位符配置,这和 外部配置配合使用非常有用。 .

Settings defined in either ConfigSlurper scripts of Java properties files can be used as placeholder values for Spring configuration in grails-app/conf/spring/resources.xml. For example given the following entries in grails-app/conf/Config.groovy (or an externalized config):

database.driver="com.mysql.jdbc.Driver"
database.dbname="mysql:mydb"

接着在 resources.xml中用${..}语法定义占位符:

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
   <property name="driverClassName"><value>${database.driver}</value></property>
   <property name="url"><value>jdbc:${database.dbname}</value></property>
 </bean>

14.6 属性重载

通过扩展的Spring PropertyOverrideConfigurer,Grails提供了对属性重载配置的支持, 外部配置配合使用非常有用。 .

你可以提供一个 ConfigSlurper脚本文件,该文件中定义了一个 beans 属性块,属性块中定义的属性值会覆盖bean中定义的属性值:

beans {
   bookService.webServiceURL = "http://www.amazon.com"
}

重载的属性应用在Spring ApplicationContext 创建之前. 格式如下:

[bean name].[property name] = [value]

你也可以提供一个常规的Java属性文件,属性文件中的每个条目加上beans前缀:

beans.bookService.webServiceURL=http://www.amazon.com

15. Grails 与 Hibernate

如果 GORM (Grails Object Relational Mapping)没有你想象的那么足够灵活,作为选择,你可以使用Hibernate映射你的domain类. 要做到这点,需要在你项目的grails-app/conf/hibernate目录创建一个hibernate.cfg.xml文件并为你的domain类对应HBM映射文件 .

更多关于这方面的信息,请查看Hibernate站点的文件映射

这允许你映射Grails domain类适用于更广的遗留系统并更加灵活的创建数据库模式 .

Grails也允许你在Java中编写domain类或重用以存在的domain model,这些都通过使用Hibernate来映射 . 你需要做的是放置必须的hibernate.cfg.xml 文件和对应的映射文件在grails-app/conf/hibernate目录中 .

另外,令人兴奋的是你仍然可以调用GORM中所有动态之久和查询方法 !

15.1 通过Hibernate注解映射

Grails 也支持通过Hibernate的Java 5.0注解支持来创建domain类映射. 为了做到这点,你需要通过设置DataSource 中的configClass属性告诉Grails你要使用注解配置,如下 :

import org.codehaus.groovy.grails.orm.hibernate.cfg.GrailsAnnotationConfiguration
dataSource {
	configClass = GrailsAnnotationConfiguration.class
   … // remaining properties
}

这就是它的配置!确保你安装了Java 5.0,因为这需要使用注解. 现在,为了创建一个注解类,我们在src/java中简单的创建一个新的Java类并使用EJB 3.0规范来定义注解(详情 参考Hibernate Annotations Docs):

package com.books;
@Entity
public class Book {
    private Long id;
    private String title;
    private String description;
    private Date date;

@Id @GeneratedValue public Long getId() { return id; }

public void setId(Long id) { this.id = id; }

public String getTitle() { return title; }

public void setTitle(String title) { this.title = title; }

public String getDescription() { return description; }

public void setDescription(String description) { this.description = description; } }

一旦完成,你需要使用Hibernate sessionFactory注册这个类 ,为了做到这点,你需要添加 如下的grails-app/conf/hibernate/hibernate.cfg.xml 文件:

<!DOCTYPE hibernate-configuration SYSTEM
  "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory>
        <mapping package="com.books" />
        <mapping class="com.books.Book" />
    </session-factory>
</hibernate-configuration>

当Grails加载时,会注册这个类必要的动态方法 . 查看Scaffolding了解 Hibernate domain 类中可以做的其他事情.

15.2 进一步阅读

Grails提交者, Jason Rudolph,花了许多时间写了许多关于通过自定义Hibernate使用Grails:

16. 脚手架

根据指定的领域类,脚手架为你自动生成一个领域相关的完整应用,包括:

启动脚手架

让脚手架生效的最简单方法是通过设置 scaffold 属性。 以领域类 Book 为例,你需要在其控制器中设置 scaffold 属性为true就可以了,代码如下:

class BookController {
   def scaffold = true
}

上述代码可以正常工作是因为控制器 BookController 命名跟领域类 Book 相一致, 如果我们想脚手架对特定的领域类使用,你可以直接将特定的领域类赋值给scaffold属性,代码如下:

def scaffold = Author

设置完毕后,如果你运行grails应用,那么那些必要的动作和视图都将在运行期间自动生成。 根据脚手架的动态机制,以下一些动作将被动态实现:

即基本的CRUD接口将被自动生成。 为了访问以上示例生成的接口,只需要去 http://localhost:8080/app/book

如果你倾向于使用 基于Hibernate映射 的Java领域模型,你依然可以使用脚手架, 只需简单的导入必要的类,并且将此类赋值给scaffold属性即可。

动态脚手架

注意当使用scaffold属性的时候,Grails并不是通过代码模板或者代码生成来实现脚手架功能,因此你照样可以在被脚手架过的控制器中增加自己的动作,来跟脚手架过的动作进行交互。比如,在下面的示例中, changeAuthor 可以重新定向到一个并不存在的 show 的动作:

class BookController {
   def scaffold = Book

def changeAuthor = { def b = Book.get( params["id"] ) b.author = Author.get( params["author.id"] ) b.save()

// redirect to a scaffolded action redirect(action:show) } }

当然必要的时候,你也可以使用自己的动作来重写被脚手架过的动作,代码如下:

class BookController {
   def scaffold = Book

// overrides scaffolded action to return both authors and books def list = { [ "books" : Book.list(), "authors": Author.list() ] } }

所有这些就是所谓的“动态脚手架”,在这里CRUD接口将在运行期间动态生成。不过Grails同样也支持所谓的“静态”脚手架,这将在接下来的章节中讨论。

自定义生成的视图

Grails生成的视图中,有些表单能智能地适应 验证约束. 如下面代码所示,只需要简单地重新排列生成器(builder)中约束的顺序,就可以改变其在视图中出现的顺序:

def constraints = {
      title()
      releaseDate()
}

你也可以通过使用 inList 约束来生成一个列表(list)而不是简单的文本输入框(text input):

def constraints = {
      title()
      category(inList:["Fiction", "Non-fiction", "Biography"])
      releaseDate()
}

或者通过基于数字的 range 约束来生成列表 :

def constraints = {
        age(range:18..65)
}

通过约束来限制大小(size)也可以影响生成的视图中可以输入的字符数:

def constraints = {
        name(size:0..30)
}

生成控制器和视图

以上的脚手架特性虽然很有用,但是在现实世界中有可能需要自定义逻辑和视图。Grails允许你通过使用命令行的方式,来生成一个控制器和相关视图(跟脚手架所做的事情差不多)。为了生成控制器,只需要输入:

grails generate-controller Book

或者为了生成视图,只需输入:

grails generate-views Book

或者生成控制器和视图,只需输入:

grails generate-all Book

如果你的领域类有包名或者从 Hibernate映射的类 来生成,那需要记住一定要用类的全名(包名+类名),如下:

grails generate-all com.bookstore.Book

定制脚手架模板

使用的Grails自动生成的控制器和试图模板可以自己定制安装模板通过 install-templates 这个命令.

17. 部署

Grails可以使用很多种方式来部署,每一种都有它的缺点和优点.

"grails run-app"

现在,你已经非常属性这个方式 , 因为它是在部署阶段运行应用程序非常普通的方法. 内置的Jetty服务器被启动并加载来自开发时的应用程序源代码, 因此,允许 获取应用程序文件的改变.

这种方式在产品部署时不被推荐,因为性能非常差。 检查和加载改变在服务器端是非常大的开销. 话虽如此, grails prod run-app 移除每次请求开支 并允许你控制进行定期检查的频率.

设置系统属性"disable.auto.recompile" 为true彻底禁止常规检查 ,属性"recompile.frequency"控制着频率 .后者应该设置为你想要每次检查之间的秒数 . 默认为 3.

"grails run-war"

这非常类似于上面的选项,但 Jetty 运行依靠的是打包的WAR文件而不是开发时源代码. 热重载被禁止, 因此你无需在别处部署WAR文件而获得良好性能.

WAR 文件

当涉及到它时, 目前的java基本设备都要求web应用程序被当做WAR文件部署,因此,这是目前为止最常见的方式Grails应用程序用于生产部署 . 创建WAR文件只需要简单的执行war 命令:

grails war

这里也有许多方式用于定制WAR文件的创建 . 例如, 你可以指定命令路径 (任何现对和绝对) ,这会指定在哪里放置文件和给定什么样的名字:

grails war /opt/java/tomcat-5.5.24/foobar.war

作为选择,你可以在grails-app/conf/BuildConfig.groovy添加一行来改变默认的位置和文件名 :

grails.war.destFile = "foobar-prod.war"

当然,任何命令行参数都优先于这个设置.

它也可以控制在WAR文件包含什么样的类库 , 例如,如果你需要在共享文件夹中避免类库冲突.默认行为是包含所有Grails所需要的全部类库 ,添加的任何类库都被包含在 "lib"目录, 添加的任何类库都被包含在应用程序的"lib"目录.作为默认行为的选择,通过使用Ant包含模式或闭包包含AntBuilder语法的任一种设置Config.groovy的grails.war.dependenciesgrails.war.java5.dependencies属性来明确指定WAR文件所包含的完整的类库列表,闭包的调用来自Ant "copy"阶段 ,因此只有像 "fileset"的元素可以被包含,尽管每个项目都包含在模式列表中.任何闭包或模式被分配给后面的属性被包含在增加的grails.war.dependencies只在你运行在JDK1.5或以上 .

注意这些问题: 假如任何Grails依赖的类库丢失,应用程序肯定会失败,这里有个示例包含了标准Grails依赖所需的小子集:

def deps = [
    "hibernate3.jar",
    "groovy-all-*.jar",
    "standard-${servletVersion}.jar",
    "jstl-${servletVersion}.jar",
    "oscache-*.jar",
    "commons-logging-*.jar",
    "sitemesh-*.jar",
    "spring-*.jar",
    "log4j-*.jar",
    "ognl-*.jar",
    "commons-*.jar",
    "xstream-1.2.1.jar",
    "xpp3_min-1.1.3.4.O.jar" ]

grails.war.dependencies = { fileset(dir: "libs") { deps.each { pattern -> include(name: pattern) } } }

这个示例只是为了说明属性的语法,假如你想在自己的应用程序中尝试使用它们,应用程序可能不会工作. 你可以在未打包的根目录的 "dependencies.txt"文件中找到Grails所需的依赖列表.你也可以在产生WAR文件的"War.groovy"脚本中找到默认的依赖 - 查看 "DEFAULT_DEPS" 和 "DEFAULT_J5_DEPS" 变量.

2个遗留的配置选项用于grails.war.copyToWebAppgrails.war.resources. 第一个允许你定制来自"web-app"目录的WAR文件包含什么. . 第2个允许你在WAR文件完全创建之前执行任何额外的数据处理.

// This closure is passed the command line arguments used to start the
// war process.
grails.war.copyToWebApp = { args ->
    fileset(dir:"web-app") {
        include(name: "js/**")
        include(name: "css/**")
        include(name: "WEB-INF/**")
    }
}

// This closure is passed the location of the staging directory that // is zipped up to make the WAR file, and the command line arguments. // Here we override the standard web.xml with our own. grails.war.resources = { stagingDir, args -> copy(file: "grails-app/conf/custom-web.xml", tofile: "${stagingDir}/WEB-INF/web.xml") }

应用程序服务器

理想情况下,你可以把通过Grails创建的WAR文件简单的放置于任何应用程序服务器并能马上工作. 不过,事情并没这么简单. Grails 站点 包含最新的Grails测试过的服务器列表, 连同任何其他用让Grails WAR文件工作的额外步骤.