JFinal RESTful


在阅读之前, 先参考

  1. JFinal 极速开发框架

  2. RESTful API 设计指南

  3. JFinal实现严格的Restful


当看完这些之后, 大概应该就能理解, RESTful 的规范, 以及 JFinal 的路由实现方式.

首先, 先看下 JFinal 目前的 RESTful 风格路由实现.

翻开 com.jfinal.core.ActionMapping#getAction 方法


/**
 * Support four types of url
 * 1: http://abc.com/controllerKey                 ---> 00
 * 2: http://abc.com/controllerKey/para            ---> 01
 * 3: http://abc.com/controllerKey/method          ---> 10
 * 4: http://abc.com/controllerKey/method/para     ---> 11
 * The controllerKey can also contains "/"
 * Example: http://abc.com/uvw/xyz/method/para
 */
Action getAction(String url, String[] urlPara) {
  Action action = mapping.get(url);
  if (action != null) {
    return action;
  }

  // --------
  int i = url.lastIndexOf(SLASH);
  if (i != -1) {
    action = mapping.get(url.substring(0, i));
    urlPara[0] = url.substring(i + 1);
  }

  return action;
}


这里有说明, 目前 JFinal 支持的路由格式, 在此之外, JFinal 还提供了一个 Restful 的拦截器, 来达到严格的 RESTful url 风格实现.

/**
 * Invocation 中添加 Method method
 *
  The standard definition is as follows:
  index - GET - A view of all (or a selection of) the records
  show - GET - A view of a single record
  add - GET - A form to post to create
  save - POST - Create a new record
  edit - GET - A form to edit a single record
  update - PUT - Update a record
  delete - DELETE - Delete a record
 *
 * GET    /user      --->  index
 * GET    /user/id    --->  show  
 * GET    /user/add    --->  add
 * POST    /user      --->  save
 * GET    /user/edit/id  --->  edit
 * PUT    /user/id    --->  update
 * DELETE  /user/id    --->  delete
 */
public class Restful implements Interceptor {
  // ...
}


当看完这些之后, 进入到正题.

首先, JFinal 基本上已经可以很好的实现 RESTful 风格的 url 了, 但是仍然有不足, 无论是默认的路由实现, 或者是 Restful 拦截器的实现, 仍然无法做到全可控的 RESTful 实现.
Restful 拦截器的实现实际上是匹配链接以及 Request method, 然后进行 forward 到相应的 action 中.

例如, 参考链接 RESTful API 设计指南 文章中的

DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物

就没办法通过 Restful 拦截器来实现了.

那么, 所有问题都抛出来了.

因此, 这里要说的就是, 把 JFinal 的路由机制给改了下, 达到完美兼容 RESTful 链接, 并且保留 JFinal 的默认实现.

具体实现代码已提交至 iaceob/jfinal

先不讨论怎么实现, 其实原理也很简单, 看看代码很容易理解. 这里主要说说使用方式, 以及一些问题.

使用的时候非常简单, 首先在你的 AppConfig.configConstant 中, 设置开启 RESTful 模式.

@Override
public void configConstant(Constants constants) {
    constants.setRestful(true);
    constants.setXxx();
}


当设置了 constants.setRestful(true); 之后, 路由解析就是 RESTful 风格实现的, 默认是 false, 也就是目前 JFinal 自身的路由实现.

接下来就是定义你的 RESTful 链接格式就好, 下面是一个测试案例.

package com.jfinal.test.restful.test.ctl;

import com.jfinal.core.ActionKey;
import com.jfinal.core.Controller;
import com.jfinal.plugin.activerecord.Record;
import com.jfinal.restful.Method;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

/**
 * Created by iaceob on 2017/3/17.
 */
public class ZooCtl extends Controller {

    private List<Record> getAttrs() {
        List<Record> rets = new ArrayList<Record>();
        Enumeration<String> ens = super.getAttrNames();
        while (ens.hasMoreElements()) {
            String key = ens.nextElement();
            rets.add(new Record().set(key, super.getAttr(key)));
        }
        return rets;
    }

    private Record buildRet(String intro) {
        Record ret = new Record();
        ret.set("intro", intro)
                .set("attrs", this.getAttrs())
                .set("paras", super.getParaMap());
        return ret;
    }

    @ActionKey("/zoo")
    public void list() {
        super.renderJson(this.buildRet("zoo list"));
    }

