26Play框架教程2学习笔记
Play框架教程2学习笔记
文章目录
- 1 play框架01
- 1.1 概述
- 1.2 特性
- 1.2.1 无缝集成现有开发环境
- 1.2.2 热重载和修改Bug
- 1.2.3 简单的无状态MVC架构
- 1.2.4 HTTP到代码的映射
- 1.2.5 高效的模板引擎
- 1.2.6 内置JPA支持
- 1.2.7 Full Stack应用框架
- 1.2.8 Play的特性总结
- 2 play框架02
- 2.1 app目录
- 2.2 conf目录
- 2.3 public 的目录
- 2.4 lib目录
- 2.5 project目录
- 2.6 target目录
- 3 play框架03
- 3.1 安装Play
- 3.2 创建项目
- 3.3 运行程序
- 3.4 配置数据库
- 3.5 补充
- 3.6 将play项目导入myeclipse
- 4 play框架04
- 4.1 路由
- 4.1.1 HTTP方法
- 4.1.2 URI表达式
- 4.1.3 定义Java调用
- 4.1.4 404作为Action
- 4.1.5 指定静态参数
- 4.1.6 变量和脚本
- 4.1.7 staticDir:mapping
- 4.1.8 staticFile:mapping
- 4.1.9 虚拟主机
- 4.2 逆向生成URL
- 4.3 设置content type
- 4.4 HTTP内容协商
- 4.3.1 在HTTP头中设置content type
- 4.3.2 自定义格式
- 4.5 关于REST
- 5 play框架05
- 5.1 概述
- 5.2 获取HTTP参数
- 5.2.1 使用Map参数
- 5.2.2 高级HTTP绑定
- 5.2.2.1 简单类型
- 5.2.2.2 日历类型
- 5.2.2.3 文件类型
- 5.2.2.4 数组和集合类型
- 5.2.2.5 POJO对象绑定
- 5.2.3 JPA对象绑定
- 5.2.4 自定义绑定
- 5.3 结果返回
- 5.3.1 返回文本内容
- 5.3.2 返回JSON字符串
- 5.3.3 返回XML字符串
- 5.3.4 返回二进制内容
- 5.3.5 下载附件功能
- 5.3.6 执行模板
- 5.2.7 为模板作用域添加数据
- 5.3.8 更简单方式
- 5.3.9 指定其他模板进行渲染
- 5.3.10 重定向URL
- 5.3.11 自定义Web编码
- 5.4 Action链
- 5.5 拦截器
- 5.5.1 @Before
- 5.5.2 @After
- 5.5.3 @Catch
- 5.5.4 @Finally
- 5.5.5 使用@with注解增加更多拦截器
- 5.6 Session和Flash作用域
- 6 play框架06
- 6.1 模板语法
- 6.1.1 表达式(expression):${…}
- 6.1.2 标签(tag): #{tagName /}
- 6.1.3 引用(action):@{…}或者@@{…}
- 6.1.4 国际化(messages):&{…}
- 6.1.5 注释(comment):
- 6.1.6 脚本(script): %{…}%
- 6.2 模板继承
- 7 play框架07
- 7.1 属性模拟
- 7.2 数据库配置
- 7.3 数据持久化
- 7.4 无状态模型
- 8 play框架08
- 8.1 Job实现
- 8.2 Bootstrap Job
- 8.2.1 应用启动
- 8.2.2 应用停止
- 8.3 Scheduled Job
- 8.4 Job的直接调用
- 9 play框架的请求处理流程
- 9.1 PlayHandler
- 9.2 Invoker与Invocation
- 9.3 Result类
- 9.4 Template类
- 10 play框架的拦截器
- 11 play源码分析
- 11.1总体流程
- 11.2 Start流程
- 11.3 启动HTTP服务
1 play框架01
play框架01–介绍
1.1 概述
Play框架颠覆了臃肿的企业级Java EE规范,以Restful为目标并专注于开发效率,是Java敏捷开发的最佳参考方案。
开发者只要具备Java以及数据库的相关基础知识就可以轻松上手,从而让Web应用开发变得更加容易,提高项目催化速度。
作为Full Stack的Java Web应用框架,Play包括了所有开发中涉及的领域:NIO应用容器,无状态MVC模型,Hibernate数据持久化,
Groovy模板引擎,以及建立Web应用所需要的各种工具类。需要注意的是,这里虽然使用了Groovy,但只是将其作为页面模板语言,
和Freemaker、Velocity使用自己定义的语言是同样的道理。
Groovy的成熟以及它和Java的相似性决定了采用Groovy远远好于定义自己的模板语言。
1.2 特性
1.2.1 无缝集成现有开发环境
Play1.x是基于Java的Web开发框架,允许开发者使用自己常用的集成开发工具(如Eclipse)和类库。
如果读者已经以Java作为开发方向,那么无须进行开发语言、IDE或者类库的切换,要做的就是在更加高效的Java环境中开发Web应用。
1.2.2 热重载和修改Bug
Java在过去因为开发效率低下而臭名昭著,主要是因为其重复和乏味的编译-打包-部署周期。因此在设计框架的时候对这些因素都进行了重新考量,目标是让Play应用的开发过程变得更加高效。
Play框架会自动编译Java源文件,而不用重新启动Web服务器将代码热加载至JVM。这样做的好处是:当代码修改完保存后,框架自动编译并重载修改后的类,只需刷新浏览器就可以查看更改的结果,就像在LAMP或者Rails环境中开发一样。另外一个好处是:开发的时候甚至可以只用简单的文本编辑器,而不使用功能完备的Java IDE进行开发。
1.2.3 简单的无状态MVC架构
一端是数据库,另一端是Web浏览器,为什么我们需要在这两者之间保存状态?
有状态并且基于组件的Java Web框架能够更加容易地保存页面状态,但这同样带来了很多其他的问题:如果用户在新的浏览器窗口中重新打开应用会发生什么?用户按了后退按钮又会是什么结果?
无共享架构是很多Web应用框架所提倡的(ROR,Django等)。由于浏览器变得越来越强大,我们并不需要技巧性地构建HTTP模型来创建伪造的状态,只需在客户端使用Ajax或者离线存储技术就可以很容易地解决状态问题。无共享架构的另一优势是使页面的呈现更加平滑,更容易地实现局部页面更新(或者渐进式的页面处理流程)。
1.2.4 HTTP到代码的映射
如果读者使用过其他的Java Web框架(比如说Struts)可能会发现,这些框架的底层实现其实是对HTTP协议做了进一步封装,
所以它们提供的Java API和自身的理念会让人觉得很不自然。Play框架在设计过程中换了一种思维方式,
即Web应用框架也应该提供完整、直接的方式去访问HTTP————这也是Play框架和其他Java Web框架最根本的差异。
HTTP,Request/Response模式,Rest架构风格,HTTP内容协商(Content–type negotiation),URI等等,
所有这些都是Play框架的主要概念。如果用户需要将URI绑定到指定的Java方法调用,只需要在路由文件中以如下方式进行配置:
GET /clients/{id} Clients.show
如果Ajax,REST以及管理页面之间的“前进/后退”操作是日常开发中需要频繁考虑的需求,
那么Play框架无疑是最佳的选择,因为针对这些问题它都提供了非常优秀的解决方案。
Play是一个完全无状态的,只面向请求/响应的框架,所有HTTP请求都具有相同的处理流程:
1.框架接收HTTP请求。
2.路由组件找到最匹配的规则,接受并处理请求,随后调用相应的Action方法。
3.执行Action中的应用代码。
4.如果需要生成复杂的视图,使用模板文件进行渲染。
5.Action方法的返回结果(HTTP响应代码以及内容)被转换为HTTP响应。
1.2.5 高效的模板引擎
也许读者已经深深地感受到了JSP和表达式语言背后的理念,但是为什么在创建标签库的时候需要如此多的配置文件?为什么不能直接访问底层的模型对象?JSP中太多的限制确实让开发者感到失望,受JSP启发又不被其约束,Play框架提供了自定义的模板引擎机制。
开发者再也不需要编写这些令人厌倦的代码了:
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %><c:choose><c:when test="${emails.unread != null && fn:size(emails.unread)}">You have ${fn:size(emails.unread)} unread email(s)!</c:when><c:otherwise>You have no unread emails!</c:otherwise>
</c:choose>
相信开发者更倾向于用以下方式来书写模板代码:
You have ${emails.unread ?: 'no'} ${emails.unread?.pluralize('email')} !
Play模板引擎使用的表达式语言为Groovy,它提供了与Java一致的语法。
Play主要使用模板机制来渲染HTML,当然也可以生成其他的文档格式,比如e-mail messages,JSON等等。
1.2.6 内置JPA支持
JPA(Java Persistence API)是Java中最简洁的对象关系映射(object-relational mapping即ORM)API。
如果读者以前了解或者使用过JPA,就会发现与其他框架相比,在Play中使用会更加方便。这是因为Play框架对其做了进一步封装,
不需要任何配置,Play会自动开启JPA实体管理器(EM),一旦代码被调用就自动进行持久化操作。
此外,实体如果继承Play提供的play.db.jpa.Model类,操作代码将会更加简洁,更加美观:
public static void messages(int page) {User connectedUser = User.find("byEmail", connected()).first();List<Message> messages = Message.find("user = ? and read = false order by date desc",connectedUser).from(page * 10).fetch(10);render(connectedUser, messages);
}
1.2.7 Full Stack应用框架
Play框架的最初设计受到实际Java Web开发的启发,包含了所有创建主流Web应用所需要的工具:
通过JDBC提供关系数据库支持。
使用Hibernate进行对象关系映射(JPA)。
使用分布式Memcached集成缓存支持。
以JSON或者XML的形式提供web service支持。
基于OpenID的分布式用户信息验证。
Web应用可以部署在任何应用服务器上(Tomcat,Jboss,GAE,Cloud等)。
图像处理API(验证码)。
此外Play还提供了很多实用的模块。开发者可以结合这些模块构建Web应用,
这使得我们可以以更加简单,更加直接的方式重用Java代码、模板以及静态资源(比如JavaScript和CSS文件)。
1.2.8 Play的特性总结
自动编译和重载:当编辑Java文件并保存后,刷新浏览器就能立即查看结果。
使用Play开发不需要手动编译、部署以及重新启动Web服务器等操作。
无状态模型:Play是真正的无共享框架,为REST而准备。它可以将同一个应用的多个实例分别部署在多台服务器上,因而扩展性非常强。
高效的模板引擎:基于表达式语言Groovy的清晰模板引擎,提供了模板的继承、导入以及标签自定义等功能。
快速解决错误:当错误发生时,Play会在浏览器中显示出错代码块并提示问题发生的确切位置。
Full Stack:提供创建Web应用所需的全部功能,集成了Hibernate、OpenID、Memcached等第三方类库。
纯Java:Play采用Java编写代码,可以方便地使用任何Java类库,并且能够非常好地和Eclipse、Netbeans等IDE集成,只需通过命令生成匹配的项目文件即可。
基于非阻塞的IO模型:允许创建基于长轮询和WebSocket的主流Web应用。
有趣并且高效:省去了Java应用重启的时间,提高了应用的开发效率。
2 play框架02
play框架02–细说目录结构
play的目录结构制作的相当精简,以下是从play官网截下的图片:
2.1 app目录
app目录是代码目录,包含了所有的Java或者Scala的源码,一般的“hello-world”sample程序都含有controllers、models、和views三个目录,分别对应MVC三层结构中的:C、M和V;我想这大家都和清楚,大家还可以根据自己的项目需要创建其他的目录,
例如utils、dao等等。例如以下:
如果有需要,你还可以建一个名为“assets”的目录,里面可以放LESS或者CoffeeScript源文件。
注意:这些controllers、models和views等目录可以随着你项目的需要而改变,
例如:你可以写成com.yourcompany.controllers、com.yourcompnay.model和com.yourcompany.views而不必非得写成controllers、models和views。
2.2 conf目录
在这个目录里,放置的都是这个应用的一些配置文件信息,有两个主要的文件:
一个是application.conf:意思很明显,就是整个应用的配置信息,里面会有一些配置的参数。
包括数据库链接中数据源的信息填写,日志打印的级别等信息等等,还可以自定义一些参数。
注意:在conf中,play默认定义的有:数据库信息、应用信息(名字、 Secret key、语言等)、日志;
这三块儿的信息,在conf中直接改后,效果会在应用程序中直接出现。
假如你想一用conf中自定义的配置参数:例如上图中的阿里云相关的信息,你需要在application.conf中定义之后,在程序中使用
Play.configuration.getString("oss.access_id").getOrElse("diSnug5q4zb9y2mq")
来调用。实际上某人的那三块信息也是这么来调用的。
假如你在application.conf中不想定义过多的自定义信息,你也可以写一个自定义的conf文件,然后在application.conf中引用(include “fileName.conf”)如下:
routes:路由。非常重要的部分!使用方法非常简单,在这里定义你需要的rest接口,然后接口后面对应的处理函数。如下图:
2.3 public 的目录
这里放置的都是前端页面相关的信息,例如js、css、json文件、图片等等。
这些目录文件的名字是可以改的,但是引用的时候需要注意目录名字。包括public的名字也是可以改的。前端页面中需要其中的静态文件的话,需要再routes中添加:
然后在前端需要静态文件的地方这么引用:
这里就是用的public目录下images目录中的静态文件。
2.4 lib目录
如果之前你是做J2EE项目的,这个目录你一定清楚,这就是放置其他依赖包的地方。
(当然如果Maven有依赖链接,尽量用Maven的依赖链接)
build.sbt file:
这个文件是整个项目添加依赖包的地方,所有的依赖都写在这里。
如果你是J2EE开发者的话,你一定知道Maven的pom.xml文件,在这里,build.sbt文件就相当于pom.xml的文件。
2.5 project目录
这个目录包含了sbt构建之后的东西:
1、pulgins.sbt:插件sbt
2、build.properties:包含了sbt的版本。
2.6 target目录
target目录包含了应用编译之后的东西,就是编译后的可执行文件。
3 play框架03
play框架03–创建项目
3.1 安装Play
从下载页面下载最新的二进制包,然后在你喜欢的地方解压它。
如果你用的是Windows,最好避免在路径中混入空格。比如c:\play就是个比c:\Documents And Settings\user\play更好的选择。
为了方便操作,你需要添加Play文件夹到你的系统路径中。这样你就不需要在play命令前面敲一大通路径名了。
要想检查安装是否成功,打开一个新的命令行窗口,敲下play
;应该会出来play的基本使用帮助。
3.2 创建项目
现在Play已经安好了,是时候开始写博客应用。创建一个Play应用非常简单,仅需要play命令行工具。之后会生成Play应用的基本架构。
打开一个新的命令行并敲入:
~$ play new yabe
它会提醒输入应用的全名。输入yabe。
play new
命令创建了一个新的文件夹yabe/外加一系列文件和文件夹。其中包括下面各部分:
app/ 包括应用的核心,划分为models,controllers和views文件夹。它也可以包括其他Java的包。这是.java源代码文件所在之处。
conf/ 包括所有的应用配置文件,特别是主application.conf文件,路由定义文件和用于国际化的信息文件。
lib/ 包括所有可选的Java库,比如标准的.jar。
public/ 包括所有可以公开的资源,比如Javascript文件,样式表和图片。
test/ 包括所有的应用测试。测试可以是Java的JUnit测试或者Selenium测试。
因为Play只使用UTF-8编码,故所有的文本文件都需要使用UTF-8编码。确保你的文本编辑器已经做了相应的配置。
如果你开发过Java应用,你可能会奇怪.class文件到哪儿去了。答案是……没有.class文件了:Play并不使用任何class文件;相反它直接处理Java源代码。实际上我们使用Eclipse的编译器来即时编译Java源代码
这导致了开发过程中的两点重要的改进。
第一个,Play会自动监测Java源代码的改变并在运行时自动重载。
第二个,当一个Java异常发生时,Play能向你展示更好的错误报告 - 带对应的源代码的哦~
事实上Play在应用的tmp/文件夹下有字节码的缓存,但只用于加速重新启动项目的过程。
如果需要,你可以用play clean清空缓存。
3.3 运行程序
现在可以测试一下新建立的程序了。回到命令行窗口,在yabe/目录下输入play run命令。
Play框架将载入程序,启动Web服务器并监听9000端口。
打开浏览器键入http://localhost:9000,程序显示了一个缺省的欢迎页。
现在我们来看看这个页面是怎样显示的。
程序的主入口配置在conf/routes文件里。这个文件定义了程序所有可访问的URL。
打开routes文件,会看到第一个“route”:
GET / Application.index
它告诉Play,当服务器收到来自于/路径的GET请求时要调用Application.index的方法。
在这个程序中,Application.index是controllers.Application.index简写,因为controllers包是隐式附加的。
在创建一个标准Java程序时,通常会定义一个入口方法,比如:
public static void main(String[] args) { ...
}
Play程序则有多个入口方法,每个URL就有一个。这些方法称为action方法。定义Action方法的类称为controller。
看看什么是controller。打开yabe/app/controllers/Application.java源文件:
package controllers; import play.mvc.*;
public class Application extends Controller { public static void index() { render(); }
}
controller 类继承于play.mvc.Controller类,这个类提供了许多controller需要的方法,比如在index action中的render方法。
index action定义为public static void,因为controller类不需要实例化和返回值。
index action很简单,只是调用了render()方法来通知Play渲染模板。
使用模板是返回HTTP响应的一个最通用的方式。
模板是在/app/views 目录下的简单文本文件。
因为这里没有指定一个模板,index action会使用一个默认的模板:Application/index.html。
打开/yabe/app/views/Application/index.html:
#{extends 'main.html' /}#{set title:'Home' /} #{welcome /}
在这个模板中,只有Play tag,与JSP tag类似,#{welcome /} tag会在浏览器中生成欢迎信息。
#{extends /} tag 表示这个模板继承于main.html这个模板。模板继承可用来创建复杂的web也并重用公共部分。
打开/yabe/app/views/main.html模板:
<!DOCTYPE html>
<html> <head> <title>#{get 'title' /}</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <link rel="stylesheet" type="text/css" media="screen" href="@{'/public/stylesheets/main.css'}" /> <link rel="shortcut icon" type="image/png" href="@{'/public/images/favicon.png'}" /> </head> <body> #{doLayout /} </body>
</html>
#{doLayout /}tag表示index.html插入内容的地方。
试着编辑controller类来看看Play怎么自动加载它。
打开yabe/app/controllers/Application.java,删除render()后的分号,让它出错,就像这样:
public static void index() { render()
}
然后到浏览器刷新这个页面,Play会检测源文件变更并试着加载程序controller,
但是因为controller有错误,所以在浏览器中显示一个编译错误。
把刚才的错误修改正确,在编辑模板,打开yabe/app/views/Application/index.html覆盖欢迎消息。
#{extends 'main.html' /}
#{set title:'Home' /}
<h1>A blog will be here</h1>
在浏览器刷新这个页面。
3.4 配置数据库
在开始写代码之前还要多做一件事。作为博客引擎,我们需要一个数据库。为了便于与开发,Play内置了一个叫做H2的数据库。
当然如果需要,我们也可以切换到一个更加健壮的数据库。
你可以选择设置数据时存储在内存中,还是在文件系统中(这样即使你重新启动,你的数据也会保留)。
在一开始,我们将对应用模型做许多测试和改动。因此,最好选择存储在内存中,这样每次启动,都不会跟旧数据有任何牵连。
打开yabe/app/application.conf,解除这一行的注释:
db=em
正如你在注释中看到的一样,你可以冗余的配置任何JDBC数据库,甚至配置链接池。
现在回到浏览器并刷新欢迎页面。Play将自动启动数据库。
检查下面一行是否出现在应用日志中:
INFO ~ Connected to jdbc:h2:mem:play
3.5 补充
如果运行play run
命令出现下面提示:
解决办法:
找到play\framework\build.bat
修改
java -Xms512M -Xmx1024M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256M %DEBUG_PARAM% %JAVA_OPTS% -Dfile.encoding=UTF-8 -Dinput.encoding=Cp1252 -Dplay.version="%PLAY_VERSION%" -Dsbt.ivy.home="%~dp0..\repository" -Dplay.home="%~dp0." -Dsbt.boot.properties="%fp%sbt/sbt.boot.properties" %PLAY_OPTS% -jar "%~dp0sbt\sbt-launch.jar" %*
为
java -XX:+CMSClassUnloadingEnabled %DEBUG_PARAM% -Dfile.encoding=UTF8 -Dplay.version="%PLAY_VERSION%" -Dsbt.ivy.home="%~dp0..\repository" -Dplay.home="%~dp0." -Dsbt.boot.properties="file:///%p%sbt/sbt.boot.properties" -jar "%~dp0sbt\sbt-launch.jar" %*
3.6 将play项目导入myeclipse
前提:已安装play并配置了环境变量
第一步 :打开你的项目将下面几项删除(没有就跳过)
第二步:在cmd中来到项目的路径下(cd,不是来到项目里,而是项目名前一级目录),
然后输入play eclipsify +项目名
第三步:导入
4 play框架04
4.1 路由
Play框架中的路由器是负责将传入的HTTP请求映射为Action调用(即控制器中被声明为public static void的方法)的组件。
HTTP请求被MVC框架视为事件,其主要包括以下两块内容:
。请求路径(比如/clients/1542,/photos/list),其中可以包含查询字符串。
。HTTP方法(GET,POST,PUT,DELETE)。
Play路由器使用的配置文件为conf/routes,该文件列出了应用需要的所有路由规则。
每条路由由HTTP方法和与Java调用相关联的URI组成。以下是路由配置的例子:
GET /clients/{id} Clients.show
路由配置总是从HTTP方法开始,URI作为中间部分,最后的元素是Java调用。
在路由文件中可以使用#进行注释:
# Display a client
GET /clients/{id} Clients.show
4.1.1 HTTP方法
HTTP协议支持以下所列的方法,用于指定客户请求服务器的动作,其中GET和POST是最为常用的两种方法:
。GET
。POST
。PUT
。DELETE
。HEAD
Play同时也支持以WebSocket的方式来调用服务器端的Action方法
如果在路由文件中指定*作为HTTP方法,那么这个路由会匹配任何HTTP请求:
* /clients/{id} Clients.show
使用上述的路由配置,以下两个HTTP请求都会被框架接受:
GET /clients/1541
PUT /clients/1212
4.1.2 URI表达式
URI表达式定义了路由规则需要的请求路径,请求路径中允许存在动态内容,但必须被声明在{}中。
/clients/all
以上的路由配置只能精确匹配到:
/clients/all
但是如果以包含动态部分配置路由规则:
/clients/{id}
则可以分别匹配:
/clients/12121
和
/clients/toto
如果某条路由配置的URI中需要包含多个动态部分,可以采用下例方法进行配置:
/clients/{id}/accounts/{accountId}
默认情况下,动态部分的匹配策略采用的是正则表达式/[^/]+/。
也可以为动态部分定义自己的正则表达式,以下是使用正则表达式的例子。
路由规则只允许接受id为数字的值:
/clients/{<[0-9]+>id}
路由规则确保id是长度为4到10字符的小写单词:
/clients/{<[a-z]{4,10}>id}
正则表达式的使用非常灵活,还可以定义更多的路由规则,本节就不做赘述了。
注意:
动态部分指定后,控制器可以在HTTP参数map中获取该值。
默认情况下,Play将URI尾部的斜线(“/”)作为重要的组成部分,因为有无“/”将会出现不同的结果。比如:
GET /clients Clients.index
该路由规则会匹配/clients,而不是/clinets/(注意这里的区别),但可以通过在斜线后面增加问号来同时匹配两个URI:
GET /clients/? Clients.index
注意:
URI除了尾斜线不允许有其他可选的部分。
4.1.3 定义Java调用
路由定义的最后部分为需要调用的Java方法:控制器中必须定义指定的Action方法,否则会提示找不到控制器方法的错误信息;
必须声明为public static void方法;控制器需作为play.mvc.Controller的子类定义在controllers包中。
如果控制器没有在controllers包中定义,在配置路由规则时可以在其名称之前增加Java包(比如admin.Dashboard.index)的说明。
由于controllers包本身被Play默认包含,所以用户在配置路由时不需要显式地指定。
GET /admin admin.Dashboard.index
4.1.4 404作为Action
可以直接使用404作为路由配置中的Action部分。如果这样进行配置,对应的URL路径就会被Play应用所忽略。
比如:
# 忽略favicon请求
GET /favicon.ico 404
4.1.5 指定静态参数
在某些情况下,可能会需要基于不同的参数值定义特殊路由。以下是预先定义好的Action:
public static void page(String id) {Page page = Page.findById(id);render(page);
}
针对该Action,常规的路由配置为:
GET /pages/{id} Application.page
现在给参数id=home的页面指定一条特殊的URL,需要通过设置静态参数来实现:
GET /home Application.page(id:'home')
GET /pages/{id} Application.page
当参数id=home时,两条路由配置等价,但是由于前者具有较高的优先级,
所以被作为默认的URL来调用Application.page。
4.1.6 变量和脚本
与模板中的使用方法类似,在routes文件中可以使用${…}作为变量表达式,使用%{…}作为脚本表达式,
比如:
%{ context = play.configuration.getProperty('context', '') }%# 主页
GET ${context} Secure.login
GET ${context}/ Secure.login
在路由文件中定义变量和脚本的典型例子是CRUD模块的routes文件。
该文件中使用crud.types标签对model类型进行迭代,为每种类型生成控制器路由定义。
以后文章会详细介绍CRUD模块的使用。
#{crud.types}
GET /? ${type.controllerClass.name.substring(12).replace('$','')}.index
GET /${type.controllerName} ${type.controllerClass.name.substring(12).replace('$','')}.list
GET /${type.controllerName}/new ${type.controllerClass.name.substring(12).replace('$','')}.blank
GET /${type.controllerName}/{id} ${type.controllerClass.name.substring(12).replace('$','')}.show
GET /${type.controllerName}/{id}/{field} ${type.controllerClass.name.substring(12).replace('$','')}.attachment
GET /${type.controllerName}/{id}/edit ${type.controllerClass.name.substring(12).replace('$','')}.edit
POST /${type.controllerName} ${type.controllerClass.name.substring(12).replace('$','')}.create
POST /${type.controllerName}/{id} ${type.controllerClass.name.substring(12).replace('$','')}.save
DELETE /${type.controllerName}/{id} ${type.controllerClass.name.substring(12).replace('$','')}.delete
#{/crud.types}
Play会按照声明的顺序,优先选择最先声明的路由,比如:
GET /clients/all Clients.listAll
GET /clinets/{id} Clients.show
在上例的路由配置中,虽然请求/clients/all可以同时匹配这两条路由配置,
但按照声明的优先顺序会被第一条路由拦截,并调用相应的Clients.listAll方法。
如果id参数需要匹配5个数字,在不使用重复规则的前提下,只能连续使用五个\d元字符,而使用重复规则后,规则的如下:
GET /clinets/{<\d{5}>id} Clients.index
以下路由规则匹配2个大写字母以及3-4个数字:
GET /clinets/{<[A-Z]{2}[0-9]{3,4}>id} Clients.index
4.1.7 staticDir:mapping
Play的路由配置使用特殊的Action(staticDir)将存放静态资源的public目录开放。
该目录里包含的资源可以是图片,Javascript,Stylesheet等,这些资源将直接响应给客户端,并不需要服务器做进一步加工处理:
GET /public/ staticDir:public
当客户端请求/public/*路径时,Play会从应用的public文件夹中获取相应的静态资源。这里的优先级与标准路由配置一样适用。
4.1.8 staticFile:mapping
还可以直接将URL路径映射为静态文件:
GET /home staticFile:/public/html/index.html
当客户端通过GET方法请求/home时,服务器将不做任何处理直接把/public/html目录下面的index.html文件返回给客户端。
4.1.9 虚拟主机
Play的路由器具有主机匹配功能,当Action的变量需要从主机参数(指子域名,而不是子目录)中获取时,就显得特别有用。
比如SAAS应用可以使用如下方式配置路由规则:
GET {client}.mysoftware.com/ Application.index
根据以上配置,框架会自动获取client的值作为请求的参数:
public static void index(String client) {...
}
如果在模板中使用@@{…}标签,那么框架会根据指定的条件来选择对应的路由,这种方式在很多场合下都非常实用。比如,需要在产品中使用额外的服务器来提供静态资源,则可以采用如下方式进行路由配置:
#{if play.Play.mode.isDev()}GET /public/ staticDir:public
#{/}
#{else}GET assets.myapp.com/ staticDir:public
#{/}
对应模板中的代码如下:
<img src="@@{'/public/images/logo.png'}">
当应用在DEV模式下运行时,静态资源的URL为http://locahost:9000/public/images/logo.png;
如果运行在PROD模式下,URL为http://assets.myapp.com/images/logo.png。
4.2 逆向生成URL
Play路由器是按照Java调用生成URL的,所以可以将URI表达式都集中到同个配置文件中,使得重构应用变得更加便捷。
比如,为conf/routes文件添加如下路由配置:
GET /clients/{id} Clients.show
之后在Java代码中,就可以调用Client.show来生成URL:
map.put("id", 1541);
String url = Router.reverse("Clients.show", map).url; // GET /clients/1541
注意:
URL的生成已经集成到框架的大部分组件当中,一般我们不需要直接调用Router.reverse方法。
如果增加的参数不包含在URI表达式中,这些参数会被添加到查询字符串中:
map.put("id", 1541);
map.put("display", "full");
String url = Router.reverse("Clients.show", map).url; // GET /clients/1541?display=full
同样地,路由器会根据优先顺序匹配最适的URL。
4.3 设置content type
Play会根据request.format设定的值,选择指定的media类型来响应HTTP请求。
该值通过文件扩展名来决定使用何种视图模板进行渲染,并且通过Play框架中的mime-types.properties文件进行映射处理(映射关系详见play\framework\src\play\libs\mime-types.properties文件),为media类型设定Content-type响应。
Play请求的默认格式为html,因此index()控制器方法默认的渲染模板文件为index.html。
如果需要指定其他的格式,有以下四种方式:
(1)可以在程序代码调用render()方法之前进行格式设置。比如将media类型设置为text/css,就可以使用CSS文件进行渲染:
public static void index() {request.format = "css"; render();
}
(2)推荐一种更直接的做法,直接在routes文件中使用URL来指定格式。以下列路由配置为例:首先客户端通过index.xml请求服务器,服务器端将响应格式设置为xml,最后使用index.xml模版进行渲染。
GET /index.xml Application.index(format:'xml')
同样地,我们也可以使用CSS进行渲染:
GET /stylesheets/dynamic_css css.SiteCSS(format:'css')
(3)Play还可以直接从URL中获取请求格式,动态指定渲染的模板类型。参考如下路由配置:
GET /index.{format} Application.index
当请求为/index.xml时,服务器会将返回格式设置为xml并使用相应的XMl文件进行渲染;
请求为/index.txt时,则会使用文本进行渲染。
(4)使用Play中的HTTP内容协商进行格式设置,详见以后更新的内容。
4.4 HTTP内容协商
Play与其他REST架构的框架一样,直接使用HTTP方法,而不是试图隐藏HTTP或者在上面构建抽象层。
内容协商是HTTP的特性,它允许HTTP服务器根据客户端的请求类型,实现同个URL提供不同的media类型响应。
客户端可以在Accept header中设置media属性,指定可接收的响应类型。如果用户需要XML响应,则进行如下设置:
Accept:application/xml
客户端可以指定多种media类型,或使用cacth-all通配符(/)来指定任何media类型。
Accept:application/xml,image/png,*/*
常规的Web浏览器总是在Accept header中包含了通配符的值,这样浏览器便会接受任何media类型。
Play将HTML作为默认格式进行渲染,因此在客户端使用HTTP内容协商就显得特别有用:通过Ajax请求返回JSON格式,
或是使文档以PDF和EPUB形式显示等
4.3.1 在HTTP头中设置content type
如果Accept header中包含了text/html,application/xhtml或者通配符 /,Play会选择使用其默认的请求格式(即HTML)。
只有当通配符的值被显式指定时,Play才会选择其默认的请求格式。
Play内置了一些常规格式支持:html、txt、json、xml。
下例代码定义了控制器方法(Action)进行数据渲染:
public static void index() { final String name = "Peter Hilton"; final String organisation = "Lunatech Research"; final String url = "http://www.lunatech-research.com/"; render(name, organisation, url);
}
如果在浏览器中访问http://localhost:9000,Play默认会使用index.html模板进行渲染,因为浏览器发送了包含text/html的Accept header。
通过将请求的格式设置为xml,可以使用index.xml模板响应标识为Accept: text/xml的请求:
<?xml version="1.0"?>
<contact>
<name>${name}</name>
<organisation>${organisation}</organisation>
<url>${url}</url>
</contact>
下表针对index()控制器方法给出了Play内置的Accept header请求格式映射:
Accept header包含了Play能够映射成的所有格式(最后转化为相应的模板文件),如表3.1:
(表3.1 Play内置的Accept header请求格式映射)
Accept header | Format | Template file name | mapping |
---|---|---|---|
null | null | index.html | null格式请求提供默认模版扩展 |
image/png | null | index.html | media类型没有映射为指定格式 |
/, image/png | html | index.html | 默认将media类型映射为html格式 |
text/html | html | index.html | 内置映射 |
application/xhtml | html | index.html | 内置映射 |
text/xml | xml | index.xml | 内置映射 |
application/xml | xml | index.xml | 内置映射 |
text/plain | txt | index.txt | 内置映射 |
text/javascript | json | index.json | 内置映射 |
application/json, / | json | index.json | 内置映射, 忽略默认media类型 |
4.3.2 自定义格式
在Play中可以通过检查HTTP请求头,为应用选择相应的media类型来实现自定义格式。
比如使用@Before标签拦截该控制器下的所有Action,检查请求的media类型是否为text/x-vcard:
@Before
static void setFormat() { if (request.headers.get("accept").value().equals("text/x-vcard")) { request.format = "vcf"; }
}
如果检查后发现请求头中media类型为text/x-vcard时,将调用index.vcf模板渲染:
BEGIN:VCARD
VERSION:3.0
N:${name}
FN:${name}
ORG:${organisation}
URL:${url}
END:VCARD
4.5 关于REST
REST全称为Representational State Transfer,即表述性状态传输。
它是一种为分布式超媒体系统(比如万维网)而设计的软件架构方式。REST定义了一些关键的规则:
。应用的所有功能都被划分为资源。
。每个资源都使用URI来唯一访问。
。所有资源共享统一的接口用于客户端与资源之间进行状态传输。
如果应用使用的是HTTP协议,那么这些接口是通过一系列可用的HTTP方法来定义的。
HTTP协议往往通过以下方法来使用资源的状态:
。客户端-服务器模式。
。无状态模式。
。缓存模式。
。分层模式。
如果应用遵循了REST设计规则,那么该应用就可以被称为RESTful了。
Play框架可以很容易地构建RESTful应用:
。Play的路由器通过解析URI和HTTP方法,将请求路由至Action方法,基于正则表达形式的URI为开发提供了更好的灵活性。
。协议是无状态的,这意味着在两次成功的请求之间不会把任何状态保存在服务器中。
。Play将HTTP作为关键的特性,因此框架提供了对HTTP信息的完全访问。
5 play框架05
play框架05–控制层
5.1 概述
Play的控制层位于应用的controllers包中,其中的Java类即为控制器(Controller)。
如图4.1所示,Application.java和MyController.java都属于控制层。
(图4.1 控制器为controllers包中的Java类)
控制器需要继承play.mvc.Controller:
package controllers;import models.Client;
import play.mvc.Controller;public class Clients extends Controller {public static void show(Long id) {Client client = Client.findById(id);render(client);}public static void delete(Long id) {Client client = Client.findById(id);client.delete();}}
在控制器中,每个以public static声明,返回值为void的方法称为Action。
Action的方法声明如下:
public static void action_name(params…);
Play会自动将HTTP请求参数转化为与之相匹配的Action方法参数,这部分内容会在后面的获取HTTP参数小节进行详细讲解。
通常情况下,Action方法无需返回任何值,以调用结果方法来终止执行。
在上述例子中,render(…)方法就是用来渲染模板的结果方法。
HTTP请求中往往包含各种参数,这些参数的传递形式如下:
URI路径:在路径/clients/1541中,1541是URI的动态部分。
查询字符串:clients?id=1541。
请求体:如果请求是来自HTML的表单提交(GET或者POST),
那么请求体包含的是表单数据(采用x-www-urlform-encoded作为编码格式)。
针对以上几种情况,Play会自动提取这些HTTP参数并将他们保存在Map<String,String>类型的变量中,以参数名作为Map的key。
这些参数名分别来自于:
- URI中动态部分的名称(在routes文件中定义)。
- 查询字符串中“名称/值”对中的名称部分 。
- 采用x-www-urlform-encoded编码的表单数据的参数名。
5.2 获取HTTP参数
5.2.1 使用Map参数
HTTP请求中参数对象(params)在任何控制器中都是可访问的(该实现在play.mvc.Controller超类中定义),
它包含了当前所有HTTP请求的参数,并且可以通过get()方法得到,具体如下:
public static void show(){String id=params.get("id");String[] names=params.getAll("name");
}
这些参数也可以进行类型转换:
public static void show(){Long id=params.get("id",Long.class);
}
本节将推荐一种更好的解决方案。Play框架提供了自动将Action声明的参数与HTTP参数自动匹配的功能(只需要保持Action方法的参数名和HTTP参数名一致即可):
/clients?id=1541
Action方法可以在声明中以id作为参数,以此匹配HTTP中变量名为id的参数:
public static void show(String id){System.out.println(id);
}
当然,也可以使用其他Java参数类型,而不仅仅是String。
在下面的例子中框架会自动将参数转换为正确的数据类型:
public static void show(Long id){System.out.println(id);
}
如果参数含有多个值,那么可以定义数组参数,具体如下:
public static void show(Long[] id){for(Long anId:id){System.out.println(anId);}
}
参数甚至可以是List类型:
public static void show(List<Long> id){for(Long anId:id){System.out.println(anId);}
}
注意:
如果Action与HTTP之间的参数无法匹配,Play会将该参数设置为默认值(通常情况下对象类型为null,原始数据类型为0)。
如果参数可以匹配但不能正确进行数据转换,那么Play会先生成错误并添加到验证器的error对象集合中,然后将参数设置为默认值。
5.2.2 高级HTTP绑定
5.2.2.1 简单类型
Play可以实现所有Java原生的简单数据类型的自动转换,
主要包括:int,long,boolean,char,byte,float,double,Integer,Long,Boolean,Char,String,Float,Double。
日期类型
如果HTTP参数字符串符合以下几种数据格式,框架能够自动将其转换为日期类型:
yyyy-MM-dd'T'hh:mm:ss’Z' // ISO8601 + timezone
yyyy-MM-dd'T'hh:mm:ss" // ISO8601
yyyy-MM-dd
yyyyMMdd'T'hhmmss
yyyyMMddhhmmss
dd'/'MM'/'yyyy
dd-MM-yyyy
ddMMyyyy
MMddyy
MM-dd-yy
MM'/'dd'/'yy
而且还能通过@As注解,指定特定格式的日期,例如:
archives?from=21/12/1980
public static void articlesSince(@As("dd/MM/yyyy") Date from) {List<Article> articles = Article.findBy("date >= ?", from);render(articles);
}
也可以根据不同地区的语言习惯对日期的格式做进一步的优化,具体如下:
public static void articlesSince(@As(lang={"fr,de","*"}, value={"dd-MM-yyyy","MM-dd-yyyy"}) Date from) {List<Article> articles = Article.findBy("date >= ?", from);render(articles);
}
在这个例子中,对于法语和德语的日期格式是dd-MM-yyyy,其他语言的日期格式是MM-dd-yyyy。
语言值可以通过逗号隔开,且需要与参数的个数相匹配。
如果没有使用@As注解来指定,Play会采用框架默认的日期格式。为了使默认的日期格式能够正常工作,
按照以下方式编辑application.conf文件:
date.format=yyyy-MM-dd
在application.conf文件中设置默认的日期格式之后,就可以通过${date.format()}方法对模板中的日期进行格式化操作了。
5.2.2.2 日历类型
日历类型和日期类型非常相像,当然Play会根据本地化选择默认的日历类型。
读者也可以通过@Bind注解来使用自定义的日历类型。
5.2.2.3 文件类型
在Play中处理文件上传是件非常容易的事情,首先通过multipart/form-data编码的请求将文件发送到服务器,
然后使用java.io.File类型提取文件对象:
public static void create(String comment, File attachment) {String s3Key = S3.post(attachment);Document doc = new Document(comment, s3Key);doc.save();show(doc.id);
}
新创建文件的名称与原始文件一致,保存在应用的临时文件下(Application_name/tmp)。
在实际开发中,需要将其拷贝到安全的目录,否则在请求结束后会丢失。
5.2.2.4 数组和集合类型
所有Java支持的数据类型都可以通过数组或者集合的形式来获取。
数组形式:
public static void show(Long[] id){...
}
List形式:
public staic void show(List<Long> id){...
}
集合形式:
public static void show(Set<Long> id){...
}
Play还可以处理Map<String, String>映射形式:
public static void show(Map<String, String> client) {...
}
例如下面的查询字符串会转化为带有两个元素的map类型,
第一个元素key值为name,value为John;
第二个元素key值为phone,value为111-1111, 222-2222。:
?user.name=John&user.phone=111-1111&user.phone=222-2222
5.2.2.5 POJO对象绑定
Play使用同名约束规则(即HTTP参数名必须与模型类中的属性名一致),自动绑定模型类:
public static void create(Client client){client.save();show(client);
}
以下的查询字符串可以通过上例的Action创建client:
?client.name=Zenexity&client.email=contact@zenexity.fr
框架通过Action创建Client的实例,并将HTTP参数解析为该实例的属性。如果出现参数无法解析或者类型不匹配的情况,会自动忽略。
参数绑定是递归执行的,这意味着可以深入到关联对象:
?client.name=Zenexity
&client.address.street=64+rue+taitbout
&client.address.zip=75009
&client.address.country=France
Play的参数绑定提供数组的支持,可以将对象id作为映射规则,更新一组模型对象。
假设Client模型有一组声明为List的customers属性,那么更新该属性需要使用如下查询字符串:
?client.customers[0].id=123
&client.customers[1].id=456
&client.customers[2].id=789
5.2.3 JPA对象绑定
通过HTTP参数还可以实现JPA对象的自动绑定。Play会识别HTTP请求中提供的参数user.id,自动与数据库中User实例的id进行匹配。
一旦匹配成功,HTTP请求中的其他User属性参数可以直接更新到数据库相应的User记录中:
public static void save(User user){user.save();
}
和POJO映射类似,可以使用JPA绑定来更改对象,但需要注意的是必须为每个需要更改的对象提供id:
user.id = 1
&user.name=morten
&user.address.id=34
&user.address.street=MyStreet
5.2.4 自定义绑定
绑定机制支持自定义功能,可以按照读者的需求,自定义参数绑定的规则。
@play.data.binding.As
@play.data.binding.As注解可以依据配置提供绑定的支持。下例使用DateBinder指定日期的数据格式:
public static void update(@As("dd/MM/yyyy") Date updatedAt) {...
}
@As注解还具有国际化支持,可以为每个本地化提供专门的注解:
public static void update(@As(lang={"fr,de","en","*"},value={"dd/MM/yyyy","dd-MM-yyyy","MM-dd-yy"})Date updatedAt) {...
}
@As注解可以和所有支持它的绑定一起工作,包括用户自定义的绑定。以下是使用ListBinder的例子:
public static void update(@As(",") List<String> items) {...
}
上例中的绑定使用逗号将字符串分隔成List。
@play.data.binding.NoBinding
@play.data.binding.NoBinding注解允许对不需要绑定的属性进行标记,以此来解决潜在的安全问题。
比如:
//User为Model类
public class User extends Model {@NoBinding("profile") public boolean isAdmin;@As("dd, MM yyyy") Date birthDate;public String name;
}//editProfile为Action方法
public static void editProfile(@As("profile") User user) {...
}
在上述例子中,user对象的isAdmin属性始终不会被editProfile方法(Action)所修改,
即使有恶意用户伪造POST表单提交user.isAdmin=true信息,也不能修改user的isAdmin权限。
play.data.binding.TypeBinder
@As注解还提供完全自定义绑定的功能。自定义绑定必须是TypeBinder类的实现:
public class MyCustomStringBinder implements TypeBinder<String> {public Object bind(String name, Annotation[] anns, String value, Class clazz) {return "!!" + value + "!!";}
}
定义完成后,就可以在任何Action中使用它:
public static void anyAction(@As(binder=MyCustomStringBinder.class) String name) {...
}
@play.data.binding.Global
Play中还可以自定义全局Global绑定。以下是为java.awt.Point类定义绑定的例子:
@Global
public class PointBinder implements TypeBinder<Point> {public Object bind(String name, Annotation[] anns, String value, Class class) {String[] values = value.split(",");return new Point(Integer.parseInt(values[0]),Integer.parseInt(values[1]));}
}
因此外部模块很容易通过自定义绑定来提供可重用的类型转换组件。
5.3 结果返回
Action方法需要对客户端作出HTTP响应,最简单的方法就是发送结果对象。当对象发送后,常规的执行流程就会中断。
以下面这段代码为例,最后一句System.out.println的输出不会被执行:
public static void show(Long id) {Client client = Client.findById(id);render(client);System.out.println("This message will never be displayed !");
}
render(…)方法向模板发送client对象,之后的其他语句将不会执行,所以在控制台中,
并不会打印出“This message will never be displayed !”。
5.3.1 返回文本内容
renderText(…)方法直接将文本内容写到底层HTTP响应中:
public static void countUnreadMessages(){Integer unreadMessages=MessagesBos.countUnreadMessage();renderText(unreadMessages);
}
也可以通过Java标准的格式化语法对输出的文本进行处理:
public static void countUnreadMessages(){Integer unreadMessages=MessagesBox.countUnreadMessages();renderText("There are %s unread messages",unreadMessages);
}
5.3.2 返回JSON字符串
越来越多的应用使用JSON作为数据格式进行交互,Play对此进行了很好的封装,
只需要使用renderJSON(…)方法就可以轻松地返回JSON字符串。
在使用renderJSON(…)方法时,Play会自动将服务器返回的响应的content type值设置为application/json,
并且将renderJSON(…)方法中的参数以JSON格式返回。
在使用renderJSON(…)方法时,可以输入字符串格式的参数,自行指定JSON返回的内容。
public static void countUnreadMessages() {Integer unreadMessages = MessagesBox.countUnreadMessages();renderJSON("{\"messages\": " + unreadMessages +"}");
}
以上范例在使用renderJSON(…)方法时,传入了拼接成JSON格式的字符串参数。
Play框架会对其进行自动设置,改变content type的值为application/json。
当然,renderJSON(…)方法的功能并不只有这些。因为大部分的应用需求,都会要求服务端返回比较复杂的JSON格式,如果都采用字符串拼接的方式组成JSON内容,就太不人性化了。renderJSON(…)的输入参数还可以是复杂的对象,如果采用这种方式使用renderJSON(…)方法,Play在执行renderJSON(…)时,底层会先调用GsonBuilder将对象参数进行序列化,之后再将复杂的对象以JSON的格式返回给请求。这样开发者就可以完全透明地使用renderJSON(…)方法,不需要做其他的任何操作了,以下代码范例将会展示renderJSON(…)的这个功能。
public static void getUnreadMessages() {List<Message> unreadMessages = MessagesBox.unreadMessages();renderJSON(unreadMessages);
}
5.3.3 返回XML字符串
与使用renderJSON(…)方法返回JSON内容类似,如果用户希望以XML格式对内容进行渲染,
可以在Controller控制器中直接使用renderXml(…)方法。
使用renderXml(…)方法时,Play会自动将服务器返回的响应的content type值设置为application/xml。
在使用renderXml(…)方法时,可以输入字符串格式的参数,自行指定XML返回的内容。
public static void countUnreadMessages() {Integer unreadMessages = MessagesBox.countUnreadMessages();renderXml("<unreadmessages>"+unreadMessages+"</unreadmessages>");
}
如果希望将复杂的对象以XML格式进行渲染,可以在使用renderXml(…)方法时输入org.w3c.dom.Document格式的对象,
或者直接输入POJO对象。以POJO对象作为参数使用renderXml(…)方法时,Play会使用XStream将其进行序列化操作。
同样的,这些序列化操作都不需要由开发者去做,全部交给Play就行,开发者需要做的就是按照规范简单地调用renderXml(…)方法即可。
public static void getUnreadMessages() {Document unreadMessages = MessagesBox.unreadMessagesXML();renderXml(unreadMessages);
}
5.3.4 返回二进制内容
Play为开发者提供了renderBinary(…)方法,可以非常方便的返回二进制数据(如存储在服务器里的文件、图片等)给客户端。
以下代码范例将会展示如何使用renderBinary(…)方法进行二进制图片的渲染。
public static void userPhoto(long id) { final User user = User.findById(id); response.setContentTypeIfNotSet(user.photo.type());java.io.InputStream binaryData = user.photo.get();renderBinary(binaryData);
}
首先,开发者需要建立用于持久化的域模型User,该User模型具有play.db.jpa.Blob类型的属性photo。
play.db.jpa.Blob是经过Play封装的特有的属性类型,可以很方便的处理二进制数据。之后,在Controller控制器中使用时,
需要调用域模型的findById(…)方法加载持久化的数据,并将图片以二进制数据流InputStream的形式进行渲染。
5.3.5 下载附件功能
如果开发者希望将存储在服务器端的文件,采用下载的形式渲染给客户端用户,需要对HTTP的header进行设置。
通常的做法是通知Web浏览器将二进制响应数据以附件的形式,下载至用户的本地电脑上。
在Play中完成这个功能非常简单,只需要在使用renderBinary(…)方法时多传入一个文件名的参数即可。
这样做会触发renderBinary(…)的额外功能,提供文件名并设置响应头的Content-Disposition属性。
之后二进制文件(包括图片)将会以附件下载的形式,渲染给用户。
public static void userPhoto(long id) { final User user = User.findById(id); response.setContentTypeIfNotSet(user.photo.type());java.io.InputStream binaryData = user.photo.get();renderBinary(binaryData, user.photoFileName);
}
5.3.6 执行模板
如果需要响应的内容比较复杂,那么就应该使用模板来进行处理:
public class Clients extends Controller{public static void index(){render();}
}
模板的名称遵从Play的约束规则,默认的模板路径采用控制器和Action的名称相结合的方式来定义,
比如在上述例子中,模板对应的路径为:app/views/Clients/index.html。
5.2.7 为模板作用域添加数据
通常情况下模板文件都需要数据进行显示,可以使用renderArg()方法为模板注入数据:
public class Clients extends Controller {public static void show(Long id) {Client client = Client.findById(id);renderArgs.put("client", client);render(); }
}
在模板执行过程当中,client变量可以被使用:
<h1>Client ${client.name}</h1>
5.3.8 更简单方式
这里介绍一种更简单的方式向模板传递数据。
直接使用render(…)方法注入模板数据:
public static void show(Long id){Client client=Client.findById(id);render(client);
}
以该方式进行数据传递,模板中可访问的变量与Java本地变量的名称(也就是render()方法中的参数名)一致。
当然也可以同时传递多个参数:
public static void show(Long id){Client client=Client.findById(id);render(id,client);
}
注意:
render()方法只允许传递本地变量。
5.3.9 指定其他模板进行渲染
如果读者不希望使用默认的模板进行渲染,那么可以在renderTemplate(…)方法的第一个参数中指定其他自定义的模板路径,
例如:
public static void show(Long id) {Client client = Client.findById(id);renderTemplate("Clients/showClient.html", id, client);
}
5.3.10 重定向URL
redirect(…)方法产生HTTP重定向响应,可以将请求转发到其他URL:
public static void index(){redirect("http://www.oopsplay.org");
}
5.3.11 自定义Web编码
Play推荐开发者使用UTF-8作为应用开发的编码格式,如果不进行任何设置,Play框架默认使用的也就是UTF-8格式。
但是具体情况并不总是这么理想,有些特殊的需求可能要求某些响应(response)的格式为ISO-8859-1,
或者要求整个应用都必须保持ISO-8859-1编码。
为当前响应设置编码格式
如果需要改变某一个响应(response)的编码格式,可以直接在Controller控制器中进行修改,
具体做法如下所示:
response.encoding = "ISO-8859-1";
当开发表单提交功能时,如果开发者希望某一表单提交的内容采用非框架默认使用的编码(即Play框架采用默认的编码格式UTF-8,
而该form表单提交的内容希望采用ISO-8859-1编码格式),Play的做法有一些特殊。在书写form表单的HTML代码时,
需要对采用何种编码格式进行两次标识。首先需要在标签中添加accept-charset属性(如:accept-charset=“ISO-8859-1”),
accept-charset属性会通知浏览器当form表单提交的时候,采用何种编码格式;
其次,需要在form表单中添加hidden隐藏域,name属性规定为“charset”,value属性为具体需要的编码格式,
这样做的目的是当form提交的时候,可以通知服务端的Play采用何种编码方式,
具体范例如下:
<form action="@{application.index}" method="POST" accept-charset="ISO-8859-1"><input type="hidden" name="_charset_" value="ISO-8859-1">
</form>
定义全局编码格式
通常情况下,整个应用应该保持统一的编码格式。
如果开发者需要设置应用全局的编码格式,可以在application.conf配置文件中修改application.web_encoding属性,配置相应的编码。
5.4 Action链
Play中的Action链与Servlet API中的forward不尽相同。Play的每次HTTP请求只能调用一个Action,
如果需要调用其他的Action,那么必须将浏览器重定向到相应的URL。
在这种情况下,浏览器的URL始终与正在执行的Action保持对应关系,使得后退、前进、刷新操作更加清晰。
调用控制器中其他Action方法也可以实现重定向,框架会拦截该调用并生成正确的HTTP重定向。
具体实现如下:
public class Clients extends Controller {public static void show(Long id) {Client client = Client.findById(id);render(client);}public static void create(String name) {Client client = new Client(name);client.save();show(client.id);}
}
相应的路由规则定义如下:
GET /clients/{id} Clients.show
POST /clients Clients.create
按照定义,Action链的生命周期为:
浏览器向/clients发送POST请求;
路由器调用Clients控制器中的create方法;
create方法直接访问show方法;
Java调用被拦截,路由器逆向生成带有id参数的URL来调用Clients.show;
HTTP响应重定向为:/clients/3132;
浏览器地址栏中URL展现为:/clients/3132;
5.5 拦截器
控制器中可以定义拦截方法(也可称之为拦截器),为控制器及其子类的所有Action提供服务。
当所有的Action都需要进行通用的处理时,该功能就显得非常有用:比如验证用户的合法性,加载请求范围内的信息等。
读者在使用时需要注意的是,这些拦截器方法不能定义为public,但必须是static,并通过有效的拦截标记进行注解。
5.5.1 @Before
使用@Before注解的方法会在每个Action调用之前执行。如创建具有用户合法性检查的拦截器:
public class Admin extends Application {@Beforestatic void checkAuthentification() {if(session.get("user") == null) login();}public static void index() {List<User> users = User.findAll();render(users);}...}
如果不希望@Before注解拦截所有的Action方法,那么可以使用unless参数列出需要排除的方法:
public class Admin extends Application {@Before(unless="login")static void checkAuthentification() {if(session.get("user") == null) login();}public static void index() {List<User> users = User.findAll();render(users);}...}
或者直接使用only参数把需要拦截的方法列举出来:
public class Admin extends Application {@Before(only={"login","logout"})static void doSomething() { ...}...
}
unless和only参数对@After,@Before以及@Finally注解都适用。
5.5.2 @After
使用@After注解的方法会在每个Action调用之后执行:
public class Admin extends Application {@Afterstatic void log() {Logger.info("Action executed ...");}public static void index() {List<User> users = User.findAll();render(users);}...}
5.5.3 @Catch
如果有Action方法抛出了异常,那么使用@Catch注解的方法就会执行,且抛出的异常会以参数的形式传递到@Catch注解的方法中。
具体实现如下:
public class Admin extends Application {@Catch(IllegalStateException.class)public static void logIllegalState(Throwable throwable) {Logger.error("Illegal state %s…", throwable);}public static void index() {List<User> users = User.findAll();if (users.size() == 0) {throw new IllegalStateException("Invalid database - 0 users");}render(users);}
}
使用@Catch注解和普通的Java异常处理程序一样,捕获父类往往可以获得更多的异常类型。
如果拥有多个需要捕获的方法,可以通过指定优先级来确定他们的执行顺序。具体实现如下:
public class Admin extends Application {@Catch(value = Throwable.class, priority = 1)public static void logThrowable(Throwable throwable) {// Custom error logging…Logger.error("EXCEPTION %s", throwable);}@Catch(value = IllegalStateException.class, priority = 2)public static void logIllegalState(Throwable throwable) {Logger.error("Illegal state %s…", throwable);}public static void index() {List<User> users = User.findAll();if(users.size() == 0) {throw new IllegalStateException("Invalid database - 0 users");}render(users);}
}
5.5.4 @Finally
@Finally注解的方法总是在每个Action调用之后执行(无论Action是否成功执行):
public class Admin extends Application {@Finallystatic void log() {Logger.info("Response contains : " + response.out);}public static void index() {List<User> users = User.findAll();render(users);}...}
如果@Finally注解的方法中包含的参数是可抛出的异常,其方法中的内容还是可以继续执行的,具体如下:
public class Admin extends Application {@Finallystatic void log(Throwable e) {if( e == null ){Logger.info("action call was successful");} else{Logger.info("action call failed", e);}}public static void index() {List<User> users = User.findAll();render(users);}...
}
5.5.5 使用@with注解增加更多拦截器
如果某个控制器是其他一些类的父类,那么该控制器中定义的所有拦截器会影响到所有子类。
由于Java不允许多重继承,对单纯通过继承来使用拦截器造成了一定的局限性。
Play可以通过@With注解,调用其他控制器中已经定义好的拦截方法,从而突破这一局限。
比如创建Secure控制器,定义checkAuthenticated()拦截方法验证用户合法性:
public class Secure extends Controller {@Beforestatic void checkAuthenticated() {if(!session.containsKey("user")) {unAuthorized();}}
}
在其他的控制器中,可以通过@With(Secure.class)注解将其包含进来:
@With(Secure.class)
public class Admin extends Application {...
}
5.6 Session和Flash作用域
在Play开发中,如果需要在HTTP请求之间保存数据,可以将数据保存在Session或者Flash内。
保存在Session中的数据在整个用户会话中都是有效的,而保存在Flash的数据只对下一次请求有效。
特别需要注意的是,Session和Flash作用域中的数据都是采用Cookie机制添加到随后的HTTP响应中的(并没有存储在服务器上的),
所以数据大小非常有限(不能超过4K),而且只能存储字符串类型的数据。
由于Cookie是使用密钥签名过的,所以客户端不能轻易修改Cookie的数据(否则会失效)。
不要将Play的Session当作缓存来使用,如果需要在特定的会话中缓存一些数据,那么可以使用Play内置的缓存机制,
并将session.getId()作为缓存的key进行储存。
public static void index() {List messages = Cache.get(session.getId() + "-messages", List.class);if(messages == null) { // 处理缓存失效messages = Message.findByUser(session.get("user"));Cache.set(session.getId() + "-messages", messages, "30mn");}render(messages);
}
Session在用户关闭浏览器后就会失效,除非修改配置文件中的application.session.maxAge属性。
设置方法如下:
application.session.maxAge=7d # Remember for one week.
使用Play内置的Cache缓存时需要注意,Cache与传统Servlet的HTTP Session对象是不同的。
框架无法保证这些缓存对象会一直存在,所以在业务代码中必须处理缓存失效的问题,以便保持应用完全无状态化。
6 play框架06
play框架06–模板语法、模板继承
Play具有高效的模板体系,采用Groovy作为其表达式语言,允许动态生成HTML、XML、JSON或者任何基于文本格式的文档,
并且具有创建可重用标签(tag)的功能。
模板储存在Play应用的app/views目录下。
6.1 模板语法
与其他的语言一样,Play的模板也具有严格定义的语法。模板语法被划分为多种元素,用于完成不同类型的任务。
Play模板的本质是普通的文本文件,其中带有占位符的部分可以生成动态内容。
模板的动态部分采用Groovy语言编写,其语法与Java非常类似。
框架可以将需要渲染的结果追加至HTTP响应的数据部分,并发送至模板。
所有的动态部分将会在模板的执行期间被解析。
(表1 模板语法)
元素 | 描述 | 语法 |
---|---|---|
表达式 | 用于输出表达式的值。如 ${note.title} 的作用是将域对象note的属性title的值输出。 | ${…} |
标签 | 用于调用Play框架内置的或是开发人员自定义的标签。如#{get ‘title’ /}:获取变量 title 的值,该值仅在模板页面中有效。 | #{…} |
引用 | 用于生成调用控制器中Action方法的URL,在页面链接中使用的最为频繁。@{…} 和 @@{…} 的区别在于生成的URL分别是相对路径还是绝对路径。如: 1.首页:生成指向首页的链接。 2.@{’/public/stylesheets/main.css’}:引入CSS静态资源文件。 | @{…} 和@@{…} |
国际化 | 用于显示经过国际化处理后的消息内容。 | &{…} |
注释 | 用于在模板中添加注释。如:{ 这是注释 }。 | {…} |
脚本 | 用于添加复杂的 Groovy 脚本,可以声明变量和执行业务逻辑。 | %{…}% |
6.1.1 表达式(expression)
26Play框架教程2学习笔记相关推荐
- 26Play框架教程1学习笔记
Play框架教程1学习笔记 文章目录 1play教程 第一课 1.1 play是什么 1.2 REST风格是什么 1.3 play的优点和缺点 2 play教程 第二课 2.1 搭建开发环境 2.1. ...
- MongoDB 入门教程实战学习笔记-31-mongo 聚合查询管道 Aggregation Pipieline
aggregation 聚合操作处理数据记录并返回计算结果. 聚合操作将多个文档中的值组合在一起, 并且可以对分组数据执行各种操作以返回单个结果. mongodb 提供了三种执行聚合的方法: 聚合管道 ...
- python编程16章教程_Python学习笔记__16.2章 TCP编程
# 这是学习廖雪峰老师python教程的学习笔记 Socket是网络编程的一个抽象概念.通常我们用一个Socket表示"打开了一个网络链接",而打开一个Socket需要知道目标计算 ...
- 前端获取div里面的标签_web前端教程JavaScript学习笔记DOM
web前端教程JavaScript学习笔记 DOM一DOM(Document Object Model): 文档对象模型 其实就是操作 html 中的标签的一些能力 我们可以操作哪些内容 获取一个元素 ...
- 机器学习框架ML.NET学习笔记【1】基本概念与系列文章目录
一.序言 微软的机器学习框架于2018年5月出了0.1版本,2019年5月发布1.0版本.期间各版本之间差异(包括命名空间.方法等)还是比较大的,随着1.0版发布,应该是趋于稳定了.之前在园子里也看到 ...
- 机器学习框架ML.NET学习笔记【5】多元分类之手写数字识别(续)
一.概述 上一篇文章我们利用ML.NET的多元分类算法实现了一个手写数字识别的例子,这个例子存在一个问题,就是输入的数据是预处理过的,很不直观,这次我们要直接通过图片来进行学习和判断.思路很简单,就是 ...
- 《C++大学教程》学习笔记(九)
<C++大学教程>学习笔记(九) 1.Time类实例研究 1.1包含防护 在开始之前,先说明一个重要的C++软件工程概念:在头文件中使用"包含防护",从而避免头文件中的 ...
- 《崔庆才Python3网络爬虫开发实战教程》学习笔记(5):将爬虫爬取到的数据存储到TXT,Word,Excel,Json等文件中
本篇博文是自己在学习崔庆才的<Python3网络爬虫开发实战教程>的学习笔记系列,此套教程共5章,加起来共有34节课,内容非常详细丰富!如果你也要这套视频教程的话,关注我公众号[小众技术] ...
- 《Solid Edge高级应用教程》学习笔记
<Solid Edge高级应用教程>学习笔记 第一章.绪论 第一节.CAD概论 计算机辅助设计(Computer Aided Design,简称CAD)是一种用计算机硬件.软件系统辅助人们 ...
最新文章
- 蓝桥杯 十进制数转八进制数
- mysql正在加载_mysql 数据库基本操作
- SQL工作笔记-达梦7存储过程中游标的使用(for循环 IF等)
- 图解VMWare10创建虚拟机
- 北航、商汤提出的网络二值化新算法 IR-Net,到底好使不?
- 程序员快放弃 Android 9.0 吧,10.0 正在来的路上!
- 如何对西数硬盘固件进行逆向分析(下)
- C# 删除DataTable中的空行
- Java学习电子书大全
- PG数据库插件扩展搭建(一)
- 几行代码,搞定 SpringBoot 接口恶意刷新和暴力请求!
- excel两列数据对比找不同_对比excel,轻松学习python数据分析
- 海思Hi3518E MPP学习_02视频输入(VI模块)
- wifi计费认证系统php,TP-LINK认证计费系统 - TP-LINK官方网站
- iOS之支持https与ssl双向验证(包含:解决UIWebView加载不了https网页的图片,css,js等外部资源)
- Android Studio 微信登录
- html打印页眉页脚_HTML5基本元素:页眉,导航和页脚
- w ndows英文读音,Windows是什么意思
- 1、mybatis配置版
- 漫画:三分钟学习一道位运算的面试题,万一遇到了呢?
热门文章
Play框架教程1学习笔记 文章目录 1play教程 第一课 1.1 play是什么 1.2 REST风格是什么 1.3 play的优点和缺点 2 play教程 第二课 2.1 搭建开发环境 2.1. ...
aggregation 聚合操作处理数据记录并返回计算结果. 聚合操作将多个文档中的值组合在一起, 并且可以对分组数据执行各种操作以返回单个结果. mongodb 提供了三种执行聚合的方法: 聚合管道 ...
# 这是学习廖雪峰老师python教程的学习笔记 Socket是网络编程的一个抽象概念.通常我们用一个Socket表示"打开了一个网络链接",而打开一个Socket需要知道目标计算 ...
web前端教程JavaScript学习笔记 DOM一DOM(Document Object Model): 文档对象模型 其实就是操作 html 中的标签的一些能力 我们可以操作哪些内容 获取一个元素 ...
一.序言 微软的机器学习框架于2018年5月出了0.1版本,2019年5月发布1.0版本.期间各版本之间差异(包括命名空间.方法等)还是比较大的,随着1.0版发布,应该是趋于稳定了.之前在园子里也看到 ...
一.概述 上一篇文章我们利用ML.NET的多元分类算法实现了一个手写数字识别的例子,这个例子存在一个问题,就是输入的数据是预处理过的,很不直观,这次我们要直接通过图片来进行学习和判断.思路很简单,就是 ...
<C++大学教程>学习笔记(九) 1.Time类实例研究 1.1包含防护 在开始之前,先说明一个重要的C++软件工程概念:在头文件中使用"包含防护",从而避免头文件中的 ...
本篇博文是自己在学习崔庆才的<Python3网络爬虫开发实战教程>的学习笔记系列,此套教程共5章,加起来共有34节课,内容非常详细丰富!如果你也要这套视频教程的话,关注我公众号[小众技术] ...
<Solid Edge高级应用教程>学习笔记 第一章.绪论 第一节.CAD概论 计算机辅助设计(Computer Aided Design,简称CAD)是一种用计算机硬件.软件系统辅助人们 ...