开篇:
文字块说明:
1)蓝字:正文
2)棕字:小节标题
3)@引用:写给入门阶段的学弟学妹,下面示例:
4)需要醒目的文字:比较重要的步骤或者阅读代码中比较重要的部分
5)其他引用:一些解释性的描述,下面示例
自我介绍:
- 小弟是一名二年级学生,目前正在企业里做Intern,实习内容是JAVA后台方面的。
发文动因:
- 公司的一项很好的有奖新技术调研比赛活动,奖都是其次,更重要的是有了一个这样的平台,有了一定的压力,可以促使自己把平时的一些念头借此良机落实(肯定 很多人和我一样,看到些新技术总会有些想法,比如等有空了做做例子,把源码读一读,但是随着时间的推移,这份热情伴随着生活琐事也许就消逝了。。。。 o(╯□╰)o)
- 之前学习Linux,入门时看鸟哥系列( linux.vbird.org/ )非常的高效,也很欣赏他朴实踏实的文风,由浅入深,有条有理。因此自己也希望能写出个大部分人能看懂,且不觉得枯燥的东西来。
发文目的:
- 自我学习:实习了大半年,在现行框架上做过些coding的工作,但是对框架的理解还是皮毛,而且在企业里待着,身边每一位都是技术牛人,觉得压力巨大,所以需要不断学习来尽量弥补这巨大的差异,尽量使自己毕业时能达到在软件行业从业的水平。框架方面技术之前还学习过Spring和Spring Security,但是也是仅限于能参照指南做些相应的应用而已,因此想借为期50天的PLAY调研机会,顺便横向比较学习其他框架。通过写博客,给别人看是一方面,更重要的是写东西过程本身就是对学习的总结和加深记忆的很好方式。
- 指导学弟:我在刚入行的时候就很茫然,知道要多编程,多读经典源码,但是往往是事倍功半,说白了就是没上道,没开窍,当时特别希望有个入行不太深,但是多少知道点,又勤于分享的学长能有个对于阅读源码,做例子之类手把手的教程,而小弟正巧目前处于此阶段,过了懵懂,但是了解也不深,趁着记忆犹存,尽量把自己在之前学习遇到的不解问题和困难在学习Play!时再解释一遍,非常希望能帮助处于入门阶段的学弟学妹们O(∩_∩)O哈哈~
- 参加公司的技术调研比赛:不求名次,重在参与,希望我这种裹脚布的叙述方式能给公司众编程高手达人一种不同的技术表达风格。我初衷是尽量用最二(傻瓜,通 俗,简单)的语言和图案把PLAY说明白。
- 喜欢写博:虽然有论文的压力,但是说实话我讨厌死写论文,非要用些自己都觉得恶心的专业术语描述一些本应该很有活力的东西。我相信中国的大部分论文写出来,很多都是自己都不想多看几遍的,别再说别人了。而且写博可以闲暇时一个人待在宿舍听着音乐喝着茶用闲暇活动慢慢写,很悠闲,很享受。
困惑:
- 最困惑的莫过于官网的指导不但详细而且傻瓜,且用到的英语也很简单,这么写下去可能大部分照搬官网,显得很没意义。而自己也没有足够的经验和知识来评判Play!的优劣。所以唯一有点价值的可能就是稍微深入一下,争取把Play!的内部机理和部分源码搞明白。
- 由于小弟资历尚浅,学艺不精,文中难免出错,望各位大师如果耐心观看此文,发现错误请指正!!非常感谢!!
目标一:学习官网Getting Started和Run Demo
@
1.1 基本概念和特点
不免落俗,先说些基本概念和特点
基本都是照搬官网的,所以也可以自己去看
http://www.playframework.org/documentation/1.0.2/home
概念:Play framework是个轻快的REST风格J2EE FULL-STACK框架
Full-stack:现在很流行的SSH(Struts+Spring+Hibernate)的整合就是Full-Stack框架,及能提供J2EE开发全套功能的框架。比较出名的Full-Stack框架还有ruby on rails。
一些优点:
- Play is fast. So damn fast.
- No configuration. Not even one.
- Play is fun and joyful to develop web applications.
- Update your code and refresh your browser. Yes, you don't need to restart the server.
- It doesn't use maven. It uses Python.
- Play uses Groovy for the template system.
- Play Routing is amazingly easy.
- Play is heavily influenced by Rails, Django and Grails.
- Testing your code is easy.
- Play easily integrates with Eclipse, NetBeans, Intellij IDEA and TextMate.
稍作解释:
No configuration是指没有web.xml等配置文件(比如:如果自己组合SSH,得配置web.xml , spring和struts的配置文件,要配很多bean,注入以及过滤器)。
框架使用的应用服务器支持热加载,写好代码后,框架再编译后直接将类加载到服务器中,不需要重启服务器,这就大大提高了工作效率。
Routing非常简单,类似windows的hosts文件,定义了HTTP请求和应用程序的映射,再第一个例子中可以看到。
测试工作变得简单,是因为Play提供了良好的测试框架,在例子中可以看到。
1.2 准备工作
原文在此 : www.playframework.org/documentation/1.0.2/firstapp
- 安装JDK 1.5以上
- 下载Play! 1.02 http://download.playframework.org/releases/play-1.0.2.1.zip
- 解开压缩包,将play的根目录添加至环境变量的Path
1.3 创建默认项目
- 打开命令行,切换至你希望放置Hello world项目的目录,输入 “play new helloworld”,根据提示再输入一遍项目名。
- 这样项目就创建好了,然后输入“play run helloworld”,将我们新创建的项目先启动起来看一下。
- 打开浏览器,输入http://localhost:9000,我们先看一下默认的创建的项目是什么样子的。
- Play!真是很贴心,默认的项目启动后显示的内容是告诉你这个框架大概 是 怎么运行的。
- app目录:放置了java文件,可以看到app目录下还有三个目录controllers,models和views,目录结构非常清晰,分别存放MVC模式的三层的源码。
-
pig345 评注 // 2010/5/24文章里面有处用词稍微不当:“MVC模式的三层的源码”
我理解,MVC模式中 M、V、C 是3类平等的组件, 一定要和 3层模式中的 表现层、逻辑层、持久层,相区别,此处应酌情改下,否则容易暗示读者(尤其是你说的学弟学妹)把3层模式和MVC模式混为一谈。
- conf目录:放置了Play的配置文件,其中:
- application.conf是Play!框架的核心功能配置文件,比如配置DB,应用程序端口号等基础性应用程序配置都在此。
- routes 是配置http请求与该请求调用的应用程序方法之间的映射
# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~
# Home page
GET / Application.index
# Map static resources from the /app/public folder to the /public path
GET /public/ staticDir:public
# Catch all
* /{controller}/{action} {controller}.{action}
package controllers;
import play.mvc.*;
public class Application extends Controller {
public static void index() {
render();
}
}
,所对应的调用方法是controllers目录下的Application类的index()方法。
#{extends 'main.html' /}
#{set title:'Home' /}
#{welcome /}
<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'}">
#{get 'moreStyles' /}
<link rel="shortcut icon" type="image/png" href="@{'/public/images/favicon.png'}">
<script src="@{'/public/javascripts/jquery-1.4.2.min.js'}" type="text/javascript" charset="utf-8"></script>
#{get 'moreScripts' /}
</head>
<body>
#{doLayout /}
</body>
</html>
1.4 将默认项目改写成第一个app:Hello world
编辑helloworld/app/views/Application/index.html
#{extends 'main.html' /}
#{set title:'Home' /}
<form action="@{Application.sayHello()}" method="GET">
<input type="text" name="myName" />
<input type="submit" value="Say hello!" />
</form>
与默认项目的index区别,去掉了welcome tag,添加了一个form,注意action部分
@{}的用途是让Play自动生成能够Invoke Application.sayHello action的URL"@{Application.sayHello()}" method="GET"
我们将其转换成routes的表达方式是: GET /Application/sayHello Application.sayHello
保存后我们重新访问 localhost:9000/
出错的原因是“No route able to invoke action Application.sayHello was found.”
因为route找不到 Application.sayHello action。
这里我们还能看到Play友好的出错信息,一目了然。
回头看routes,最后两行定义了
# Catch all
* /{controller}/{action} {controller}.{action}
这是一个共通的匹配
因为@{Application.sayHello()}实质上时转化成url:/Application/sayHello
而*能匹配所有Http方法,因此route这里对应的action自然是Application.sayHello
package controllers;
import play.mvc.*;
public class Application extends Controller {
public static void index() {
render();
}
public static void sayHello(String myName) {
render(myName);
}
}
点击‘Say Hello’按钮后,URL跳转到如我们先前所说的/application/sayhello,并且带上参数myName=Darcy。
route通过此URL找到匹配的action后Invoke application.sayhello("Darcy")
将参数传入render("Darcy").
而render所作的工作,通过出错提示可以猜测到是需要模板sayhello.html来显示相应的PAGE.
其实这里Play帮我们做了很多工作,不需要我们一 一配置。
关于MVC:类Application继承自play.mvc.Controller,在MVC中处于控制器。
控制器的作用将数据模型(model)和视图(view)联系起来。
说白了最简单的用途是怎样将数据模型传递到相应页面上。
本例我们在app/models没有类,并不意味着我们没有model,其实myName就是个简单的model,当我们在文本框输入后并点击按钮后,myname被传递到render里,下一步自然是要将myName传递到视图层,此处我们没显式的指定传到哪个视图,其实Play默认做法是Application中的方法名与模板名是一致的。
#{extends 'main.html' /}
#{set title:'Home' /}
<h1>Hello ${myName ?: 'guest'}!</h1>
<a href="@{Application.index()}">Back to form</a>
其实Play的模板引擎是基于Groovy语言开发的,里面的一些表达式也是Play独有的,到了后面我们再细细交待,先说说这两个表达式的用法。
The simplest way to make a dynamic element is to declare an expression. The syntax used here is ${…}. The result of evaluating the expressions is inserted in place of the expression.
A tag is a template fragment that can be called with parameters. If the tag has only one parameter, by convention it is called “arg” and its name can be omitted.
现在的URL( localhost:9000/application/sayhello?myName=darcy )看起来不是很舒服,我们可以在routes里对他优化一下。
在默认route前加上我们自定义的route
在routes文件中,是靠上的r oute 优先。
GET /hello Application.sayHello
保存一下再跑一次看看,URL变了 : localhost:9000/hello?myName=ddd
自定义一些页面元素
编辑helloworld/app/views/main.html
将<body>稍作修改
<body>
The Hello world app.
<hr/>
#{doLayout /}
</body>
保存后查看
这里我们可以再次看到,main作为一个父类模板,将一些共通的页面元素写在main里,其他继承于他的模板都会有这些元素,大大提高了复用性,因此,我们在自己用Play做东西时,也应该灵活使用此特性。
添加基本校验(Adding validation)
编辑helloworld/app/controllers/Application.java
package controllers;
import play.mvc.*;
import play.data.validation.*;
public class Application extends Controller {
public static void index() {
render();
}
public static void sayHello(@Required String myName) {
if(validation.hasErrors()) {
flash.error("Oops, please enter your name!");
index();
}
render(myName);
}
}
除了修改sayHello方法,别忘了import play.data.validation.*
annotation @Required的用途是校验myName是否存在。若验证失败(validation.hasErrors() == ture) ,则抛出错误信息(flash.error("Oops, please enter your name!");。
这些错误信息是存放于Play的flash scope
flash scope能够在redirection时保存消息。
为了显示错误信息,我们还需添加显示错误信息的代码,由于这个错误信息是在Redirect至index后显示的,因此我们编辑index.html
编辑helloworld/app/views/Application/index.html
#{extends 'main.html' /}
#{set title:'Home' /}
#{if flash.error}
<p style="color:#c00">
${flash.error}
</p>
#{/if}
<form action="@{Application.sayHello()}" method="GET">
<input type="text" name="myName" />
<input type="submit" value="Say hello!" />
</form>
注意看#{if flash.error},这是Play中if Tag的用法,后面跟着Boolen参数。
flash虽然没作为参数传至render,但是也传递过来了,这就是flash scope的作用。
保存后我们访问主页,不填值,直接点按钮。
出错信息正常显示。
添加自动化测试
Play的测试框架继承JUnit和Selenium等,使写测试代码也非常方便。
因为本例没什么逻辑,所以我们只能进行一些页面的测试。
这里我们写一个Selenium测试脚本进行页面的测试。
首先,我们需要切换到测试模式。
到CMD中关闭应用模式服务器(ctrl+c)
使用命令:
切换到测试模式。
打开浏览器访问http://localhost:9000/@tests
这个页面是play测试框架的控制台,可以选择要进行测试的项目并执行测试,查看测试结果等。
我们将三项都选择,然后点Start进行测试,结果都是绿灯。原因是此时的测试代码都是必通过的= =#
比如:assertEquals(2, 1 + 1);
从此控制台还能看出,Play的测试框架可以进行单元测试,功能测试和Selenium web测试,真是太方便了!!
Selenium 是HTML的脚本,有点冗余,Play在测试框架中对Selenium 脚本也进行了优化,及可以使用Play的模板来写Selenium 脚本
(此处我想应该是Play对html脚本进行了二次封装,转成Play模板,当Selenium 读测试脚本是,Play的模板引擎会将模板再还原成Selenium 脚本,这里顺便标记一下,以后通过看Play的具体实现来验证)。
编辑现成的Selenium 脚本:helloworld/test/Application.test.html
#{selenium}
// Open the home page, and check that no error occurred
open('/')
assertNotTitle('Application error')
// Check that it is the form
assertTextPresent('The Hello world app.')
// Submit the form
clickAndWait('css=input[type=submit]')
// Check the error
assertTextPresent('Oops, please enter your name!')
// Type the name and submit
type('css=input[type=text]', 'bob')
clickAndWait('css=input[type=submit]')
// Check the result
assertTextPresent('Hello bob!')
assertTextPresent('The Hello world app.')
// Check the back link
clickAndWait('link=Back to form')
// Home page?
assertTextNotPresent('Hello bob!')
#{/selenium}
真是简洁不少= =#
保存后我们运行一下
至此我们参照官网的第一个例子教程完成了helloworld,这个最简单例子。
我们现在做的事情无非是照着官网step by step的把这个例子做完,并对Play有了初步的印象。
这里想插一件事情:
不知道你们有没有选董群峰老师的课,我有幸听了他的数据挖掘的课,更有幸的是听到他在第二节课的即兴演讲:
记得不是非常准确了,但是大致情形是,他给我们讲数据挖掘的某算法之前,让我们先想想应该用什么样的算法来解决那个问题,当时我们很囧,因为我们刚初学,怎么能想到那些大师发明的算法呢?
但是这个头脑风暴在一个非常葱的MM大胆发言后居然就这么在整个课堂讨论开了,最后短短半小时居然答案越来越接近了(当然这和直接发明算法的大师还是区别很大的,因为每次发言后董老师会给出些提示,比如‘不对’ ‘接近了’ ‘更近了’之类的,帮我们剔除掉很多错误的路径)。
随后董老师展开演说,让我们千万不要忽视初识某个领域时脑海里想法,因为我们的思想还没被以往沉积的知识体系固化,所以此时我们是创造力,创新力最旺盛的时期,任何想法都可能对此领域有重大的贡献,不要退缩,不要自我否定,不要急功近利去啃书本。要勇于挑战权威,勇于坚定自己的想法。
我可能有点演绎了,总之,我觉得这话说到我心坎了,尤其在我们中国,总觉得没有个砖家叫兽之类的头衔都不敢正视自己的想法,非要觉得自己的想法应该是错的,地位高的人说的就是对的。
这里我又想到Hibernate的创始人Gavin King(www.iteye.com/wiki/Celebrity/293-hibernate-founder-gavin-king)。他的经历可能正是这种需要自信,需要能坚持自己的想法,需要能挑战权威的最好榜样。
总结:经过了最简单的例子,我们看到了Play的部分特点:
- 写完代码后不用重启应用服务器重新部署包就可以直接运行改变后的代码。
- Play的模板引擎优化了UI开发的流程,模板间可以通过继承实现复用,另外,模板中的表达式和Tag也非常好用。
- 测试框架界面良好,功能齐全,对selenium的脚本也进行了优化(可以用Play的模板写)。
- 控制器的接口简单,通过render()方法传递参数即可将数据模型传至视图层。
目标二:Demo总结及阅读Play源码
以上特点都是我们实际跑例子直观看到的一些东西,下面我们深入一点,通过Debug demo的方式深入Play的源码看看Play是具体如何工作的:
这里先说一下我觉得初涉编程,阅读源码容易出现的错误:
1:从官网下了代码包后直接解压后逐个文件扫描,比较像读小说,按页翻,这样看肯定是稀里糊涂,看不出什么门道来。
2:要深入到一定层次(比如:能灵活使用框架,能做出复杂的引用,官方文档也读了不少)才敢去读源代码,总觉得斤两不够的时候读了也没啥用。
我个人认为,读源码也应该是渐进式的,从最简单开始,即从helloworld开始。
读代码要通过debug demo的方式去进行,善用step into和step over两种步进方式(什么,你分不清step into和step over的区别!!??赶紧去谷哥之)
要从简单开始是因为最简单的例子往往debug起来跑的逻辑都是框架最核心的逻辑,没有夹杂太多附加的模块功能,能最清晰的看到程序基本功能的运行流程。
此外,由于刚开始看,肯定会有很多不明白的(因为框架的运行是个复杂的过程,其中很多工作我们可能还没有接触到,也就不会有体会,当然看不懂)。因此我们应该尽量挑看的懂的地方看,要带着问题去看:
比如本例,我们可以想想:
数据模型是怎么传递的视图的
加参数校验是怎么实际工作的。
校验失败的错误信息时何时写入flash scope并伴随redirect传递到视图层。
因为需要Debug,因此需要IDE的支持,这里我们使用Eclipse。
Play提供了命令,可以直接将Play的项目转换成eclipse项目。
进入放置helloaworld的目录,使用命令
即可完成转换。
然后直接用Eclipse导入该项目即可
在Eclipse的package Explorer中,我们可以看到项目里多了一个eclipse目录
eclipse导入项目.png
此目录里的三个文件:
- Connect JPDA to myFirstApp.launch :用来Debug程序,右键菜单 Debug As..Connect JPDA to myFirstApp[需要先run myFirstApp.launch]
- myFirstApp.launch:用来run app,效果和play run一样,右键菜单 Run As..myFirstApp
- Test myFirstApp.launch :用来test,效果和play test一样
注※ myFirstApp是项目名,如果参照例子做,项目名应该是helloworld
开始Debug阅读源码
此处我们先run myFirstApp.launch,然后Debug As..Connect JPDA to myFirstApp.
在Application的index()方法里的render()方法设上断点。
打开浏览器访问主页,程序执行到断点,转到eclipse,按F5进入render()方法,此时,由于class文件没有和source文件关联,所以看不到源码。
根据提示点击"Change Attach source",选择Play1.02目录下的xxx\play-1.0.2\framework\src即可完成关联,稍等即可看到源码。
按F6继续往下走(F5跳进方法内,F6在方法内步进)
我们现在在Controller类里面:
先看看Controller类:
这个类没有抽象方法,但是有abstract关键字,是个抽象类。
这个类所有方法都是静态的protected方法,成员变量也都是静态的,除了_currentReverse这个ThreadLocal变量外,其他也都是protected的。
因此,在\app\controllers包下要使用Controller的静态方法,必须通过继承。
然而,从作为一个父类考虑,此类没有成员变量和方法,子类继承后没有得到父类的任何成员,在面向对象这一角度观察和实现一个空接口的功能相似,即标明这类是个controller。
因此我的理解是,这种设计,当我们在\app\controllers下面写一个Application并继承controller后,作用就是标明Application是个controller,并且Application作为一个代理执行controller类的静态方法。
同时,Application要实现controller的核心功能,必须调用controller的静态方法,因此,调用此方法的方法也必须是静态的。从而Application也没有被实例化的必要,所以可以证明,Play中的控制器是没有实例存在于容易中的。
回到render()方法,这个方法的参数是个类型为object的可变参数列表,
protected static void render(Object... args) {
...
}
可见render这个接口是个吞吐量巨大的视图层入口,为模板传递数据。
这个方法的主要功能取得到要render的模板名(templateName)。
此时的逻辑很简单,由于参数为空,走入else,然后拼接访问index的路径文件名Application/index.html(用Debug的方式阅读代码的另一个好处,即一些与我们关注点不大的逻辑直接走过后用watch查看结果,能加快我们队代码的理解)
String templateName = null;
if (args.length > 0 && args[0] instanceof String && LocalVariablesNamesTracer.getAllLocalVariableNames(args[0]).isEmpty()) {
templateName = args[0].toString();
} else {
templateName = Http.Request.current().action.replace(".", "/") + "." + (Http.Request.current().format == null ? "html" : Http.Request.current().format);
}
我们继续往下走。先不管[if(templateName.startsWith("@"))]处的逻辑,因为暂时没遇到过这个case,等以后再看不迟。
走到 renderTemplate(templateName, args); 我们F5进去看看
这个方法将模板名以及render的可变参数列表传递进来,再加上方法名,我们猜测这个方法的作用是根据模板名找到模板,然后把可变参数列表里的对象传到模板上。
看看实现:
protected static void renderTemplate(String templateName, Object... args) {
// Template datas
Scope.RenderArgs templateBinding = Scope.RenderArgs.current();
for (Object o : args) {
List<String> names = LocalVariablesNamesTracer.getAllLocalVariableNames(o);
for (String name : names) {
templateBinding.put(name, o);
}
}
templateBinding.put("session", Scope.Session.current());
templateBinding.put("request", Http.Request.current());
templateBinding.put("flash", Scope.Flash.current());
templateBinding.put("params", Scope.Params.current());
try {
templateBinding.put("errors", Validation.errors());
} catch (Exception ex) {
throw new UnexpectedException(ex);
}
try {
Template template = TemplateLoader.load(templateName);
throw new RenderTemplate(template, templateBinding.data);
} catch (TemplateNotFoundException ex) {
if(ex.isSourceAvailable()) {
throw ex;
}
StackTraceElement element = PlayException.getInterestingStrackTraceElement(ex);
if (element != null) {
throw new TemplateNotFoundException(templateName, Play.classes.getApplicationClass(element.getClassName()), element.getLineNumber());
} else {
throw ex;
}
}
}
先得到Scope.RenderArgs对象,还记得我们在之前加参数校验时,按照官网说法,如果校验失败,错误信息会放在Scope.flash里。
此处又用到了Scope的内部类RenderArgs的实例储存Render参数。
Scope在J2EE里通常是指生命周期的意思,因此Scope中保存的是各种状态:
根据代码可以清晰地看出,此处Scope.RenderArgs里存放了各种状态(session,request,flash,传递进来的参数和出错信息)
Template template = TemplateLoader.load(templateName);
这个方法把模板实例load进来。模板的load过程我们后面单独开一节说,先关注核心部分。