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通过下面的规则来规定:
- 如果只存在一个操作(Action), 控制器(Controller)默认的URI映射为这个。
- 如果定义了
index
操作(Action)用于处理请求,并且没有操作(Action)在 URI/book
中指定 - 作为选择,你还可以明确的通过
defaultAction
属性来设置。 property:
def defaultAction = "list"
6.1.2 控制器(Controller) 与作用域
可用的作用域
作用域本质上就是hash对象,它允许你存储变量。 下面的作用域在控制器(Controller)中可以使用:
- servletContext - 也被称为 application 作用域, 这个作用域允许你在整个web应用程序中共享状态。 servletContext对象为一个 javax.servlet.ServletContext实体
- session - session允许关联某个给定用户的状态,通常使用Cookie把一个session与一位客户关联起来。session对象为一个 HttpSession实体
- request -request对象只允许存储当前的请求对象 。 request对象为一个HttpServletRequest实体
- params - 可变的请求参数map(CGI)。
- flash - 见下文。
存取作用域
作用域可以通过上面的变量名与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)。 否则,像上面的代码,就不会是线程安全的。
上面示例中,books
和 authors
属性在视图中都是可用的。
一个更高级的方式就是 return一个 Spring ModelAndView 类的实体:
import org.springframework.web.servlet.ModelAndViewdef 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.gsp
view (事实上, 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
方法可以选择如下用法之一:
- 同一个控制器(Controller)类中的其他闭包:
// 调用同一个类的login action redirect(action:login)
- 一个控制器(Controller)和一个操作(Action)的名字:
// 重定向到home 控制器(Controller)的index action redirect(controller:'home',action:'index')
- 相对于应用程序上下文路径的一个URI资源:
// 明确的重定向到URI
redirect(uri:"/login.html")
- 或者一个完整的URL:
// 重定向到一个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
title
和author
请求参数将会自动被设置到domain类上. 假如,你需要在一个已存在的实体上执行数据绑定,那么你可以使用 properties 属性:
def save = { def b = Book.get(params.id) b.properties = params b.save() }
这个和使用隐式构造函数是完全一样的效果.
数据绑定和单向关联
如果你有one-to-one
或 many-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
没有顺序的概念, 所以,你引用的books0
和 books1
不能确保关联的顺序在服务器端的正确性,除非你自己应用明确排序 .
如果你使用基于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
的第一圆点前面的前缀参数来隔离唯一的参数绑定. 我们同样可以这样来使用Author
domain类 :
def a = new Author(params['author'])
数据绑定与类型转换错误
有时,当执行数据绑定时,它可能不会将一种指定的String转换为指定的目标类型. 你会得到类型转换错误. Grails 会保留类型转换错误在Grails domain 类的 errors 属性中 . 例如这里:
class Book { … URL publisherURL }
这里,我们有一个Book
domain 类 ,它使用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
在这种情况下,只有firstName
和 lastName
属性将被捆绑.
另一种实现这个的方式是使用 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特性提供了 encodeAsXML 和 encodeAsJSON方法:
def xml = Book.list().encodeAsXML() render xml
更多的XML 列集(Marshalling)信息见REST
自动JSON列集(Marshalling)
Grails同样支持自动列集(Marshalling)为JSON通过同样的机制. 简单替代XML
为JSON
:
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 binding 和 validation 特性 .
声明命令对象
命令对象通常作为一个控制器直接声明在控制器(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 loginServiceString 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 ,因此,如果在群集中使用,要求会话密切关联.