如何创建restful api

Submitted by Lizhe on Fri, 06/16/2017 - 15:35

展开这一话题源自一个公司的笔试题 ( 我是帮朋友做的 ), 说实话头一次碰到这样出笔试题的

题目是6个基于json格式的 restful api , 大概是 创建好友关系, 删除, 加黑名单, 活得对应用户的好友之类, 功能都不难, 给1周时间, 但是希望自己控制在4个小时之内做完

标题也挺吓人 <Full Stack Engineer> , 一开始朋友说"全栈"的时候我还是觉得挺无聊的, 试问哪个干J2EE的程序员不是html,jsp,java,服务器都能糊弄几下, 还全什么栈, 全栈这个词可能就是php写多了才弄出来的, 然后我就开始了

好吧我的实际工作断断续续大约持续了10个小时, 几乎用了两天 , 这套题比较有趣, 我有点理解他的"全栈"了, 语言不限, python , ruby, go, java 随意, 框架不限, 用什么framework都可以, 服务器不限, 估计要是用php的 Nginx也行, 最后他要求把代码放在github上这样面试官就能看见你所有的提交版本,思路是怎么样的, 然后要求提供一个公有云服务器, 最好是用Makefile或者docker把服务发布出来,或者让他们可以 1-step command to run your API server locally

这样从技术选型到代码管理,从开发到发布还真就"全栈"了

1 技术选型

说真的我真觉得一开始我就跑偏了, 有点糊弄人的嫌疑, 我从来没认为过 使用maven 像 mvn -Djetty.port=9999 jetty:run 这样来启动服务器 或者是 使用

Server server = new Server(port);

server.start();

这种东西纯java方式来启动服务器能真正用在生产环境中, 拦截器, 线程池, 数据库连接池,对象池 你总得需要一些,这种方式未免过于简单了

不过鉴于现在微服务架构, node.js 之类的东西都挺火, 而且docker一直鼓励 一个容器一个进程 的方式, 所以可能是我自己没见过这种模型的生产环境, 见识太少的缘故

这里为了省事我直接选用了 java 方式启动的jetty

然后我开始考虑用什么构建restful服务, springboot + springmvc 其实是我最熟悉的, 直接用jersey servlet也不错, 想来想去springboot毕竟还是有点重(主要是要下载的东西太多了) , 所以直接用了 代码方式的jetty+jersey 

2 关于url定义

要开发restful api url 的定义需要好好考虑, 推荐的方式把所有api定义在一个大目录下比如 /api

然后你最好根据用例或者模块来区分对应的link , 这里我只有一个user的module , 所以定义成 /api/user

这里多说两句, 这样定义对于大项目的代码结构也是很好的对应

试想两种方式构建代码结构, 我们有user和admin两种用例(模块)

第一种是把user,admin,module1, module2 这些模块的controller扁平化, 都放在名为 com.projectname.controller 的包下,然后service层以此类推

controller/[user,admin....可能有好多个controller]   

service/[user,admin....]

这种模型简单快速不用怎么切割,适合比较小的项目或者刚才说过的微服务之类的

第二种方式从module层直接开始切割

user[controller,service,bean]

admin[controller,service,bean]

这里我推荐第二种,根据以往的经验, 按照模块切割的结构更适合把工作拆分给程序员, 每个人管理自己的module, 如果项目很大的情况下每个module都可以打包成独立的,可以热插拔的jar包, 更利于部署和项目维护

这里基于上面的原因, 所以推荐把api地址设计成  /api/user 和 /api/admin

然后你最好给出一个版本号,毕竟是api, 将来会更新的概率几乎是百分之百, 事实上我现在工作的framework infonova的R6平台也是这样定义api的

r6ApiProvider.registerApiUrlCore('outages/list', 'api/v1/outages/list');
r6ApiProvider.registerApiUrlCore('outages/operator', 'api/v1/outages/operator');

所以这里我们把url定义成 /api/user/v1

3 关于GET/POST/PUT/DELETE

众所周知restful消息使用的http协议的状态码实在是太多了, 百度上你可以找到一堆,而且还对应了一组http方法

POST Create 新增一个没有id的资源
GET Read 取得一个资源
PUT Update 更新一个资源。或新增一个含 id 资源(如果 id 不存在)
DELETE Delete 删除一个资源

实际应用中我们往往不会涉及这么多复杂的定义

/api/user/v1/deleteUser 这种根据方法来定义的模式要比 /api/user/v1/delete 来得常见一些,不过可以根据实际情况考虑

毕竟大多数的restful framework都支持get和post, 而 put和delete用的太少了, 这里偷个懒全post了, 反正post更安全

还有一个考虑是get有缓存, 不想要缓存的时候还要传个时间戳蒙哄浏览器或者别的什么客户端

4 如何处理异常

这个在分层上我还是按照普通java模型做的, 在module层遇到的所有异常全部向上抛(throw到controller层), 在controller层封装各种状态码然后返回对应信息

如果一个请求json成功受理, 我返回了需求里定义好的格式的result, 如果一个json没有成功处理, 比如没找到对应记录(404), 鉴权失败, 服务器内部错误,资源冲突之类的, 我返回原始请求的json数据, 并设置相应的状态码, 然后给原始json数据附加一个 error message

public static Response buildServerExceptionResponse(JSONObject obj) {
        try {
            obj.put("ErrMessage", "There is an error in the server");
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(new GenericEntity<String>(obj.toString()){}).build();
    }

 

259

5 发布

最后出题人要求部署到公有云服务器或者提供docker镜像

对于发布我有两种选择,

1 提供ansible的playbook, 不过考虑到对方拿到我的release代码之后, 他是否真舍得在自己的电脑直接运行playbook, 我放弃了 ( 直接运行一个playbook跟运行一个不知道行为的exe没什么区别, 如果他要在虚拟机上运行, 我还不如提供一个dockerfile)

2 提供dockerfile, 也很简单,实际上我只是打包了一个可执行的jar, 所以即使没有容器也能正常一键运行

# Pull base image  
FROM docker.io/openjdk

#RUN yum install yum -y install java-1.8.0-openjdk*

# Expose ports.
EXPOSE 8888

COPY ./release_jars /release_jars

# Define default command.
ENTRYPOINT java -Djava.ext.dirs=/release_jars com.qingxin.server.JettyServer

使用这个dockerfile创建镜像和容器

docker build -t qingxin/hfm .

docker run -t -i -p 8888:8888 --name hfm qingxin/hfm

最后如果你有兴趣可以去  https://github.com/zl86790/hfm 下载这个例子

这里还有一个springboot+tomcat的版本 https://github.com/zl86790/HappyFriendsManagementSpringBoot