    @ActionKey(value = "/zoo", method = Method.POST)
    public void addZoo() {
        super.renderJson(this.buildRet("add zoo"));
    }

    @ActionKey("/zoo/:id")
    public void getZoo() {
        super.renderJson(this.buildRet("get zoo by id"));
    }

    @ActionKey(value = "/zoo/:id", method = Method.PUT)
    public void putZoo() {
        super.renderJson(this.buildRet("put zoo by id"));
    }

    @ActionKey(value = "/zoo/:id", method = Method.PATCH)
    public void patchZoo() {
        super.renderJson(this.buildRet("patch zoo by id"));
    }

    @ActionKey(value = "/zoo/:id", method = Method.DELETE)
    public void deleteZoo() {
        super.renderJson(this.buildRet("delete zoo by id"));
    }

    @ActionKey("/zoo/:id/animals")
    public void animals() {
        super.renderJson(this.buildRet("show zoo animals"));
    }

    @ActionKey("/zoo/:zooId/animal/:animalId")
    public void getAnimal() {
        super.renderJson(this.buildRet("show zoo animal by id"));
    }

    @ActionKey("/zoo/:zid/animal2/:aid")
    public void getAnimal2() {
        super.renderJson(this.buildRet("show zoo animal by id, 2"));
    }

}


使用时也是使用 @ActionKey 注解, url 中的值通过 :name 来标识, ActionKey 注解中的另外一个参数就是绑定相应的请求 method.

获取参数也未作改变, 表单传递的参数仍然通过 getPara() 来获取, 使用方式详细见 JFinal 文档.

但是需要注意的是:

  1. 开启 RESTful 模式后, getPara() 方法按索引取值的方法将失去效果
    例如, /zoo/:id/1-3 若这样的请求则会直接返回 404 错误, 因为 RESTful 模式采用的匹配规则较严格(可查看代码实现, 或对该处进行修改令其支持), 另外若是采用 RESTful 风格链接, 应该也不会使用此方式传参.

  2. 存放在 url 中的参数标识无法通过 getPara() 取值.
    若要获取 url 中指定的参数值, 需要通过 getAttr() 获取, 例如 /zoo/:id 获取这个 id 时, 使用 getAttr("id").

  3. RESTful 模式与非 RESTful 的 ActionKey 是不可共用的.
    这个描述可能比较模糊, 但是要表达的意思是, 如果 ActionKey 中的 url 是采用 RESTful 风格写的, 但是却没有开启 RESTful 模式, 就会发生问题, 非 RESTful 模式是不去分析 url 中参数的.

  4. 无需再使用 POST GET 拦截器
    JFinal 提供了 GET POST 拦截器, 用以指定某个路由只能某中请求, 但是开启 RESTful 模式后, 在路由匹配是就已经限定了指定的 method 访问, 因此无需再使用 `@Before(POST.class)` 这样的代码.


使用方式大抵如此, 剩下还有一些可能会令人不爽的小问题.


  1. 使用 getAttr() 获取 url 中参数时, 获取到的值都是 String 类型.
    HttpServletRequest 自身的设计, 在解析完 url 后无法将值写入到 parameter 中, 因此无法使用 JFinal 提供的 getPara() 取值. 后续也考虑过做一个类型自动识别, 但是最终抛弃了, 在不同的系统中, 参数的类型都不同, 自动识别是不靠谱的, 因此建议使用中, 在获取这个参数时都写一个 validator 对这个值进行验证, controller 中就可以对该值进行强转类型.

  2. 性能问题
    也不要想得太过与悲观, 这里说的性能只是相对于 JFinal 自身的路由实现. JFinal 的路由实现非常简单, 甚至可以就简单的看成是字符串匹配, 但是这里的 RESTful 模式, 匹配时使用了正则, 因此效能肯定会比 JFinal 自身实现要来的低一些. 后续会做相关方面性能测试.

  3. 非插拔式
    意思是, 并非提供扩展, 而是通过修改内核实现, 其原因其实看看 JFinal 提供 Restful 拦截器就知道了 = =, 以及 JFinal 的极简的设计缘故. 首先, JFinal 的路由实现, 路由最初加载进来时, 就通过 ActionMapping 隐射到具体的 Action 中, 随后只要有请求就通过 ActionHandler 找到具体的 Action, 因此想要在此基础上实现 RESTful 几乎是不可能, Restful 拦截器的实现也只能通过 forward 来实现, 且无法在 url 中指定参数. 也就是说, 无论你是加自己的过滤器, 甚至创建一个新的注解, 在请求进来时最先经过的 ActionHandler 就以及拒绝了请求, 无法进入到新定义的路由处理中. 因此要实现就只能从 ActionMapping 中入手, 修改路由实现方式.


