ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

REST API设计规范:从资源建模到HTTP方法、状态码与Floodlight实战

REST API设计规范:从资源建模到HTTP方法、状态码与Floodlight实战 1. 先承认一个事实很多人写的REST其实是披着JSON外衣的RPCREST这个词在技术圈里被念叨了快二十年但说实话我在实际评审过的接口里能称得上REST风格的连一半都不到。大多数团队的接口长这样GET /api/getUserInfo、POST /api/deleteOrder、PUT /api/updateProduct——看一眼URL就知道这根本不是REST这是把RPC的思维换了个皮把XML换成了JSON而已。那REST到底是什么简单说它不是一种协议也不是一套工具而是一种基于资源来组织接口的设计风格。核心就一句话把一切业务对象都抽象成资源用HTTP协议自带的动词去操作这些资源让HTTP本身成为平台无关的通用接口描述语言。本文会从最底层的资源建模讲起把URL命名、方法语义、状态码、请求响应细节、幂等性这些REST接口涉及的环节逐一拆开最后用Floodlight控制器自带的REST API作为真实案例教你怎么从零访问一套完整的REST服务。适合三类人看刚接触后端开发、正准备设计第一个对外接口的新手写了很久接口但风格始终被吐槽的进阶开发者以及需要在SDN网络环境下调试控制器的运维或实验人员。我先说一个最容易让新手懵的地方REST的英文全称是Representational State Transfer翻译过来叫表现层状态转移。这个翻译害人不浅听起来像是某种玄学其实它的意思非常朴实——客户端访问服务器上的资源时服务器把自己当前的状态表现出来客户端拿到这个表现后再发起下一次操作从而推动整个业务状态往前转移。打个比方你去银行办事银行不是一个全靠电话沟通的接线员而是一个个有固定编号的窗口。你拿着账户这个资源跟窗口说我要查询余额GET、我要存钱POST、我要改预留手机号PATCH、我要销户DELETE。每个动作都是针对账户这个名词资源进行的而不是每次打电话说喂帮我把钱存进去——那样的话每个操作就是一个全新的命令根本没法标准化。这就是REST和RPC最本质的区别RPC关注的是方法REST关注的是资源。方法和资源的差别决定了你写出来的接口是昙花一现的临时对接还是能长期演进的通用服务。2. 写一个REST接口前先学会设计URL、方法和状态码很多人以为REST的难点在JSON格式怎么写、框架怎么配置其实那些都是工具层面的事。真正决定一个接口像不像REST的是URL的语义、HTTP方法的选择、状态码的表达这三个基本功。我按顺序逐个说。2.1 URL设计永远用名词别让动词出现在路径里REST接口的URL应该被当作资源的地址来对待而不是一个远程函数名。通俗地说URL只回答你在哪里方法才回答你要干什么。拿一个电商系统举例错误的写法是这样的GET /getUserList POST /createOrder POST /updateOrderStatus GET /getOrderDetail?id123 POST /deleteUser这种写法的毛病很明显URL里的动词已经和方法动词重复了而且一旦业务变化比如更新订单状态变成了批量更新订单状态接口名就得跟着改调用方全部要跟着升级。正确写法是用复数名词表示资源集合用路径层级表达资源从属关系GET /users // 获取用户列表 GET /users/123 // 获取某个用户 POST /users // 创建用户 PUT /users/123 // 整体替换用户 PATCH /users/123 // 局部更新用户 DELETE /users/123 // 删除用户 GET /users/123/orders // 获取这个用户下的订单列表 GET /orders/456 // 获取某个订单注意两个细节这两个细节能直接看出一个人有没有真正写过REST接口第一层级不要无脑嵌套。/users/123/orders/456这种三层以上的写法要谨慎通常到第二层就够用了。因为当路径越来越深你对到底哪个是主资源的判断就会变得模糊。如果是按订单维度查询用户那你应该反过来设计成/orders/456/user从属于订单这个主资源。谁的ID作为起点谁就是当前上下文的主资源。第二改写操作不要用动词伪装。比如下单确实包含了业务动作但它的本质是创建一个订单资源所以应该就是POST /orders。支付也是同理如果把它当作文档创建那是POST /payments让支付结果成为一条可追溯的资源记录。你会发现一旦彻底贯彻名词资源的思路很多看起来复杂的业务动作都能被拆解成标准的增删改查。2.2 HTTP方法的语义GET、POST、PUT、PATCH、DELETE的边界在哪里HTTP方法本身自带了一套语义契约REST风格要求你严格遵循这套契约这也是为什么REST接口可以实现零文档也能猜着用的原因。我把这套语义做成一张表建议直接收藏方法核心语义是否安全是否幂等典型场景GET获取资源是是查询列表、查询详情POST创建资源或触发复杂动作否否新建订单、上传文件PUT整体替换资源否是用完整数据覆盖更新PATCH局部更新资源否通常幂等只修改某个字段DELETE删除资源否是删除记录这里有两个高频误区几乎每个团队都会踩第一个误区是把POST当万能药。有些团队嫌方法多麻烦什么操作都统一POST理由是反正POST啥都能干。这确实不是技术错误但它会把接口的语义糊掉。调用方看文档才知道这个POST是创建还是更新而REST的设计初衷恰恰是让调用方不看文档也能通过方法名和URL猜到意图。一旦全用POST这个优势就彻底没了。第二个误区是不清楚PUT和PATCH的区别。PUT是整体替换意思是客户端要提供完整的最新字段服务器拿这个完整数据直接覆盖旧的PATCH是按需修补客户端只提供想改的字段。一个经典的例子// 修改用户手机号用 PATCH PATCH /users/123 Body: { phone: 13800138000 } // 全量覆盖用户所有字段用 PUT PUT /users/123 Body: { name: 张三, age: 30, phone: 13800138000 }如果只用POST Body里塞一个字段methodupdate那接口就和RPC没区别了。你失去的不只是风格上的美感还有HTTP代理、缓存、重试机制这些基础网络设施对你接口的原生支持——这些设施全都默认按GET/POST/PUT/DELETE的语义来工作。2.3 状态码这是REST接口最容易翻车的环节状态码的作用是让调用方只看HTTP状态就能知道这次请求大概发生了什么。一个合格的REST接口状态码的选择优先级比你返回的JSON错误信息还高,因为各种网关、负载均衡、监控系统都会直接读取状态码做判断。我见过最典型的反例是业务错误全部返回200只在响应体里塞一个code: 500。这么做给调用方带来了巨大的麻烦因为客户端必须解析响应体才知道请求失败了而监控系统看到清一色的200认为服务一切正常直到线上事故爆发才发现异常。这是把HTTP状态码的地位彻底架空了。正常的用法是这样的状态码含义使用场景200请求成功查询成功、更新成功201创建成功POST新建资源完成并在响应头Location里给出新资源URL204无内容删除成功不需要返回Body400请求参数有误缺少必填字段、JSON格式错误401未认证未登录或token过期403无权限已登录但没有操作该资源的权限404资源不存在URL路径错误、ID不存在409状态冲突重复创建、条件不满足422语义错误参数格式对但业务上不可接受500服务器内部错误代码异常503服务不可用过载、依赖服务宕机关键是语义要对齐。比如创建一个用户如果用户名已经存在应该返回409 Conflict而不是返回200然后说创建失败。再比如前端传了个不可解析的JSON结构应该返回400 Bad Request而不是422——因为请求在语法层面就有问题还没来得及进入业务层。我还要提醒一个经验之谈401和403不要混用。401的意思是我不知道你是谁403的意思是我知道你是谁但你没资格碰这个资源。很多团队嫌麻烦全都返回401导致前端无法区分需要重新登录和功能权限不足最后只能在中间层再做一层逻辑处理平白无故增加复杂度。3. 请求和响应的细节里藏着REST接口的形与魂URL、方法、状态码是REST接口的骨架但光有骨架还不够。一个完整的REST接口请求头、响应体、错误处理、幂等性设计这些细节直接决定了你的接口好不好用、协作顺不顺畅。这一节我把这些软组织单独拿出来说明白。3.1 Content-Type与Accept别让你的接口天生只服务一种客户端Content-Type是REST接口最容易被人忽略但又最关键的字段之一。它告诉接收方我发过来的这个Body是用什么格式编码的。在绝大多数团队里这个值就是固定的application/json这没问题但你需要知道它背后代表的是标准的媒体类型协商机制。再看Accept请求头。它表示客户端期望接收什么格式的响应。很多人从来不会在意这个头但标准的REST实践应当是服务器读取Accept头能返回匹配格式就返回不能匹配就返回406 Not Acceptable。其实这是有实际意义的——如果有一天你的接口要同时服务浏览器页面和手机App浏览器希望拿到HTMLApp希望拿到JSONAccept头就是天然的版本开关。来看一条完整的、符合REST约定的curl请求长什么样curl -X GET http://localhost:8080/api/v1/users?page1page_size20 \ -H Accept: application/json \ -H Authorization: Bearer eyJhbGciOi...这里有个实操经验Response头里的Location字段值得用好。当POST /users创建资源成功时除了返回201 Created更标准的做法是在响应头的Location里直接放上新资源的URL。这么做的好处是调用方不需要猜新用户ID是多少直接GET Location就能拿到完整资源。这属于REST风格里既简单又令人眼前一亮的小细节但我很少看到有团队认真做。3.2 统一错误响应结构别让调用方针对每个接口单独写错误解析你可以想象一下这个场景你们团队有十个接口五个的异常返回结构是{error: xxx}另外三个是{msg: xxx}还有两个是{message: xxx}。前端同学每接一个接口就要去查一次文档然后在代码里写一套新的分支判断。这就是典型的接口肥大问题不是响应内容的问题而是错误结构没有统一。我个人的惯用写法是所有接口无论成功失败都返回同一个外层结构{ code: 0, message: success, data: { } }当出现业务错误时code值变为非零对应一个全局统一的业务错误码表message给出人话版本data置为null。HTTP状态码依然严格按2.3节的规则来业务错误绝不返回200。这个结构的好处是基础对接层可以抽象成一套通用的拦截逻辑每个前端团队只要封装一次请求函数后续所有接口的异常处理逻辑就自动收口了。但有一点要注意业务错误码表不要用魔法数字。不要出现code: 10001却没有文档解释10001到底是什么。要么用字符串错误码如USER_NOT_FOUND要么在错误表里强制登记。我在代码评审里看到code: 50001这种字段时第一反应永远是去找它对应的注释找不到就觉得这个接口不靠谱。3.3 幂等性很多人没搞懂但高峰期扛不住流量时才想起它幂等性这个概念说人话就是同一个请求执行一次和执行一百次最终产生的结果是一样的。这对REST接口的健壮性至关重要因为网络环境不可能永远可靠客户端极有可能因为超时而重发请求。GET、PUT、DELETE天然是幂等的查三次还是同一个结果用同样的完整Body覆盖三次结果一样删除三次第一次成功了后面的就当不存在处理结果也一致。POST不是幂等的创建一个订单发两次会创建出两个订单。所以需要靠业务侧做去重。常见的方案是客户端在POST创建时带上一个全局唯一的请求幂等号比如Idempotency-Key请求头或者放在Body里的一并把client_request_id字段。服务端拿到这个ID后缓存处理结果遇到重复请求直接返回首次结果避免重复下单。这个设计我现在看来属于那种你用不上时觉得多余一旦遇到客户端超时重放、消息队列重复消费就知道它多值钱了的细节。如果你的接口要面向第三方开放幂等设计几乎是必答题。4. 用Floodlight控制器亲手实操一次完整REST API访问讲了这么多理论接下来进入实战环节。我选Floodlight作为实操对象是因为它是一个典型的、接口覆盖相当全面的开源SDN控制器自带了大量REST API操作起来直观而且它在网络实验、课程设计、SDN开发中出镜率很高。你可以把它当成一个现成的REST服务端用最原始的curl命令去验证前面几节讲到的所有原则。4.1 Floodlight是什么以及它的REST API长什么样Floodlight是一个基于Java开发的OpenFlow控制器核心功能是管理交换机的流表、感知网络拓扑、维护主机位置信息。你可以把它理解成软件定义网络SDN里的大脑——OpenFlow交换机是手脚Floodlight决定每个数据包该怎么转发。它对外提供了一套基于HTTP的REST API用来查询和配置控制器的运行状态。这套API非常典型以资源为中心、路径即资源、方法即操作。下面是几个常用的接口功能方法URL说明查看所有交换机GET/wm/core/switch/all/json获取控制器连接的OpenFlow交换机信息查看网络拓扑连接GET/wm/topology/links/json获取设备之间的链路信息查看主机列表GET/wm/device/获取网络中发现的主机设备信息下发静态流表POST/wm/staticflowentrypusher/json向指定交换机下发流表规则删除流表DELETE/wm/staticflowentrypusher/clear/json清空所有静态流表注意看这些URL的设计它们都由模块名、资源名和/json后缀组成比如/wm/core/switch/all/json。/json后缀是Floodlight特有的接口格式声明它的作用等同于其他框架里的Accept: application/json头——告诉控制器我要的是JSON格式的响应。这套API的设计风格也许不算教科书级REST但它的方法使用是清晰的查询全部用GET下发规则用POST删除规则用DELETE。你在它的基础上观察就能很好理解资源与方法组合的表达方式。4.2 安装配置Floodlight几个关键步骤与易踩的坑我以常用的开源版Floodlight为例说一套经过验证的搭建流程。你别嫌我啰嗦这套流程按顺序操作基本一次就能跑通。第一步准备环境。Floodlight底层是Java老版本的Floodlight比如1.2版依赖JDK 1.8新版本对版本要求更宽松但为了稳妥起见我还是建议你直接用JDK 1.8。另外建议准备一个Linux环境Ubuntu或CentOS都行Windows虽然勉强能跑但网络模拟环境建议还是用Linux更可靠。第二步获取源码。开源版Floodlight可以直接从GitHub拉取git clone https://github.com/floodlight/floodlight.git cd floodlight如果你下载的是带floodlight.sh启动脚本的发行包那第三步直接执行启动即可如果是源码包需要先编译。编译用Ant构建别用MavenFloodlight老版本用的是Antant编译产物会生成在target/目录下核心的就是一个floodlight.jar。第三步启动控制器。有两种方式任选其一# 方式一直接用脚本启动 ./floodlight.sh # 方式二手动指定JAR包启动 java -jar target/floodlight.jar启动完成后日志里会出现监听地址相关的输出显示REST服务已经就绪。这里要特别注意一个坑Floodlight的REST服务默认监听在8080端口而OpenFlow协议通信默认监听在6633端口。别把两个端口搞混了——浏览器访问、curl调REST接口用的是8080控制交换机连上来用的是6633。如果你启动后发现8080端口没有监听多半是两种原因一是Java环境变量问题导致启动中途报错二是防火墙拦截了端口。排查方式很简单# 查看Java进程是否存在 jps -l # 查看8080端口是否在监听 ss -tlnp | grep 8080第四步如果只想跑最简单的单机实验可以不额外配置任何东西直接访问REST接口就行。但如果你要管真实交换机或使用Mininet做拓扑模拟记得让交换机的控制器地址指向这台机器的IP端口指向6633。4.3 用curl访问Floodlight REST API把每个细节看清楚等你把Floodlight跑起来就可以打开终端一步步验证接口了。第一次尝试用GET查询所有交换机的信息curl -X GET http://localhost:8080/wm/core/switch/all/json如果控制器还没有任何交换机接入返回的是一个空数组[]。别慌这是正常的。可以配合Mininet启动一个测试拓扑sudo mn --topo single,3 --controller remote,ip127.0.0.1,port6633Mininet启动后交换机会自动连接到Floodlight。然后再次执行上面的curl命令你就能看到类似下面这样的JSON响应里面包含了交换机的DPID、状态和计数器信息[ { dpid: 00:00:00:00:00:01, ports: [ { ... } ], connectedSince: …, numTables: 254 } ]接下来用GET查询网络拓扑的链路信息curl -X GET http://localhost:8080/wm/topology/links/json返回的本应就是交换机之间互联的链路列表每条链路包含src-switch、src-port、dst-switch、dst-port等字段。再试一试用POST下发流表规则这是Floodlight REST API里最常用也最能体现创建资源语义的操作。比如我要让数据包从交换机的1号口进来之后从2号口转发出去curl -X POST http://localhost:8080/wm/staticflowentrypusher/json \ -H Content-Type: application/json \ -d { switch: 00:00:00:00:00:01, name: flow-1, priority: 100, ingress-port: 1, actions: output2 }注意看这里的设计URL是/wm/staticflowentrypusher/json这个资源集合POST的Body则是创建流表规则所需的完整参数。响应里如果能看到{status: Entry pushed}之类的提示就说明流表下发成功了。最后用DELETE清空所有静态流表curl -X DELETE http://localhost:8080/wm/staticflowentrypusher/clear/json走到这一步你已经用GET、POST、DELETE三种方法完整地操作了一遍Floodlight的REST API。4.4 从Floodlight接口反推REST设计这套API有哪些值得学、哪些不推荐拿Floodlight这套接口当教材除了学怎么访问更要学会反推。我当初就是通过反复观察这套接口才真正把REST风格从理论落实到手感的。值得学的有三个点第一每个模块对应一组独立资源。/wm/core/...管核心信息/wm/topology/...管拓扑/wm/device/...管设备/wm/staticflowentrypusher/...管流表。URL的第一段就是命名空间哪怕接口很多也不会乱。第二查询参数用于过滤而不是扩展新URL。Floodlight在个别接口里支持用查询参数做过滤比如限制返回字段。这完全符合REST风格资源集合是同一个东西通过参数缩小范围。第三方法和动作对应明确。获取资源就是GET创建就是POST清空就是DELETE没有出现/getAllSwitch、/deleteFlow这种动词URL。这说明它的设计者是有REST意识的。不推荐学的地方也有URL末尾的/json后缀其实是把响应格式写进了路径里混入了实现细节。更规范的REST做法应该是在请求头里加Accept: application/json。另外它没有做严格的内容协商返回结构在不同版本间也有差异。你在自己设计接口时不要模仿这种后缀写法路径里只保留资源标识就对了。5. 评审时经常抓出来的REST写法翻车现场以及对应的纠正方案最后一章我把近几年在代码评审和接口联调中反复遇到的REST写法问题汇总成一个翻车清单。每一条我都给出了错误示范和纠正方案你完全可以把这一节当成自测手册写完接口后对照着自查一遍。5.1 翻车一把REST写成了RPCURL里全是动词这是最普遍的问题出现在大量自研后端项目里。表现是URL里全是/getUser、/deleteUser、/updateUserStatus。当你看到这些接口时无论上下文环境是什么样的都能直接断定——团队里没人对REST风格做统一约定。纠正方案在前面已经说过了这里再给一个动词消失的对照表错误写法正确写法GET /getUserInfoGET /users/{id}POST /createOrderPOST /ordersPOST /updateOrderStatusPATCH /orders/{id}POST /deleteUserDELETE /users/{id}GET /getDeviceListGET /devices一句话记住动词交给HTTP方法名词交给URL别让它们抢戏。5.2 翻车二状态码永远是200错误全在Body里说这个问题的根源往往在于后端同学图省事——反正错误也要返回JSON给前端那我统一返回200前端统一解析JSON不就行了这话在小型内部项目里也许能对付过去但只要是对外接口或者稍微上点规模的项目就会出问题监控系统误报服务正常、网关缓存了业务失败的结果、客户端不知道请求到底是网络错误还是业务错误。纠正方案就是严格执行2.3节的状态码映射表。5.3 翻车三分页、排序、过滤没有标准格式列表接口是REST里最容易被写烂的接口。有的接口用/users/list?p2有的用/users/page?offset20limit10还有的干脆一次性把一万条数据全塞进响应体把应用内存直接打爆。统一的推荐做法是GET /users?page1page_size20sort-created_atstatusactive响应体返回{ data: [ ], meta: { page: 1, page_size: 20, total: 156, total_pages: 8 } }分页参数的名字可以各不相同但一个项目里必须有一套固定下来的约定。排序用字段名加前缀方式-created_at表示按创建时间倒序created_at表示正序。这属于REST查询参数设计里的常见约定虽然没有硬性标准但约定俗成能极大降低沟通成本。5.4 翻车四响应字段命名风格混乱大小写混用在同一套接口里今天返回userName明天返回user_name后天返回name。调用方想骂人想写一套通用解析逻辑都做不到。REST接口虽然没有强制规定字段命名用哪种风格但一旦选定必须全局统一。我建议在Java后端项目里统一用驼峰还是下划线都不重要重要的是全局一个标准。5.5 翻车五不处理不存在或不可解析的请求有团队对不存在的路径直接返回空壳{}对格式错误的JSON返回200。这两类情况必须区分路径不存在是404JSON解析失败是400。如果图省事全返回200调用方排查故障时就像在雾里看花。最后再分享一个我实测下来很有用的自检方法用curl的-i参数看完整响应头。每写完一个REST接口先别急着写前端对接自己用curl -i请求一遍看三件事第一状态码是否语义正确第二响应头Content-Type是否为application/json第三错误响应体是否符合全局统一格式。如果这三项都过关你的接口才算是有了REST的魂。REST不是一个能靠背诵标准学会的东西它更像一种设计习惯。你每写一个接口时都想想这个URL去掉动词后还读得通吗客户端看状态码能秒懂发生了什么吗坚持一段时间风格自然就正了。
返回列表