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的特性总结

  1. 自动编译和重载:当编辑Java文件并保存后,刷新浏览器就能立即查看结果。

    使用Play开发不需要手动编译、部署以及重新启动Web服务器等操作。

  2. 无状态模型:Play是真正的无共享框架,为REST而准备。它可以将同一个应用的多个实例分别部署在多台服务器上,因而扩展性非常强。

  3. 高效的模板引擎:基于表达式语言Groovy的清晰模板引擎,提供了模板的继承、导入以及标签自定义等功能。

  4. 快速解决错误:当错误发生时,Play会在浏览器中显示出错代码块并提示问题发生的确切位置。

  5. Full Stack:提供创建Web应用所需的全部功能,集成了Hibernate、OpenID、Memcached等第三方类库。

  6. 纯Java:Play采用Java编写代码,可以方便地使用任何Java类库,并且能够非常好地和Eclipse、Netbeans等IDE集成,只需通过命令生成匹配的项目文件即可。

  7. 基于非阻塞的IO模型:允许创建基于长轮询和WebSocket的主流Web应用。

  8. 有趣并且高效:省去了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学习笔记相关推荐

  1. 26Play框架教程1学习笔记

    Play框架教程1学习笔记 文章目录 1play教程 第一课 1.1 play是什么 1.2 REST风格是什么 1.3 play的优点和缺点 2 play教程 第二课 2.1 搭建开发环境 2.1. ...

  2. MongoDB 入门教程实战学习笔记-31-mongo 聚合查询管道 Aggregation Pipieline

    aggregation 聚合操作处理数据记录并返回计算结果. 聚合操作将多个文档中的值组合在一起, 并且可以对分组数据执行各种操作以返回单个结果. mongodb 提供了三种执行聚合的方法: 聚合管道 ...

  3. python编程16章教程_Python学习笔记__16.2章 TCP编程

    # 这是学习廖雪峰老师python教程的学习笔记 Socket是网络编程的一个抽象概念.通常我们用一个Socket表示"打开了一个网络链接",而打开一个Socket需要知道目标计算 ...

  4. 前端获取div里面的标签_web前端教程JavaScript学习笔记DOM

    web前端教程JavaScript学习笔记 DOM一DOM(Document Object Model): 文档对象模型 其实就是操作 html 中的标签的一些能力 我们可以操作哪些内容 获取一个元素 ...

  5. 机器学习框架ML.NET学习笔记【1】基本概念与系列文章目录

    一.序言 微软的机器学习框架于2018年5月出了0.1版本,2019年5月发布1.0版本.期间各版本之间差异(包括命名空间.方法等)还是比较大的,随着1.0版发布,应该是趋于稳定了.之前在园子里也看到 ...

  6. 机器学习框架ML.NET学习笔记【5】多元分类之手写数字识别(续)

    一.概述 上一篇文章我们利用ML.NET的多元分类算法实现了一个手写数字识别的例子,这个例子存在一个问题,就是输入的数据是预处理过的,很不直观,这次我们要直接通过图片来进行学习和判断.思路很简单,就是 ...

  7. 《C++大学教程》学习笔记(九)

    <C++大学教程>学习笔记(九) 1.Time类实例研究 1.1包含防护 在开始之前,先说明一个重要的C++软件工程概念:在头文件中使用"包含防护",从而避免头文件中的 ...

  8. 《崔庆才Python3网络爬虫开发实战教程》学习笔记(5):将爬虫爬取到的数据存储到TXT,Word,Excel,Json等文件中

    本篇博文是自己在学习崔庆才的<Python3网络爬虫开发实战教程>的学习笔记系列,此套教程共5章,加起来共有34节课,内容非常详细丰富!如果你也要这套视频教程的话,关注我公众号[小众技术] ...

  9. 《Solid Edge高级应用教程》学习笔记

    <Solid Edge高级应用教程>学习笔记 第一章.绪论 第一节.CAD概论 计算机辅助设计(Computer Aided Design,简称CAD)是一种用计算机硬件.软件系统辅助人们 ...

最新文章

  1. 蓝桥杯 十进制数转八进制数
  2. mysql正在加载_mysql 数据库基本操作
  3. SQL工作笔记-达梦7存储过程中游标的使用(for循环 IF等)
  4. 图解VMWare10创建虚拟机
  5. 北航、商汤提出的网络二值化新算法 IR-Net,到底好使不?
  6. 程序员快放弃 Android 9.0 吧,10.0 正在来的路上!
  7. 如何对西数硬盘固件进行逆向分析(下)
  8. C# 删除DataTable中的空行
  9. Java学习电子书大全
  10. PG数据库插件扩展搭建(一)
  11. 几行代码,搞定 SpringBoot 接口恶意刷新和暴力请求!
  12. excel两列数据对比找不同_对比excel,轻松学习python数据分析
  13. 海思Hi3518E MPP学习_02视频输入(VI模块)
  14. wifi计费认证系统php,TP-LINK认证计费系统 - TP-LINK官方网站
  15. iOS之支持https与ssl双向验证(包含:解决UIWebView加载不了https网页的图片,css,js等外部资源)
  16. Android Studio 微信登录
  17. html打印页眉页脚_HTML5基本元素:页眉,导航和页脚
  18. w ndows英文读音,Windows是什么意思
  19. 1、mybatis配置版
  20. 漫画:三分钟学习一道位运算的面试题,万一遇到了呢?

热门文章

  1. 小程序自定义日期组件,不显示今日之后的日期
  2. 【.NET】简单使用Description特性
  3. git+bitbucket使用备忘录
  4. NC6.5与UAP开发学习完整教程
  5. 易语言魔兽世界怀旧服自动钓鱼源码
  6. NIUSHOP wap端广告页面设置
  7. 实验: GVRP 配置,三层交换机通讯 ,VLAN 间路由,单臂路由与路由器子接口的配置,vlan-单臂路由技术
  8. 网上购车平台蛋蛋订车上私户兴起,与汽车之家联合开展青少年嘉年华
  9. 手机优酷下载视频怎么保存到手机
  10. 一篇好文章带你走出阴霾