基本就这样.


@jfinal 官方库收不收这功能, 收我推给你 XDD


------

百度的 ueditor 好难用 = =, 换 markdown 呗, 格式全没了, http://blog.3u3.me/post/jfinal-restful/ 这里有格式.


评论区

JFinal

2017-03-19 18:37

分享贴和代码全有了,非常用心,对 restful 的改进很简单直接,非常感谢你的分享

现在 jfinal 用户基数非常大,所以对于这类改变要非常小心谨慎,如果确实要改进,也需要确保原有的使用方式可以被兼容,楼主的分享中也谈过了一些不足,所以是无法收了这功能的 ^_^

其实,jfinal 一直没有彻底的去支持 Roy Fielding 博士在他论文中提出的 RESTFUL 风格的 URL 是有强烈的原因的

首先,RESTFUL 是一种软件架构风格,该架构风格的内容很丰富,其核心在于资源的抽象,并不在于 URL 的风格,URL 风格仅仅只是 RESTFUL 对 URL 提出的一个建议,并非行业标准,更非事实标准。很多人将 restful 风格的 URL 当成了 restful 架构本身,这是个误解

其次, Roy Fielding 博士在论文中建议的 restful url 风格并不接地气。有些浏览器并不支持 DELETE、PUT 之类的请求类型,而是通过提供类似于 _method 这样额外的参进行模拟。不接地气还体现在:对资源的操作类型很可能会超出 POST、GET、PUT、DELETE 等等类型

最后,jfinal 自身在更重要的资源的抽象上建议 RESTFUL 风格,但对 url 风格保持了自己的优势,那就是将对资源的的操作从 http 的协议中转移到 url 之中,仅此而已,例如 DELETE 这个动作,在 jfinal 中是 abc.com/resource/delete/id

restful 本身是一个软件架构风格,而 url 仅为其中的一个建议,并且这个建议并不是那么接地气,所以现在腾迅、阿里、百度这些巨头的开放平台提供的 url 都是 jfinal 的风格,这里举出一下微信公众平台的 API 的 url 风格:
1:创建菜单:https://api.weixin.qq.com/cgi-bin/menu/create?access_token=
2:新增素材:https://api.weixin.qq.com/cgi-bin/media/upload?access_token=
3:微信支付:https://api.mch.weixin.qq.com/pay/unifiedorder
4:二维码生成:https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=

以上的 url 全部都只使用了 GET、POST 两种请求类型,并且对资源的操作类似于 upload、create 都放在了 url 之中,所以无论是在逻辑上,还是在实践中Roy Fielding 博士的 restful url 仅为一个学术化上的建议,并不适合用于实践

综上,jfinal 在资源抽象这个核心问题上认同 restful架构风格,但对其建议的 url 风格持高度怀疑态度

Dreamlu

2017-03-20 12:32

哈哈,再看看我这篇文章:https://my.oschina.net/qq596392912/blog/845101,你就能实现得更帅了~

abvcb

2017-03-20 14:04

@JFinal 在 WEB 开发中, 也仍同目前 JFinal 的路由风格, 这里实现像这样的方式主要是提供数据服务, 可以想成是第三方服务接口, 类似于 环信的实现方式

http://docs.easemob.com/im/100serverintegration/10intro

在 web 工程开发中也不会使用这种 RESTful 格式的, 这种格式很多问题你这都说了.

这就看有需要的人来用了吧.

abvcb

2017-03-20 14:04

@Dreamlu 观摩 观摩 XD

JFinal

2017-03-20 18:51

@abvcb 如果是非浏览器客户端,而是专用的 http 客户端,确实可以直接在 http 协议中体现 PUT、DELETE 这类请求类型,但这样做其实没什么必要,而在 url 中放入请求类型在语义上表达更为明确,站在 API 的使用者的角度看,更容易理解,可读性也更强

从开发实践的角度来说,在代码中必须去指定某 action 为某请求类型,这也会增加代码量,拉低开发体验。抽象资源用什么请求类型映射至某个 action,要么用约定的方式,要么用注解或配置的方式指令,这都会增加学习成本,但却看不到一点好处

欲风217

2017-03-27 10:19

膜拜大神!

小白Jfinal_

