1. 概述
路由是大多数Web开发框架(包括Spring MVC)中的通用概念。路由本质上是URL模式与处理程序的映射关系。处理程序可以是物理文件(如Web应用中的可下载资源),也可以是处理请求的类(如MVC应用中的控制器)。
本文将深入探讨使用Play Framework开发Web应用时的路由机制。
2. 环境搭建
首先需要创建一个Java Play应用。关于在机器上配置Play Framework的详细步骤,可参考我们的入门文章。
完成环境搭建后,我们应该能通过浏览器访问到一个可运行的Play应用。
3. HTTP路由原理
当我们发送HTTP请求时,Play如何知道该调用哪个控制器?答案在于app/conf/routes
配置文件。
Play的路由器将HTTP请求转换为动作调用。在MVC架构中,HTTP请求被视为事件,路由器通过查询routes
文件来决定执行哪个控制器及其动作。
每个事件为路由器提供两个参数:包含查询字符串的请求路径和HTTP请求方法。
4. Play基础路由配置
要让路由器正常工作,conf/routes
文件必须定义HTTP方法和URI模式到控制器动作的映射:
GET / controllers.HomeController.index
GET / assets/*file controllers.Assets.versioned(path="/public", file: Asset)
所有路由文件还必须映射play-routing/public
文件夹中的静态资源,使其可通过/assets
接口访问客户端。
注意定义HTTP路由的语法:HTTP方法 + 空格 + URI模式 + 空格 + 控制器动作。
5. URI模式详解
本节将深入探讨URI模式的不同类型。
5.1 静态URI模式
上述前三种URI模式是静态的。这意味着URL到资源的映射不需要控制器动作进行额外处理。
只要调用控制器方法,就会返回一个静态资源,其内容在请求前就已确定。
5.2 动态URI模式
最后一种URI模式是动态的。这意味着处理这些URI请求的控制器动作需要从请求中获取信息来确定响应。在上述例子中,它需要一个文件名。
典型流程是:路由器接收事件,从URL提取路径,解码路径段,然后传递给控制器。
路径参数和查询参数随后作为参数注入控制器动作。我们将在后续章节通过示例说明。
6. Play高级路由配置
本节将详细讨论使用动态URI模式的高级路由选项。
6.1 简单路径参数
简单路径参数是请求URL中位于主机和端口之后的未命名参数,按出现顺序解析。
在play-routing/app/HomeController.java
中创建新动作:
public Result greet(String name) {
return ok("Hello " + name);
}
我们希望从请求URL提取路径参数并映射到name
变量。路由器将从路由配置获取这些值。
打开play-routing/conf/routes
,为此动作创建映射:
GET /greet/:name controllers.HomeController.greet(name: String)
注意我们使用冒号语法告知路由器name
是动态路径段,并将其作为参数传递给greet
动作调用。
现在在浏览器加载http://localhost:9000/greet/john
,将看到个性化问候:
Hello john
当动作参数为字符串类型时,调用时可省略参数类型声明,但其他类型不行。
让我们为/greet
接口增加年龄信息。修改HomeController
的greet
动作:
public Result greet(String name, int age) {
return ok("Hello " + name + ", you are " + age + " years old");
}
并更新路由:
GET /greet/:name/:age controllers.HomeController.greet(name: String, age: Integer)
注意Scala风格的变量声明语法age: Integer
。在Java中我们通常使用Integer age
。Play Framework基于Scala构建,因此包含大量Scala语法。
加载http://localhost:9000/greet/john/26
:
Hello john, you are 26 years old
6.2 路径参数中的通配符
路由配置文件中的最后一个映射是:
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
我们在路径的动态部分使用了通配符。这告诉Play:实际请求中替换*file
的任何值都应作为整体解析,不像其他路径参数那样分段解码。
本例中控制器是内置的Assets
,允许客户端从play-routing/public
文件夹下载文件。加载http://localhost:9000/assets/images/favicon.png
时,浏览器会显示Play的favicon图标(因为该文件存在于/public/images
文件夹)。
在HomeController.java
中创建自定义示例动作:
public Result introduceMe(String data) {
String[] clientData = data.split(",");
return ok("Your name is " + clientData[0] + ", you are " + clientData[1] + " years old");
}
注意此动作接收一个String参数并自行解码。本例中的解码逻辑是将逗号分隔的字符串拆分为数组。之前我们依赖路由器完成解码。
使用通配符时,我们需要自行处理。理想情况下应该在使用前验证传入字符串。
创建到此动作的路由:
GET /*data controllers.HomeController.introduceMe(data)
现在加载URL http://localhost:9000/john,26
,将输出:
Your name is john, you are 26 years old
6.3 路径参数中的正则表达式
与通配符类似,我们可以对动态部分使用正则表达式。添加一个接收数字并返回其平方的动作:
public Result squareMe(Long num) {
return ok(num + " Squared is " + (num * num));
}
添加其路由:
GET /square/$num<[0-9]+> controllers.HomeController.squareMe(num:Long)
将此路由放在introduceMe
路由之后以引入新概念。此路由配置只能处理正则表达式部分为正整数的情况。
如果按前述顺序放置路由并加载http://localhost:9000/square/2
,会遇到ArrayIndexOutOfBoundsException
:
检查服务器控制台的错误日志会发现,实际调用的是introduceMe
动作而非squareMe
。如前所述,使用通配符时我们需要自行处理且未验证输入数据。
introduceMe
方法被调用时传入的是字符串"square/2"而非逗号分隔字符串。拆分后得到长度为1的数组,尝试访问索引1时抛出异常。
按理说调用应路由到squareMe
方法,为什么实际路由到了introduceMe
?原因在于Play的下一个特性:路由优先级。
7. 路由优先级
当路由存在冲突时(如squareMe
和introduceMe
的情况),Play按声明顺序选择第一个匹配的路由。
为什么会产生冲突?因为通配符路径/*data
会匹配除基础路径/
外的所有请求URL。所以所有使用通配符的URI模式路由都应放在最后。
现在调整路由声明顺序,将introduceMe
路由移到squareMe
之后并重新加载:
2 Squared is 4
为测试路由中正则表达式的威力,尝试加载http://localhost:9000/square/-1
。路由器无法匹配squareMe
路由,转而匹配introduceMe
,再次抛出ArrayIndexOutOfBoundsException
。
这是因为-1
不符合提供的正则表达式,任何字母字符同样不匹配。
8. 参数处理
到目前为止,我们已涵盖路由文件中声明参数类型的语法。本节将探讨路由参数处理的更多选项。
8.1 固定值参数
有时我们想为参数使用固定值。这相当于告诉Play:使用提供的路径参数,或者当请求路径为/
时使用特定固定值。
另一种理解方式是:两个端点指向同一控制器动作——一个端点需要从URL获取参数,另一个在参数缺失时使用默认值。
在HomeController
中添加writer()
动作演示:
public Result writer() {
return ok("Routing in Play by Baeldung");
}
假设我们不希望API总是返回固定字符串:
Routing in Play by Baeldung
我们想通过请求传递文章作者名,仅在请求未提供author
参数时默认使用固定值"Baeldung"。
修改writer
动作添加参数:
public Result writer(String author) {
return ok("REST API with Play by " + author);
}
添加固定值参数的路由语法:
GET /writer controllers.HomeController.writer(author = "Baeldung")
GET /writer/:author controllers.HomeController.writer(author: String)
注意现在有两个独立路由都指向HomeController.writer
动作。
浏览器加载http://localhost:9000/writer
时得到:
Routing in Play by Baeldung
加载http://localhost:9000/writer/john
时得到:
Routing in Play by john
8.2 默认值参数
除固定值外,参数还可设置默认值。两者都在请求未提供所需值时为控制器动作参数提供回退值。
区别在于:固定值用于路径参数的回退,默认值用于查询参数的回退。
路径参数格式如http://localhost:9000/param1/param2
,查询参数格式如http://localhost:9000/?param1=value1¶m2=value2
。
第二个区别是声明语法不同。固定值参数使用赋值运算符:
author = "Baeldung"
默认值使用条件赋值:
author ?= "Baeldung"
我们使用?=
运算符,当author
无值时才将"Baeldung"赋给它。
完整演示:修改HomeController.writer
动作。除作为路径参数的作者名外,还要传递作者ID作为查询参数,请求未提供时默认为1。
修改writer
动作:
public Result writer(String author, int id) {
return ok("Routing in Play by: " + author + " ID: " + id);
}
更新writer
路由:
GET /writer controllers.HomeController.writer(author="Baeldung", id: Int ?= 1)
GET /writer/:author controllers.HomeController.writer(author: String, id: Int ?= 1)
加载http://localhost:9000/writer
看到:
Routing in Play by: Baeldung ID: 1
访问http://localhost:9000/writer?id=10
得到:
Routing in Play by: Baeldung ID: 10
http://localhost:9000/writer/john
呢?
Routing in Play by: john ID: 1
最后,http://localhost:9000/writer/john?id=5
返回:
Routing in Play by: john ID: 5
9. 总结
本文探讨了Play应用中的路由机制。我们还有一篇关于使用Play Framework构建RESTful API的文章,其中应用了本教程的路由概念。
本教程的源代码可在GitHub获取。