JAX-RS 的核心功能是处理向 URI 发送的请求,所以它提供了一些匹配模式以便简化对 URI 的解析。楼主在本系列的上一篇文章中已经使用了最简单的路径参数,本文将介绍一些稍微高级点的咚咚。
模板参数
前面已经见过用 @Path("{id}")
和 @PathParam("id")
来匹配路径参数 id
。这种匹配方式可以被嵌入到 @Path
注解中的任何地方,从而匹配多个参数,例如下面的代码用来查找 ID 在某一范围内的电影:
@GET
@Path("{min}~{max}")
@Produces(MediaType.APPLICATION_JSON)
public List<Movie> findMovies(@PathParam("min") int min, @PathParam("max") int max) {
于是,GET /ms/rest/movie/5~16
就将返回 ID 为 5 到 16 的电影。此处的 min
和 max
已被自动转换为 int
类型。JAX-RS 支持多种类型的自动转换,详见 @PathParam
的文档。
根据 HTTP 规范,参数可能会编码。默认情况下,JAX-RS 会自动解码。如果希望得到未解码的参数,只需在参数上再加个 @Encoded
注解。该注解适用于大多数 JAX-RS 注入类型,但并不常用。
模板参数虽然灵活,也可能会带来歧义。例如想用 {firstName}-{lastName}
匹配一个人的姓名,但恰好某人的名(lastName
)含有“-”字符,像 O-live K 这种,匹配后就会变成姓 live-K,名 O。这种场景很难避免,一种简单的解决方法就是对参数值进行两次编码,然后在服务端代码解码一次,因为 JAX-RS 默认会进行一次解码,或者加上 @Encoded
注解,自己进行两次解码。
另外,在一个复杂系统中,多个 @Path
可能会造成路径混淆,例如 {a}-{b}
和 {a}-z
都能匹配路径 a-z
。虽然 JAX-RS 定义了一些规则来指定匹配的优先级,但这些规则本身就比较复杂,并且也不能完全消除混淆。楼主认为,设计一个 REST 系统的核心就是对 URI 的设计,应当小心处理 URI 的结构,合理分类,尽量保证匹配的唯一性,而不要过度使用晦涩的优先级规则。楼主将在下一篇文章介绍优先级规则。
正则表达式
模板参数可以用一个正则表达式进行验证,写法是在模板参数的标识符后面加一个冒号,然后跟上正则表达式字符串。例如在根据 ID 查询电影信息的代码中,模板参数 {id}
只能是整数,于是代码可以改进为:
@GET
@Path("{id : \\d+}")
@Produces(MediaType.APPLICATION_JSON)
public List<Movie> findMovies(@PathParam("min") int min, @PathParam("max") int max) {
冒号左右的空格将被忽略。用正则表达式验证数据很有局限性,可惜 JAX-RS 目前并不能直接集成 Bean 验证框架,因此复杂的验证只能靠自己写代码。
查询参数
查询参数很常见,就是在 URI 的末尾跟上一个问号和一系列由“&”分隔的键值对,例如查询 ID 为 5 到 16 的电影也可以设计为 /ms/rest/movie?min=5&max=16
。JAX-RS 提供了 QueryParam
来注入查询参数:
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<Movie> findMovies(@DefaultValue("0") @QueryParam("min") int min,
@DefaultValue("0") @QueryParam("max") int max) {
查询参数是可选的。如果 URI 没有设定某个查询参数,JAX-RS 就会根据情况为其生成 0、空字符串之类的默认值。如果要手动设定默认值,需要像上面的代码一样用 @DefaultValue
注解来指定。另外还可以加上 Encoded
注解来得到编码的原始参数。
有的查询参数是一对多的键值对,例如 /xyz?a=def&a=pqr
,这种情况只需将注入的参数类型改为 List
即可。
矩阵参数
矩阵参数应该属于 URI 规范中的非主流类型,但它实际上比查询参数更灵活,因为它可以嵌入到 URI 路径中的任何一段末尾(用分号隔开),用来标识该段的某些属性。例如 GET /ms/rest/movie;year=2011/title;initial=A
表示在 2011 年出品的电影中查找首字母为 A 的标题。year
是电影的属性,而 initial
是标题的属性,这比把它们都作为查询参数放在末尾更直观可读。匹配 URI 的时候,矩阵参数将被忽略,因此前面的 URI 匹配为 /ms/rest/movie/title
。矩阵参数可以用 @MatrixParam
来注入:
@GET
@Path("title")
@Produces(MediaType.APPLICATION_JSON)
public List<String> findTitles(@MatrixParam("year") int year,
@MatrixParam("initial") String initial) {
如果 URI 的多个段中含有相同名称的矩阵参数,例如 /abc;name=XXX/xyz;name=OOO
,这种直接注入就失效了,只能用下面要讲的编程式访问来取得。
编程式访问
如果简单的注入不能达到目的,就需要通过注入 PathSegment
或 UriInfo
对象来直接编程访问 URI 的信息。
一个 PathSegment
对象代表 URI 中的一个路径段,可以从它得到矩阵参数。它可以通过 @PathParam
来注入,这要求该路径段必须整个被定义为一个模板参数。例如下面的代码也可以用来处理 GET /ms/rest/movie/{id}
:
@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Movie findMovie(@PathParam("id") PathSegment ps) {
@PathParam
也可以注入多个段,如果想把 /a/b/c/d
匹配到 /a/{segments}/d
,直接注入一个字符串显然不行,因为 b/c
是两个路径段。唯一的选择是把注入的类型改为 List<PathSegment>
。楼主严重不推荐用一个模板参数匹配多个路径段,因为这很容易干扰其他匹配的设计,最后搞成一团乱麻。URI 路径段应当尽量设计得简单明晰,再辅以矩阵参数或查询参数就能应付大多数场景。不论对服务端还是客户端开发人员来说,简洁的 URI 既便于管理,又便于使用。网上有不少关于 URI 设计指南的文章,此处不再赘述。
如果想完全手动解析路径,则可以用 @Context
注入一个 UriInfo
对象,通过此对象可以得到 URI 的全部信息,详见 API 文档。例如:
@GET
@Path("{id}/{segments}")
@Produces(MediaType.PLAIN_TEXT)
public String getInfo(@PathParam("id") int id, @Context UriInfo uriInfo) {
UriInfo
主要用在某些特殊场合下起辅助作用,设计良好的 URI 用普通的注入就能完成大部分匹配。
工欲善其事必先利其器,为此 JAX-RS 提供了这些利器来解析 URI。至于如何用这些器来做出一个好系统,则还是依赖于 URI 本身的设计。
笼子大了什么鸟都有。同样的道理,不论多么细心地设计 URI 结构,在系统复杂到一定程度后,仍然难以避免路径冲突。为此,JAX-RS 使用一些规则来定义路径匹配的优先级。
如果某个请求路径可以对上多个 URI 匹配模式,那么 JAX-RS 就把可能匹配上的 URI 模式先拼接完整,按照下列规则依次进行比较,直到找出最适合的匹配模式:
- 首先,字面字符数量更多的 URI 模式优先。“字面字符”就是写死的路径段,不包含路径分隔符
/
和模板参数。例如/ms/rest/movie/{id : \\d+}
包含 11 个字面字符。 - 其次,模板参数个数最多的 URI 模式优先。例如
/ms/rest/movie/{id : \\d+}
带一个模板参数。 - 最后,含正则表达式的模板参数个数最多的 URI 模式优先。例如
/ms/rest/movie/{id : \\d+}
带一个含正则表达式的模板参数。
现在看一个例子。回顾一下,/ms/rest/movie/{id : \\d+}
已经用来根据 ID 获取电影信息。为了制造麻烦,现在引入 /ms/rest/movie/{title}
来根据电影标题获取电影信息。先请你猜一猜 /ms/rest/movie/300
代表啥?ID 为 300 的神秘电影,还是我们可爱的勇士?只能跟着规则一条一条地看:
- 首先,两个 URI 匹配模式的字面字符都是 11,下一步。
- 其次,两个 URI 匹配模式都带一个模板参数,下一步。
- 最后,只有
/ms/rest/movie/{id : \\d+}
带了一个含正则表达式的模板参数,胜利!所以返回 ID 为 300 的片片。
传说这三条规则能够覆盖 90% 以上的情景。不过我们马上就能造出一个打破规则的东西:/ms/rest/movie/{title : [ \\w]+}
。经过测试,/ms/rest/movie/300
会匹配上 /ms/rest/movie/{id : \\d+}
。如何解释?JAX-RS 规范文档 3.7.2 定义了完整的匹配规则,对于这两个简单的 URI 匹配模式,似乎一直进行到底都无法比较出优先级。莫非有另外的潜规则?或者是 JAX-RS 的实现(参考实现为 Jersey)自行规定?但无论如何,搞出这种怪物本身就是一个设计错误,所以也不必去深究原因。