Grails 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通过下面的规则来规定:

  • 如果只存在一个操作(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)。 否则,像上面的代码,就不会是线程安全的。

上面示例中,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 方法可以选择如下用法之一:

  • 同一个控制器(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

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 ,因此,如果在群集中使用,要求会话密切关联.

你可能感兴趣的:(Web,应用服务器,json,grails,groovy)