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接口增加年龄信息。修改HomeControllergreet动作:

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

play2

检查服务器控制台的错误日志会发现,实际调用的是introduceMe动作而非squareMe。如前所述,使用通配符时我们需要自行处理且未验证输入数据。

introduceMe方法被调用时传入的是字符串"square/2"而非逗号分隔字符串。拆分后得到长度为1的数组,尝试访问索引1时抛出异常。

按理说调用应路由到squareMe方法,为什么实际路由到了introduceMe?原因在于Play的下一个特性:路由优先级。

7. 路由优先级

当路由存在冲突时(如squareMeintroduceMe的情况),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&param2=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获取。


原始标题:Routing in Play Applications in Java Baeldung