Play框架教程2学习笔记
文章目录
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 {
@Before
static 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 {
@After
static 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 {
@Finally
static 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 {
@Finally
static 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 {
@Before
static 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)😒{…}
构建动态部分最简单的方法就是声明表达式。表达式需要以 ${ 开头, 并以 } 结尾,作为占位符使用。
具体例子如下:
<h1>Client ${client.name}</h1>
以上是输出客户姓名的表达式例子。在该例子中,将客户类定义为Client。
经过控制器中Action的业务逻辑执行,首先需要向模板注入对象client,之后就可以在模板中使用${…}表达式语法输出client对象的name属性。
如果不能确定向模板注入的client对象是否为null,可以使用如下Groovy快捷语法:
<h1>Client ${client?.name}</h1>
此时,只有client不为null的情况下,才进行client.name的输出。
6.1.2 标签(tag): #{tagName /}
标签是能够附带参数调用的模板片段,如果标签只有一个参数,按照约定,参数的名称为arg,并且该参数名是可以省略的。
例如,可以使用#{script}标签加载JavaScript文件:
#{script 'jquery.js' /}
Play模板中的标签必须是闭合的,可以通过两种方式闭合标签。采用直接闭合的形式:
#{script 'jquery.js'/}
或者起始标签和结束标签成对地使用:
#{script 'jquery.js'}#{/script}
#{script}是Play模板的内置标签,由框架实现,可以直接使用。后面会进一步介绍如何自定义标签来满足开发中一些特定的需求。
#{list}标签可以对集合类进行迭代操作,使用时需要注意,必须带有两个参数(items以及as):
<h1>Client ${client.name}</h1>
<ul>
#{list items:client.accounts, as:'account' }
<li>${account}</li>
#{/list}
</ul>
上例中使用#{list}标签对client.accounts集合进行迭代,并将集合中的每一条数据作为account在页面中输出。
在应用中,模板引擎默认对所有的动态表达式进行转义,以此来避免XSS的安全问题。
如果模板中变量${title}的内容为
Title
,在页面输出时会自动进行转义:${title} --> <h1>Title</h1>
也可以通过调用扩展方法raw(),以非转义的形式在页面中输出,具体使用方法如下:
${title.raw()} --> <h1>Title</h1>
如果需要显示大量的非转义HTML内容,可以使用#{verbatim /}标签:
#{verbatim}
${title} --> <h1>Title</h1>
#{/verbatim}
6.1.3 引用(action)😡{…}或者@@{…}
在前面的章节已经有过一些介绍,Play通过路由器可以(逆向)生成URL,匹配指定的路由。
在模板中使用@{…}引用可以达到相同的目的:
<h1>Client ${client.name}</h1>
<p>
<a href="@{Clients.showAccounts(client.id)}">All accounts</a>
</p>
<hr />
<a href="@{Clients.index()}">Back</a>
该实例中,@{Clients.showAccounts(client.id)}调用了Clients控制器中的showAccounts Action方法,并传递了client.id参数。
@@{…}引用的使用语法与@{…}相同,只不过生成的是绝对URL(尤其适用于邮箱)。
6.1.4 国际化(messages):&{…}
如果应用需要进行国际化操作,那么可以在模板中使用&{…}显示国际化信息。
需要进行国际化的应用首先需要在conf/messages文件中进行国际化定义:
clientName=The client name is %s
之后在模板中就可以通过&{…}显示该国际化信息了:
<h1>&{'clientName',client.name}</h1>
6.1.5 注释(comment):
*{…}*
使用*{…}*标记的内容会被模板引擎忽略,起到注释作用:
*{**** Display the user name ****}*
<div class="name">
${user.name}
</div>
6.1.6 脚本(script): %{…}%
脚本是更加复杂的表达式集合,能够声明一些变量或者定义一些语句。
Play的模板中使用%{…}%插入脚本:
%{
fullName = client.name.toUpperCase()+' '+client.forname;
}%
<h1>Client ${fullName}</h1>
也可以直接使用out内置对象输出动态内容:
%{
fullName = client.name.toUpperCase()+' '+client.forname;
out.print('<h1>'+fullName+'</h1>');
}%
在模板中还可以使用脚本编写结构化语句,执行一些逻辑操作,比如迭代:
<h1>Client ${client.name}</h1>
<ul>
%{
for(account in client.accounts) {
}%
<li>${account}</li>
%{
}
}%
</ul>
使用模板时切记:模板不适合处理复杂的业务逻辑,所以在模板中请尽量使用标签,或者直接将处理交给控制器或模型对象。
6.2 模板继承
Play提供继承机制为多模板之间进行页面布局或设计提供服务。
#{extends /} 和 #{doLayout /}可以使页面的共享和重用变得更加方便简洁,
同时也能够帮助开发人员实现页面中动态内容和静态外观装饰的分离。
在模板与装饰之间可以使用#{get}和#{set}标签进行参数传递。
将simpledesign.html定义为父模板,也就是装饰模板。
可以将页面中重用的静态内容定义在装饰模板中,如导航条、页面顶端的图片banner、页面底端的footer说明等。
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>#{get 'title' /}</title>
<link rel="stylesheet" type="text/css" href="@{'/public/stylesheets/main.css'}" />
</head>
<body>
<h1>#{get 'title' /}</h1>
#{doLayout /}
<div class="footer">Built with the play! framework</div>
</body>
</html>
在simpledesign.html中将、
、 中的内容定义为公用元素,所有继承于该模板的页面都会包含这些内容。其中#{doLayout /}标签起到占位的作用,包含其子模板的页面内容。
其他所有继承于simpledesign.html模板的页面内容都将显示在#{doLayout /}所占的页面区块。
其他页面使用#{extends}标签可以非常简单地植入该装饰模板,
具体使用方法如下:
#{extends 'simpledesign.html' /}
#{set title:'A decorated page' /}
This content will be decorated.
该子模板使用#{extends ‘simpledesign.html’ /}标签来继承simpledesign.html,
使用#{set title:‘A decorated page’ /}标签传递页面的title变量,
最后在页面中输出This content will be decorated。
7 play框架07
play框架07–域模型
7.1 属性模拟
查看Play提供的示例应用,模型类里面会频繁地使用声明为public的变量。即使是经验尚浅的Java开发者,也懂得慎用public类型的变量。
在Java开发中(当然还有其他的面向对象语言),实践经验是这样告诉我们的:将所有的成员变量声明为私有,只提供获取与修改的方法。
这样做的目的在于增强程序的封装性,而“封装”在面向对象设计中恰恰是非常关键的概念。
Java没有真正的内置属性定义机制,而是使用Java Bean来进行约束:Java对象的属性通过一对getXxx/setXxx的方法来修改,
如果对象是只读的那么只需要提供getXxx方法。
在过去的开发中我们一直这样做,但是编码过程就显得有些乏味了。
每个属性必须声明为private,同时还有相应的getXxx/setXxx方法,
而且大多数情况下,getXxx和setXxx方法的实现都是类似的。
private String name;
public String getName() {
return name;
}
public void setName(String value) {
name = value;
}
Play框架的模型部分会自动生成getXxx/setXxx方法,保持代码的简洁。
也就是说,在Play中开发者可以直接把属性变量声明为public,运行时Play会自动生成相应的getXxx/setXxx方法(在这里我们将声明为public的字段都视为属性)。
public class Product {
public String name;
public Integer price;
}
上述代码被框架载入后就会转换成如下形式:
public class Product {
public String name;
public Integer price;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
}
因为变量被声明为public,可以使用如下方式对属性进行操作:
product.name = "My product";
product.price = 58;
程序在加载时会自动地转换成:
product.setName("My product");
product.setPrice(58);
注意:
因为这些getXxx/setXxx方法是在运行时动态生成的,所以不能直接调用。
如果在编码阶段使用他们,编译器会因为找不到该方法而报错误。
当然也可以自己定义相应的getXxx/setXxx方法,Play会优先选择手动编写的方法。
如果需要保护Product类的price属性就可以定义setPrice()方法:
public class Product {
public String name;
public Integer price;
public void setPrice(Integer price) {
if (price < 0) {
throw new IllegalArgumentException("Price can’t be negative!");
}
this.price = price;
}
}
如果为Product类的price属性赋负值就会抛出异常:
product.price = -10: // Oops! IllegalArgumentException
Play总会优先使用已经定义的getXxx/setXxx方法:
@Entity
public class Data extends Model {
@Required
public String value;
public Integer anotherValue;
public Integer getAnotherValue() {
if(anotherValue == null) {
return 0;
}
return anotherValue;
}
public void setAnotherValue(Integer value) {
if(value == null) {
this.anotherValue = null;
} else {
this.anotherValue = value * 2;
}
}
public String toString() {
return value + " - " + anotherValue;
}
}
补充:
@Entity注解的作用是通知Play自动开启JPA实体管理器,@Required是对该属性的约束。
该类继承于play.db.jpa.Model,Model提供了非常简单的对象处理方式,在后面章节会做详细介绍。
针对以上例子可以进行如下测试断言:
Data data = new Data();
data.anotherValue = null;
assert data.anotherValue == 0;
data.anotherValue = 4
assert data.anotherValue == 8;
以上的断言都会执行通过,而且因为这种改进的类遵从JavaBean规范,可以满足开发中的更复杂需求。
7.2 数据库配置
通常情况下,开发者需要将模型对象持久化。最常用的方法是把这些数据保存到数据库中。
在Play应用的开发过程中,开发者可以迅速配置嵌入式内存数据库或者直接将数据保存到文件系统中。
开启内存数据库H2,只需要在conf/application.conf文件中进行如下配置:
db=mem
补充:
H2是开放源代码的Java数据库,其具有标准的SQL语法和Java接口,可以自由使用和分发,且非常简洁和快速。
将数据保存在内存中相比从磁盘上访问能够极大地提高应用的性能,
但由于内存容量的限制,内存数据库适用于开发阶段,或者原型示例开发。
如果需要将数据保存在文件系统中,则使用如下配置:
db=fs
如果需要连接到MySQL服务器,则使用如下配置:
db=mysql:user:pwd@database_name
Play框架集成了H2数据库和MySQL数据库的驱动程序,存放在$PLAY_HOME/framework/lib/目录下。
如果需要使用PostgreSQL,Oracle或者其他数据库,需要在该目录(或者应用程序的lib/目录)下添加相应的数据库驱动。
Play可以连接任何JDBC兼容的数据库,只需要将相应的驱动类库添加到/lib目录中,并在conf/application.conf文件中定义JDBC配置:
db.url=jdbc:mysql://localhost/test
db.driver=com.mysql.jdbc.Driver
db.user=root
db.pass=123456
还可以在conf/application.conf文件中用配置选项指定JPA方言:
jpa.dialect=<dialect>
补充:
由于不同的数据库产品支持不同的ANSI SQL标准,所以Hibernate必须要使用“方言”才能与各种数据库成功的进行通信。
在Play中,大多数情况下会自动根据配置信息识别特定数据库方言,但是存在某些数据库,Play无法判断其使用的方言。
这时就需要开发者显式地在Play配置文件中指定。
除了使用Hibernate外,在编码时还可以直接从play.db.DB中获得java.sql.Connection,然后使用标准SQL语句来执行数据库操作。
Connection conn = DB.getConnection();
conn.createStatement().execute("select * from products");
7.3 数据持久化
Play的持久层框架采用的是Hibernate,使用Hibernate(通过JPA)自动地将Java对象持久化到数据库。
当在任意的实体类上增加@javax.persistence.Entity注解后,Play会自动为其开启JPA实体管理器。
@Entity
public class Product {
public String name;
public Integer price;
}
注意:
Play应用开发者一开始可能经常会犯的错误是使用Hibernate的@Entity注解来取代JPA。
这里请读者注意,Play是直接调用JPA的API来使用Hibernate。
也可以直接从play.db.jpa.JPA对象中得到实体管理器,通过实体管理器可以将Model持久化到数据库或者执行HQL语句,
例如:
EntityManager em = JPA.em();
em.persist(product);
em.createQuery("from Product where price > 50").getResultList();
Play为JPA的使用提供了非常好的支持,只需要继承Play提供的play.db.jpa.Model类:
@Entity
public class Product extends Model {
public String name;
public Integer price;
}
接着就可以执行Product实例中CRUD操作进行对象持久化:
Product.find("price > ?", 50).fetch();
Product product = Product.findById(2L);
product.save();
product.delete();
补充:ActiveRecord模式
ActiveRecord也属于ORM层,由Rails最早提出,遵循标准的ORM模型:表映射到记录,记录映射到对象,字段映射到对象属性。
配合遵循的命名和配置惯例,能够很大程度的快速实现模型的操作,而且简洁易懂。
Play也提倡使用ActiveRecord模式进行快速开发,其主要思想是:
- 每一个数据库表对应创建一个类,类的每一个对象实例对应于数据库中表的一行记录;通常表的每个字段在类中都有相应的Field;
- ActiveRecord同时负责把自己持久化,在ActiveRecord中封装了对数据库的访问,即CURD;
- ActiveRecord是一种领域模型(Domain Model),封装了部分业务逻辑。
7.4 无状态模型
Play被设计成为“无共享”的架构,目的就是为了保持应用的完全无状态化。
这样做的好处在于可以让一个应用同一时刻在多个服务器节点上运行。
Play为了保持模型无状态化,需要避免一些常见的陷阱,最重要的就是不要因为多请求而将对象保存到Java堆中。
Play应用中多请求之间保存数据有以下几种解决方案:
1.如果数据很小而且非常简单,那么可以将其存储在Session或者Flash作用域,
但是这些作用域最大只允许存放4K的内容,并且存储的数据只能为字符串类型。
2.将数据保存到持久化存储中(数据库或者文件系统)。
比如用户创建了需要跨越多个请求的对象,就可以按照以下步骤对其进行操作:
-
在第一次请求时初始化对象并将它保存到数据库中。
-
将创建的对象的id保存在Flash作用域中。
-
在以后不停的请求链执行过程中,使用id从数据库中获取对象,更新并重新保存它。
3.将数据保存在瞬时存储中(比如Cache):
-
在第一次请求时初始化对象并将它保存在缓存中。
-
将创建的对象的id保存在Flash作用域中。
-
在请求链的执行过程中,从Cache里获取对象(通过保存在Flash作用域中的对象id),更新后并将它再次保存回Cache。
-
当请求链结束后,将对象进行持久化操作(数据库或者文件系统)。
根据具体应用的需求,第三种解决方案使用缓存可以是一种非常好的选择,也是Java Servlet Session的良好的替代方案。
但缓存并不是可靠的数据存储方式,因此如果选择将对象保存到缓存中,就必须确保能够将它重新读取回来。
8 play框架08
play框架08–Job异步处理
8.1 Job实现
在Play中建立Job只需要继承play.jobs.Job类:
package jobs;
import play.jobs.*;
public class MyJob extends Job {
public void doJob() {
// 执行一些业务逻辑
}
}
如果希望创建具有返回值的Job,那么需要覆盖doJobWithResult()方法:
package jobs;
import play.jobs.*;
public class MyJob extends Job<String> {
public String doJobWithResult() {
// 执行一些业务逻辑
return result;
}
}
上例自定义的Job覆盖了doJobWithResult()方法,并且方法的返回类型为String,事实上Job可以返回任何类型的值。
8.2 Bootstrap Job
8.2.1 应用启动
Bootstrap Job,顾名思义就是在应用开始时运行的任务,
只需要添加@OnApplicationStart注解就可以把当前Job设置为Bootstrap Job:
import play.jobs.*;
@OnApplicationStart
public class Bootstrap extends Job {
public void doJob() {
if(Page.count() == 0) {
new Page("root").save();
Logger.info("A root page has been created.");
}
}
}
需要注意的是,Bootstrap Job不需要任何返回值。
如果有多个带有@OnApplicationStart注解的Bootstrap Job,那么默认情况下这些Job会按照定义的先后顺序执行。
当所有的Bootstrap Job执行完成之后,Web应用就处于等待阶段,等待处理那些即将到来的请求。
如果希望Web应用启动后,能够在执行Bootstrap Job的同时,又能很快地处理到来的请求,
可以为@OnApplicationStart注解添加async=true属性:@OnApplicationStart(async=true)。
这样应用程序开启后,Bootstrap Job就会作为后台程序异步执行了。
不仅如此,所有的异步Job(async=true)也会在Web应用开启之后同时运行。
注意:
Play具有两种不同的工作模式:开发模式(DEV)和产品模式(PROD),因此Job的启动时间也有略微差异。
在DEV模式下,直到第一个HTTP请求到达时才会开启应用,且不会预先编译Java文件。
如果在该模式下更改Java源文件可以立即生效,刷新浏览器即可查看修改后的结果。
此外,应用还会在需要的时候自动重启;而在PROD模式下,应用会在服务器启动时同步开启,一旦应用开启就会自动编译所有的Java文件,之后不再重载任何文件(包括模板文件和配置文件),所以如果有文件修改必须重启应用才能生效。
所以DEV模式下Job会延迟启动。
8.2.2 应用停止
Web应用停止或关闭的时候,常常也需要进行一些额外的操作,如进行数据的清理、日志的打印等。
如果开发者需要这类任务调度操作,可以使用Play提供的@OnApplicationStop注解。
import play.jobs.*;
@OnApplicationStop
public class Bootstrap extends Job {
public void doJob() {
Fixture.deleteAll();
}
}
用法非常简单,继承Job类之后,重写doJob()方法即可。
8.3 Scheduled Job
Scheduled Job是指可以被框架周期性执行的任务,可以使用@Every注解指定时间间隔控制Scheduled Job运行,
例如:
import play.jobs.*;
@Every("1h")
public class Bootstrap extends Job {
public void doJob() {
List<User> newUsers = User.find("newAccount = true").fetch();
for(User user : newUsers) {
Notifier.sayWelcome(user);
}
}
}
在实际开发中,@Every注解并不能够完全满足开发需求,比如有时候需要指定Scheduled Job在具体的某个时间点执行。
这时候可以使用@On注解指定时间点来执行Job,例如:
import play.jobs.*;
/** Fire at 12pm (noon) every day **/
@On("0 0 12 * * ?")
public class Bootstrap extends Job {
public void doJob() {
Logger.info("Maintenance job ...");
...
}
}
与Bootstrap Job一样,Scheduled Job也是不需要任何返回值的,即使返回了也会丢失。
补充:
@On标签中使用的是Quartz库的CRON表达式。CRON表达式是由7个子表达式组成的字符串,每个子表达式都描述了单独的日程细节。
这些子表达式用空格分隔,分别表示:
Seconds 秒
Minutes 分钟
Hours 小时
Day-of-Month 一个月中的某一天
Month 月
Day-of-Week 一周中的某一天
Year 年(可选)
具体CRON表达式的例子:"0 0 12 ? * WED",表示“每周三的中午12:00”。
8.4 Job的直接调用
Play的Job除了被框架自动调用外,也可以通过now()方法手动调用Job对象的实例,随时触发Job来执行指定任务。
使用now()方法调用Job后,任务会立即执行:
public static void encodeVideo(Long videoId) {
new VideoEncoder(videoId).now();
renderText("Encoding started");
}
now()方法的返回值是Promise对象,通过该值可以在结束后获得Job的执行结果。
9 play框架的请求处理流程
Play框架的请求处理流程
Play框架使用事件驱动模型,以提供在不可预知的使用环境下的灵活的处理能力。
在一个web应用中,事件主要指用户向服务器发起一次HTTP请求。对于Play框架,此类事件定义在routes文件中,play根据routes文件的内容以及用户的请求,确定应该调用哪些过程。Play框架使用了Netty服务器,该服务器使用管道(pipeline),提供了在高并发情况下的优秀的异步处理能力。
当服务器接收到一个用户请求的时候,将获取一个管道,将请求相关信息传入,之后交由Play框架处理。Play框架会根据该请求的内容,查找相应的路由规则,根据路由规则调用相应的事件处理流程,并(一般来说会)最终向用户返回结果,完成一次事件处理:
图1. 事件流向
用户请求处理流程相关类
作为一个web应用框架,Play框架的最基本的功能就是响应用户请求。
在本小节中,将概要讲述当一个用户请求(request)到来时,play将启动怎样的流程来对该请求进行处理,并最终返回相应给用户。
本小节的重点在于阐明流程,由于这一流程涉及到M-V-C三个方面,对于其具体实现细节,将在后文叙述。
相关类介绍
在介绍处理流程之前,需要介绍在这里流程中涉及到的一些类。本小节将重点介绍这些类的类结构,以及它们在这个流程中发挥的主要作用。
9.1 PlayHandler
PlayHandler继承了org.jboss.netty.channel.SimpleChannelUpstreamHandler,用于在管道中处理服务器监听到的用户请求。
类图如下:
图2 PlayHandler类图
其中比较重要的就是
messageReceived(final ChannelHandlerContext ctx,final MessageEvent messageEvent)
方法。该方法为对父类同名函数的重写(Override)。父类提供这个函数,由子类提供各自的具体实现。
当有消息到来的时候,服务器调用handler类的messageReceived函数,利用多态,将执行不同的实现。
在HttpServerPipelineFactory.getPipeline()中,当每次需要获得pipeline时,新建一个PlayHandler的实例,注册到pipeline中。
因此,每次的请求都会对应一个新的PlayHandler实例,接着PlayHandler.messageReceiveed()方法被调用以处理用户请求。PlayHandler.messageReceived()方法执行过程将在后文叙述。
9.2 Invoker与Invocation
Play框架采用了命令模式(Command pattern),用于在多线程多任务情况下调度任务,
命令模式的原型如下:
图3 命令模式原型
在命令模式中,Client调用Invoker,往其中添加命令。
Command代表了一个命令,开发者继承Command并实现一个具体的命令,提交给Invoker进行执行。
Invoker负责管理这些命令,在合适的时候执行它们,并提供处理结果。
命令模式把请求一个操作的对象与知道怎么执行一个操作的对象分割开
如此一来,开发者只需要关注命令的实现,而不需要关注何时、如何执行该命令。
在Play框架中,Invoker及其内部类Invocation实现了命令模式,Invocation相当于Command类,
由子类实现其execute方法(在这里表现为run()方法)。它们的类图如下:
图4 Invoker类图
图5 Invocation及DirectInvocation类图
在Invoker中,主要是invoke方法与invokeInThread方法。
前者使用(ScheduledThreadPoolExecutor) executor调度线程执行Invocation;
后者直接在当前线程中执行任务,当执行不成功时,等待一段时间之后重新执行。
Invoke方法还提供了另一个版本的重载函数,可以在等待一段时间之后再执行当前任务:invoke(final Invocation invocation, longmillis)。
关于java.util.concurrent.ScheduledThreadPoolExecutor:继承自java.util.concurrent.ThreadPoolExecutor,用于在给定的延迟后执行命令,或者定期执行命令,当需要多个辅助线程,或者要求ThreadPoolExecutor具有额外的灵活性或功能时,此类要优于Timer。
Invoker.invoke(Invocation)的代码如下,可以看到executor是如何调度Invocation的:
publicstatic Future<?> invoke(final Invocation invocation) {
Monitor monitor = MonitorFactory.getMonitor(
"Invokerqueue size", "elmts.");
monitor.add(executor.getQueue().size());
invocation.waitInQueue = MonitorFactory.start("Waiting forexecution");
returnexecutor.submit(invocation);
}
publicvoid run() {
if (waitInQueue != null) {
waitInQueue.stop();
}
try {
preInit();
if (init()) {
before();
execute();
after();
onSuccess();
}
} catch (Suspend e) {
suspend(e);
after();
} catch (Throwable e) {
onException(e);
} finally {
_finally();
}
}
在Invocation中实现了模板方法模式(Template method pattern),定义了run()方法的执行步骤,
而将各个步骤的实现方式交由其子类实现,以此方式来完成命令模式中的自定义命令。
模板方法模式原型如下:
图3-6 模板方法模式原型
在Play框架的模板方法模式具体实现中,Invocation实现了Runnable接口,实现了run()方法,
代码如下:
run()方法即为模板方法,在run()方法中,按顺序调用了init(),before(),execute(),after(),onSuccess()等方法,
这些方法需要由Invocation的子类实现,从而实现不同的功能。通过这样的设计,将执行过程分解为多个部分,
每个部分由子类实现,而各部分的执行顺序由父类规定。
在下文中,将提到PlayHandler.NettyInvocation,即为Invocation的一个子类。
9.3 Result类
Result继承自RuntimeException,封装了视图层渲染结果(可能是HTML文件,也可能是XML文件或者二进制文件等),
类继承关系如下:
图3-7 Result类继承结构图
FastRunTimeException是在Play框架中定义的一个可以快速实例化并抛异常类(Exception),
主要是修改了fillInStackTrace()方法,使其直接返回null,从而实现快速实例化:
public Throwable fillInStackTrace() {returnnull;}
在Result中,提供了apply()方法,该方法需要其子类重写,实现将相应的内容输出的功能。
在Result之下,play提供了多个Result的子类,用于执行对不同的相应的操作,其中比较常见的是RenderTemplate类,实现了对模板文件进行最后输出的功能。
将Result作为一个Exception并以try/catch的形式来捕获Result,而不是用返回值的方式,这是Play框架比较奇特的一点。
这不符合通常对“异常(Exception)”的看法——一般来说,只有程序出现不可预知的情况的时候,才会使用try/catch代码块来捕获Exception。然而,将渲染结果当做异常抛出并捕捉,将简化代码,由Java自身决定执行过程,提高了开发者的开发效率;
另外,在处理用户请求过程中,多处地方可能直接返回结果(如直接返回404页面),
框架在处理过程中将所有的处理结果归到同一个地方并统一作判断和处理,
如果将结果作为返回值,则需要更繁复的方法来对结果进行收集和处理。
9.4 Template类
Template类提供了对模板文件的封装,并实现了对模板文件的编译与执行。
类继承关系如下:
图3-8 Template类继承结构图
最终实际使用的是GroovyTemplate类,该类代表一个模板文件,
提供对页面进行渲染(将template文件转换为html)的方法(render(Map<String, Object>))。
在前面讨论的RenderTemplate中,在构造函数中传入一个(Template)template(即一个RenderTemplate“拥有”一个Template实例),
并执行
this.content = template.render(args);
调用Template.render(args)将渲染结果保存在content中。
在一个请求处理流程中,依次会经历PlayHandler->Invoker->Invocation->Result->Template
10 play框架的拦截器
Play框架的拦截器
在控制器里,定义拦截器方法。拦截器将被控制器类及后代的所有action调用。
这些方法必须是static的,但不能是public的,并使用有效的拦截注释。
@Before
用@Before注释的方法将在控制器的每个action前被调用执行
public class weixinIntercept extends Controller{
@Before(unless="login") //""中写的是方法的具体位置,例:wechat.account.wechatInformation
static void check(){
if(session.get("user") == null)
login();
}
}
/*
可以使用unless和only,还可以使用@After,@Before,@Finally注释
*/
控制器继承
如果一个控制器类是其他控制器类的子类,那么会按照继承顺序应用于相应的子类。
使用@With注释添加更多的拦截器
public class Security extends Controller{
@Before
protected static void checkAuthentic(){
if(!session.containsKey("user"){
unAuthen();
}
}
}
//另一个控制器
@With(Security.class)
public class Admin extends Controller{
......
}
11 play源码分析
==Play源代码分析1—Server启动过程==
Play是个Rails风格的Java Web框架,需要了解背景请看:
如何调试请看此处。以下进入正题_
Server启动过程主要涉及三个地方:
- play.Play类:代表Play本身业务模型。
- play.server.Server类:负责服务器启动。
- play.classloading包:负责.java文件读取、编译和加载。
11.1总体流程
Server.main为入口方法:
public static void main(String[] args) throws Exception {
…
Play.init(root, System.getProperty("play.id", ""));
if (System.getProperty("precompile") == null) {
new Server();
} else {
Logger.info("Done.");
}
}
做两件事:
- Play.init
- 然后创建Server对象。
Play.init
public static void init(File root, String id) {
…
readConfiguration();
Play.classes = new ApplicationClasses();
…
// Build basic java source path
VirtualFile appRoot = VirtualFile.open(applicationPath);
roots.add(appRoot);
javaPath = new ArrayList<VirtualFile>(2);
javaPath.add(appRoot.child("app"));
javaPath.add(appRoot.child("conf"));
// Build basic templates path
templatesPath = new ArrayList<VirtualFile>(2);
templatesPath.add(appRoot.child("app/views"));
// Main route file
routes = appRoot.child("conf/routes");
…
// Load modules
loadModules();
…
// Enable a first classloader
classloader = new ApplicationClassloader();
// Plugins
loadPlugins();
// Done !
if (mode == Mode.PROD ||preCompile() ) {
start();
}
…
}
主要做:
- 加载配置
- new ApplicationClasses();加载app、views和conf路径到VirtualFile中,VirtualFile是Play内部的统一文件访问接口,方便后续读取文件
- 加载route
- 加载Module,Play的应用扩展组件。
- 加载Plugin,Play框架自身的扩展组件。
- 工作在产品模式则启动Play.
关键步骤为new ApplicationClasses(),执行computeCodeHashe(),后者触发目录扫描,搜索.java文件。
相关过程简化代码如下:
public ApplicationClassloader() {
super(ApplicationClassloader.class.getClassLoader());
// Clean the existing classes
for (ApplicationClass applicationClass : Play.classes.all()) {
applicationClass.uncompile();
}
pathHash = computePathHash();
…
}
int computePathHash() {
StringBuffer buf = new StringBuffer();
for (VirtualFile virtualFile : Play.javaPath) {
scan(buf, virtualFile);
}
return buf.toString().hashCode();
}
void scan(StringBuffer buf, VirtualFile current) {
if (!current.isDirectory()) {
if (current.getName().endsWith(".java")) {
Matcher matcher = Pattern.compile("\\s+class\\s([a-zA-Z0-9_]+)\\s+").matcher(current.contentAsString());
buf.append(current.getName());
buf.append("(");
while (matcher.find()) {
buf.append(matcher.group(1));
buf.append(",");
}
buf.append(")");
}
} else if (!current.getName().startsWith(".")) {
for (VirtualFile virtualFile : current.list()) {
scan(buf, virtualFile);
}
}
}
11.2 Start流程
简化代码如下:
public static synchronized void start() {
try {
...
// Reload configuration
readConfiguration();
...
// Try to load all classes
Play.classloader.getAllClasses();
// Routes
Router.detectChanges(ctxPath);
// Cache
Cache.init();
// Plugins
for (PlayPlugin plugin : plugins) {
try {
plugin.onApplicationStart();
} catch(Exception e) {
if(Play.mode.isProd()) {
Logger.error(e, "Can't start in PROD mode with errors");
}
if(e instanceof RuntimeException) {
throw (RuntimeException)e;
}
throw new UnexpectedException(e);
}
}
...
// Plugins
for (PlayPlugin plugin : plugins) {
plugin.afterApplicationStart();
}
} catch (PlayException e) {
started = false;
throw e;
} catch (Exception e) {
started = false;
throw new UnexpectedException(e);
}
}
关键步骤为执行Play.classloader.getAllClasses()加载app目录中的类型。
简化代码如下:
public List<Class> getAllClasses() {
if (allClasses == null) {
allClasses = new ArrayList<Class>();
if (Play.usePrecompiled) {
...
} else {
List<ApplicationClass> all = new ArrayList<ApplicationClass>();
// Let's plugins play
for (PlayPlugin plugin : Play.plugins) {
plugin.compileAll(all);
}
for (VirtualFile virtualFile : Play.javaPath) {
all.addAll(getAllClasses(virtualFile));
}
List<String> classNames = new ArrayList<String>();
for (int i = 0; i < all.size(); i++) {
if (all.get(i) != null && !all.get(i).compiled) {
classNames.add(all.get(i).name);
}
}
Play.classes.compiler.compile(classNames.toArray(new String[classNames.size()]));
for (ApplicationClass applicationClass : Play.classes.all()) {
Class clazz = loadApplicationClass(applicationClass.name);
if (clazz != null) {
allClasses.add(clazz);
}
}
...
}
}
return allClasses;
}
主要步骤:
- plugin.compileAll,给所有plugin一次机会进行自定义编译。
- Play.classes.compiler.compile(classNames.toArray(new String[classNames.size()]));编译所有.java文件。编译后的.class存储在ApplicationClass中。内部使用了eclipse的JDT编译器。
- loadApplicationClass,取出ApplicationClass中的.class加入List中返回。
**到此完成.java的加载。**相关对象关系如下图:
11.3 启动HTTP服务
接着new Server()启动HTTP服务,监听请求
简化代码如下:
public Server() {
...
if (httpPort == -1 && httpsPort == -1) {
httpPort = 9000;
}
...
InetAddress address = null;
try {
if (p.getProperty("http.address") != null) {
address = InetAddress.getByName(p.getProperty("http.address"));
} else if (System.getProperties().containsKey("http.address")) {
address = InetAddress.getByName(System.getProperty("http.address"));
}
} catch (Exception e) {
Logger.error(e, "Could not understand http.address");
System.exit(-1);
}
ServerBootstrap bootstrap = new ServerBootstrap(new NioServerSocketChannelFactory(
Executors.newCachedThreadPool(), Executors.newCachedThreadPool())
);
try {
if (httpPort != -1) {
bootstrap.setPipelineFactory(new HttpServerPipelineFactory());
bootstrap.bind(new InetSocketAddress(address, httpPort));
bootstrap.setOption("child.tcpNoDelay", true);
if (Play.mode == Mode.DEV) {
if (address == null) {
Logger.info("Listening for HTTP on port %s (Waiting a first request to start) ...", httpPort);
} else {
Logger.info("Listening for HTTP at %2$s:%1$s (Waiting a first request to start) ...", httpPort, address);
}
} else {
if (address == null) {
Logger.info("Listening for HTTP on port %s ...", httpPort);
} else {
Logger.info("Listening for HTTP at %2$s:%1$s ...", httpPort, address);
}
}
}
} catch (ChannelException e) {
Logger.error("Could not bind on port " + httpPort, e);
System.exit(-1);
}
...
}
主要步骤:
- 设置端口,地址
- new ServerBootstrap,创建jboss netty服务器。Play1.1.1使用了netty作为底层通讯服务器。
- new HttpServerPipelineFactory(),设置netty所需的请求处理管道工厂。它负责当请求到达时提供处理者。
- bootstrap.bind(new InetSocketAddress(address, httpPort),绑定地址,端口。