JAX-RS3:路径参数

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 模式先拼接完整,按照下列规则依次进行比较,直到找出最适合的匹配模式:

  1. 首先,字面字符数量更多的 URI 模式优先。“字面字符”就是写死的路径段,不包含路径分隔符 / 和模板参数。例如 /ms/rest/movie/{id : \\d+} 包含 11 个字面字符。
  2. 其次,模板参数个数最多的 URI 模式优先。例如 /ms/rest/movie/{id : \\d+} 带一个模板参数。
  3. 最后,含正则表达式的模板参数个数最多的 URI 模式优先。例如 /ms/rest/movie/{id : \\d+} 带一个含正则表达式的模板参数。

现在看一个例子。回顾一下,/ms/rest/movie/{id : \\d+} 已经用来根据 ID 获取电影信息。为了制造麻烦,现在引入 /ms/rest/movie/{title} 来根据电影标题获取电影信息。先请你猜一猜 /ms/rest/movie/300 代表啥?ID 为 300 的神秘电影,还是我们可爱的勇士?只能跟着规则一条一条地看:

  1. 首先,两个 URI 匹配模式的字面字符都是 11,下一步。
  2. 其次,两个 URI 匹配模式都带一个模板参数,下一步。
  3. 最后,只有 /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)自行规定?但无论如何,搞出这种怪物本身就是一个设计错误,所以也不必去深究原因。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值