开篇:
文字块说明:
1)蓝字:正文
2)棕字:小节标题
3)@引用:写给入门阶段的学弟学妹,下面示例:
4)需要醒目的文字:比较重要的步骤或者阅读代码中比较重要的部分
5)其他引用:一些解释性的描述,下面示例
自我介绍:
发文动因:
发文目的:
困惑:
目标一:学习官网Getting Started和Run Demo
@
1.1 基本概念和特点
不免落俗,先说些基本概念和特点
基本都是照搬官网的,所以也可以自己去看
http://www.playframework.org/documentation/1.0.2/home
概念:Play framework是个轻快的REST风格J2EE FULL-STACK框架
一些优点:
稍作解释:
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
1.3 创建默认项目
# 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(); } }
#{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友好的出错信息,一目了然。
package controllers; import play.mvc.*; public class Application extends Controller { public static void index() { render(); } public static void sayHello(String myName) { render(myName); } }
#{extends 'main.html' /} #{set title:'Home' /} <h1>Hello ${myName ?: 'guest'}!</h1> <a href="@{Application.index()}">Back to form</a>
现在的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有了初步的印象。
总结:经过了最简单的例子,我们看到了Play的部分特点:
目标二:Demo总结及阅读Play源码
以上特点都是我们实际跑例子直观看到的一些东西,下面我们深入一点,通过Debug demo的方式深入Play的源码看看Play是具体如何工作的:
因为需要Debug,因此需要IDE的支持,这里我们使用Eclipse。
Play提供了命令,可以直接将Play的项目转换成eclipse项目。
进入放置helloaworld的目录,使用命令
即可完成转换。
然后直接用Eclipse导入该项目即可
在Eclipse的package Explorer中,我们可以看到项目里多了一个eclipse目录
eclipse导入项目.png
此目录里的三个文件:
注※ 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过程我们后面单独开一节说,先关注核心部分。