2017-03-27 17:58

我刚才下载了Demo , import 的时候我连选什么都不知道,好尴尬!小白真的是伤不起!

oppos69

2017-03-30 10:16

jFinal能集成swagger

欲风217

2017-04-02 16:26

@oppos69 swagger 会污染源码,不知道你用 swagger 是什么目的?

2017-05-17 13:56

@JFinal 关注波总的JFinal 很久了,并且也私下里会用到JFinal 写一些小东西。非常实用。不过对于波总举例腾讯的例子,我个人持保留意见。不能因为腾讯的官方文档是这样,就否定作者的设计思路。IBM的很多产品,都是严格按照REST风格设计,我们还是拿你给的腾讯的例子来看。1:创建菜单:https://api.weixin.qq.com/cgi-bin/menu/create?access_token= xxx. IBM有一门课程叫 API Management Concepts,里面就明确说明,Do not use the resource path to represents all the resources on the server。当然,这个插件有需要改进的地方。比如与现有JFinal的兼容性等。

JFinal

2017-05-17 17:29

@饭 不仅仅是腾迅,阿里、百度都是如此。我个人非常赞同 REST 风格架构的绝大部分内容,这里仅仅是指 url 风格设计,url 风格并不是 REST 重点

我要做菜鸟

2017-07-24 15:40

把路由分开就好了,有的支持restful url,有的是默认。

mind

2017-08-04 16:56

博士就是博士,大公司就是大公司,大家还是大家,都会变通的。
在认识这个概念上,都有自己的看法,我个人也是有的。
restful概念在没出来之前,就已经出现了一些描述约定,后来博士给了一个比较确定思想,描述一个他的定义,描述了约束,慢慢的开始演变,抽象到一种概念的高度-restful架构;高度抽象的东西,总要有个相,这个相在每一个人心中是不同的,就像大家心中美女,那都是一人一个高度,扯远了。
博士还是伟大的,他帮大家整理了一下这个相,他说有4个成熟度等级:
1、Web服务使用http传输方式进行;这里看起来,soap也是rest的
2、Web服务引入资源,每一个资源都有唯一的标识符和表达方法;这看起来,如何表达资源,如何操作资源,必定会产生认识上分歧。
3、Web服务使用http不同方法完成不同操作,并且使用http状态返回不同结果;着看起来约束超级强悍了,因为强,所以大家都都懂这是在做什么,并不像能随意定义得了的。
举个例子:GET: /zoos/1 和GET: /zoo/get/1 ,看起来也没什么鸟用,但/zoo/get/1是一个纯表达式,而/zoos/1表示一个资源表达式,纯表达式为啥不定义/zoo/find/1,zoo/1/find 呢?想想都是问题,如果在拿着例子定义删除资源,会发现变得哭笑不得。如果zoo下面还有foo呢,资源可以这样定义/zoos/1/foos、zoos/1/foos/1,而表达式定义就更广了,让人感觉怎么都是可以的,累。看起来要求最简单资源表达式式,而且能够表达意义,还能表达状态,不是一件容易的事
4、Web服务资源使用链接,调用者根据链接就知道做什么;感觉好高级,是的这是rest架构需要做的事,目前这种实现起来难度有点高啊,可以查看HATEOAS 以及spring heateoas的实现。
相信波总的对于博文的意见,实用和快而熟的想法,也相信博主辛苦的支持资源定义意见,但总有一个成熟度是适合当前的工作的。博士没有错,大公司也没说他们的是哪一种,我们还是我们。

lwd650

2017-11-23 11:17

@JFinal 现在蛋疼的是很多情况我们开发的服务需要根据甲方的规范实现,并且规范修订的难度更高。从而阻碍了jfinal在此类项目中的应用,比如我所涉及的三大运营商的系统。

lwd650

2017-11-23 11:21

而规范又是那些偏向于学院派的人纸上谈兵式的定义出来,波总的见解很到位,但现实总是很不遂人意。

一岁穿耐克

2018-07-02 19:53

大佬能不能搞个插件来实现?

sky23456

2019-05-28 10:11

@JFinal 波总,jfinal使用restful必须配置一下setRestFul吗,能不能只针对具体的controller类使用,如果配置?这个使用restful的controller类,和普通的controller类使用有什么区别?

JFinal

2019-05-28 11:03

@sky23456 restful 参考:
http://www.jfinal.com/share/230
http://www.jfinal.com/feedback/415

热门分享

扫码入社