router 组件负责将 http 请求交给对应的 Action 处理(一个 static/public 的控制器方法)。
一个http请求在mvc框架里被当作一个mvc事件看待。这个事件包含了两个主要信息:
- 包含在查询字符串里的请求路径 (比如/clients/1542, /photos/list)。
- HTTP方法(GET, POST, PUT, DELETE)
2.1.关于REST
Representational state transfer (表述性状态转移REST)是一种应用于分布式超媒体系统(如www)的软件架构设计风格(注意:不是标准)。
REST规定了几个关键的设计原则:
- 应用程序功能都被当作资源看待
- 每个资源都有唯一的可寻址URI地址
- 所有的资源共享同一个接口来传送客户端和资源间的状态
- 其表现形式为每个超链接都不带有参数
如果你正在使用http,那么这些接口都是通过一系列可用的http方法进行定义。协议用于访问属于下列状态的资源:
- 客户端服务器Client-server
- 无状态的Stateless
- 可缓存的Cacheable
- 可分层的Layered
如果某个应用程序遵循主要的REST设计原则,那么这个应用程序就是RESTful的。play框架很容易创建RESTful风格的应用程序:
- Play router把所有的URI和HTTP方法都进行解析,并把请求路由一一对应到相应的java调用。基于正则表达式的URL范示使这个操作过程更灵活。
- 协议是stateless的,也就是说你不能在服务器上为两个连接的请求存储任何状态。
- Play把http当作关键特性进行考虑,因此play框架提供了让你完全访问http信息的能力。
2.2.routes 文件语法
Router使用conf/routes文件作为配置文件。此文件列出了所有应用程序所需要的路由。每个路由都由一个HTTP方法+ URI 范示来表示一个Java调用。
示例:
GET /clients/{id} Clients.show
每个route开始于一个http方法,接着是一个URL范示,最后一个元素是java调用定义。
你可以使用#为路由添加一个注释
# Display a client
GET /clients/{id} Clients.show
HTTP方法
HTTP方法可以是以下几个http协议支持的任何方法:
- GET
- POST
- PUT
- DELETE
- HEAD
这些方法也支持 WS(web service)作为 action 方法来指明一个 WebSocket 请求。
如果使用*作为http方法,则route将匹配所有的http方法请求,如:
* /clients/{id} Clients.show
路由将接受下面两个请求:
GET /clients/1541
PUT /clients/1212
URI 范示 Pattern
URI范示定义了路由的请求路径。请求路径的某些部分可以是动态的。任何动态的部分都必须使用大括号进行界定{…},如:
/clients/all
将精确匹配:
/clients/all
但…
/clients/{id}
可以匹配下面的请求:
/clients/12121
/clients/toto
一个URI范示可以有多个动态部分:
/clients/{id}/accounts/{accountId}
对于动态部分的默认匹配策略使用的是正则表达式 /[^/]+/,因此,我们可以为动态部分定义自己的正则表达式。
下面的正则表达式只能匹配数字值作为id:
/clients/{<[0-9]+>id}
下面的表达式只允许4至10个小写字母作为id:
/clients/{<[a-z]{4,10}>id}
在这里,可以使用任何可用的正则表达式。
注意!
凡是被命名了的动态部分,控制器随后可以从http参数map里得到动态部分的值。
默认情况下,play会把URL后的反斜线/当作不同的值,比如:
GET /clients Clients.index
此路由只会匹配/clients URL路径,不会匹配/clients/。如果你打算匹配这两个URL路径,那么你需要在路径后增加一个/?作为结束标志,比如:
GET /clients/? Clients.index
注意:
在这里,除了反斜线外,URI范示不能含有任何可选部分。
Java 调用定义
route定义的最后一部分就是java调用,通过使用action方法的完整名称来进行定义。action方法必须是控制类的public static void方法,而且这个控制器类必须定义于controllers包里,且是play.mvc.Controller的子类。
如果控制器类不在controllers包下,则需要在控制器类名称前添加java包名称。默认的controllers包可以直接使用,因此在此包下的控制器不需要单独指定包名。
GET /admin admin.Dashboard.index
把404当作action来用
你可以直接使用400作为路由action,用于标记那些应用程序必须忽略的URL路径,如忽略网站ico请求:
# Ignore favicon requests
GET /favicon.ico 404
指派静态参数
在某种情况下,你可能会重复使用一个存在的基于某些参数值的路由,如下:
public static void page(String id) {
Page page = Page.findById(id);
render(page);
}
相应的route:
GET /pages/{id} Application.page
现在,我打算为一个id为’hame’的页面定义一个URL别名,我可以定义一个具有静态参数的其他路由:
GET /home Application.page(id:'home')
GET /pages/{id} Application.page
当页面的id值为‘home’时,第一个路由与第二路由造价。但是,既然第一个具有更高优先级,那么这个路由将用作Application.page方法处理id为‘home’时的默认路由。
变量和脚本
在路由里,也可使用${…} 语法来在路由中使用变量,使用%{…}语法来在路由中使用脚本,和你在模板中使用变量和脚本的情况是一样的,比如:
%{ context = play.configuration.getProperty('context', '') }%
# Home page
GET ${context} Secure.login
GET ${context}/ Secure.login
另外一个例子就是在CRUD模块里的路由文件使用crud.types标签来循环调用所有的model types,以为每种类型生成控制器路由定义。
2.3.路由优先级
许多路由定义可能会匹配同一个请求,如果产生了冲突,那么只采用第一个路由(遵照下面的声明顺序)。
比如:
GET /clients/all Clients.listAll
GET /clients/{id} Clients.show
照此定义,则下面的URL:
/clients/all
将调用第一个路由指定的Clients.listAll方法(那怕第二个路由也同样匹配这个URL)。
2.4.服务器静态资源
staticDir: mapping
使用特定的action staticDir,可以用来定位每个你打算当作静态资源容器进行发布的文件夹。
比如:
GET /public/ staticDir:public
当提供的请求含有/public/* 路径时,Play将把应用程序/public目录下的文件发布出去。
其优先权和标准的路由优先权相同。
staticFile: mapping
也可直接映射一个URL路径到一个静态文件。
# Serve index.html static file for home requests
GET /home staticFile:/public/html/index.html
2.5.URL 编码
由于不可能对URL解码或重新编码 (比如你并能确定URL里的斜线是否就是真正的斜线或%2F), URL应该明确进行编码。play默认使用UTF-8进行编码,但你也可以使用ISO-8859-1, UTF-16BE, UTF-16LE, UTF-16当成默认的WebEncoding配置参数。详见http://download.oracle.com/javase/1.4.2/docs/api/java/nio/charset/Charset.html。
比如:
映射/stéphane时可以使用如下方法:
GET /st%C3%A9phane Application.stephane
2.6.反转路由:用于生成某些URL
Router可以用于在一个java调用里生成一个URL。因此,你就可以把所有的URI范示集中到一个配置文件里,这样,在重构应用程序时,你就更有把握了。
比如,下面的路由定义:
GET /clients/{id} Clients.show
在代码里,同样可以生成URL,以便调用Clients.show:
map.put("id", 1541);
// 结果为 GET /clients/1541
String url = Router.reverse("Clients.show", map).url;
URL生成器已经被集成到许多框架的组件里,你根本就不需要使用Router.reverse进行操作。
比如,如果你添加的参数并没有包含到URI范示里,那么这些参数将加到查询字符串里:
map.put("id", 1541);
map.put("display", "full");
// 结果为:GET /clients/1541?display=full
String url = Router.reverse("Clients.show", map).url;
优先权顺序再次用于查找最特别的路由,以生成URL。
2.7.设置内容风格(CSS)
Play选择依照request.format的值来为http response选择合适的media type。 这个值通过文件扩展名确定哪个视图模板将会被使用,同时设置response的Content-type的值为Play的mime-types.properties文件映射格式确定的媒体类型。
play请求的默认格式为html。因此,index()控制器方法的默认模板就是index.html文件。如果指定了一个不同的格式,就需要选择对应格式的模板。
你可以在调用render方法前以编程的方式设定格式。比如,为了使用媒体类型为text/css的CSS,你可这样处理:
request.format = “css”;
然而,更清晰的方法就是在rourtes文件里使用URL来指定格式。你可以通过为控制器方法指定格式来特定的路由添加格式,比如:下面的路由将处理来自/index.xml的请求,Application.index()方法将设置格式为xml,并使用index.xml模板进行渲染。
GET /index.xml Application.index(format:'xml')
类似的还有:
GET /stylesheets/dynamic_css css.SiteCSS(format:'css')
在下面的路由里,Play也可直接从URL里提取格式:
GET /index.{format} Application.index
在这个路由里,/index.xml请求将设置格式为xml,同时使用XML模板进行渲染; /index.txt请求将使用原始的text模板进行渲染。
Play也可使用http内容协商自动设置格式。
2.8.HTTP 内容协商 negotiation
play和其他RESTful架构一样,直接使用http提供的功能,而不是试着隐藏http或是在其之上放置一个抽象层。http的内容协商(Content negotiation)特性允许http服务器依照http客户端请求的媒体类型为同一个URL提供不同的媒体类型media types。客户端特定的可接受的内容类型是通过Accept header确定的,比如需要一个xml response就定义为:
Accept: application/xml
客户端可以指定多个媒体类型,并且可以指定(/)来表示可接受任意媒体类型:
Accept: application/xml, image/png, */*
传统的Web浏览器总是在其Accept header里包含了通配符:这样,他们就可以接收任意媒体类型,当然包括play提供的默认html类型。内容协商更多是用于定制的客户端,比如一个要求返回JSON的Ajax请求,或一个e-book阅读器需要的PDF或EPUB版本的文档。
从http headers开始设置内容类型
如果Accept header包含了text/html或application/xhtml以及作为/通配符的结果时, Play选择其默认格式html。如果通配符值为空时,默认格式将不被选择。
Play对html, txt, json和 xml媒体格式提供了内建支持。比如,下面定义了一个用于渲染某些数据的控制器方法:
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);
}
在一个浏览器里,如果请求的URL映射到这个方法时,那么play将渲染index.html模板,因为浏览器发送的Accept header里包含了text/html值。
通过设置请求格式为xml,Play响应的结果Accept header类型为:text/xml,同时使用index.xml模块进行渲染,比如:
<?xml version="1.0"?>
<contact>
<name>${name}</name>
<organisation>${organisation}</organisation>
<url>${url}</url>
</contact>
Accept header内建的格式对应的格式以及对应的模板文件见下表(以index()控制器方法为例):
Accept header | Format | Template file name | Mapping |
---|---|---|---|
null | null | index.html | Default template extension for null format |
image/png | null | index.html | 媒体类型没有映射到格式 |
*/*, image/png | html | index.html | 默认媒体类型映射到html格式 |
text/html | html | index.html | Built-in format |
application/xhtml | html | index.html | Built-in format |
text/xml | xml | index.xml | Built-in format |
application/xml | xml | index.xml | Built-in format |
text/plain | txt | index.txt | Built-in format |
text/javascript | json | index.json | Built-in format |
application/json, */* | json | index.json | Built-in format, default media type ignored |
定制格式
通过检查请求header和设置相应用的格式,可以为自己的自定义类型添加内容协商,因此只需在http请求选择相应的媒体类型时设置这个自定义格式即可。比如,为了在控制器里实现带有text/x-vcard的vCard功能,需要所有请求处理之前对定制格式进行检查:
@Before
static void setFormat() {
if (request.headers.get("accept").value().equals("text/x-vcard")) {
request.format = "vcf";
}
}
现在,一个Accept: text/x-vcard header请求将会渲染index.vcf模板,比如:
BEGIN:VCARD
VERSION:3.0
N:${name}
FN:${name}
ORG:${organisation}
URL:${url}
END:VCARD
继续讨论
当Router明确了哪个java调用将用于处理已经接收的http请求,play就会调用这个java调用。见Controllers 节,以了解控制器是如何工作的。