REST架构指导方案

编程

REST架构指导方案

[TOC]

何为REST

在2014年之后,社区中关于RESTFUL风格的文章开始渐渐多起,大多数RESTFUL的文章都是在阐述一种HTTP URL路径的写法风格。简单总结来说,这些文章归纳的点主要是:

  1. URL路径应该是名词而非动词。
  2. 通过HTTP几个动词:GET,POST,PUT,DELETE来对“资源”进行CURD操作。

但是为何要是名字,又为何非得通过Http 方法动词来完成CURD操作,往往语焉不详。因此,想要完整正确的理解REST,仍然要从该名词的诞生处, Roy Thomas Fielding 博士关于REST架构的论文《Architectural Styles and the Design of Network-based Software Architectures》(架构风格与基于网络的软件架构设计,以下简称设计) 中寻找答案。

在设计一文中,首先对基于网络的软件架构提炼和归纳了几种有明显特征的设计风格,诸如有:

  1. 流式数据风格,例子有:数据过滤器模式,统一接口的数据过滤器模式。该风格强调的是数据在类似“管道”的概念中流动,并且流动的过程中不断被处理和转换。
  2. 复制风格,例子有:多数据源模式,缓存模式。该风格强调的是所要求访问的数据,存在多于一个的存储点,通过增加存储点来提升整体性能。
  3. 层次风格,例子有:CS模式,分层模式,远端Session模式,远程数据访问模式。该风格强调将系统划分为不同的层次,每一个层次完成特定的功能,并且隐藏其之后(之下)层次的复杂性。
  4. 移动代码风格,例子有:虚拟机模式,按需编码(编码下发)模式。该模式强调的是通过改变处理流程和数据源之间的“距离”来提升系统的扩展性。比较形象的例子有早期Java Applet应用。
  5. 点对点风格,例子有:基于事件集成模式,分布式对象模式。该模式强调的是形成系统的组件之间,彼此之间通过某种“连接方法”直接交互。整个系统内部的交互成网状结构。

对于何为“架构风格”,设计一文中对架构风格的定义是:架构风格是特定的一组约束的名字描述。换句话说,风格是一个特定的约束集合。而REST风格并不是凭空产生的,他是根据现代WEB架构所期望实现的一些属性或者目标,从空集合中逐步添加不同的约束,进而不断满足WEB架构的目标,最终得到的一个成果。实际上,有些架构约束,在现代看来,属于司空见惯的做法。下面,从论文的角度,来逐步看看REST风格的约束是如何添加的。

首先,REST风格要求系统是基于CS模式。WEB浏览器本身就是一个弱化的,非特定的瘦客户端。在WEB系统中谈这个似乎显得比较奇怪,但是需要注意的是,REST架构是针对基于网络的应用而言的,而不是基于HTTP而言的。比方说FTP模式,也是基于网络的应用,但是其设计结构就不符合REST的要求。因此,CS模式,成为REST风格的第一个约束。该约束,实际上期望的效果是基于网络的软件在不同的层次上独立演化,client和server互不干扰。

其次,REST风格要求应用交互本身是无状态的。这就意味着,每一次组件之间(比如浏览器到服务端)的请求,都包含了完整的信息和语义,服务端不存储任何客户端的状态信息。这个约束的目的是为了提升服务端的扩展性。由于服务端不保存客户端状态信息,因此服务端可以通过扩展自身数量来提高性能和可靠性。客户端可以将请求发往任意的服务端节点而不必担心无法收到正确的响应。在现实中,有一个明显的反例就是WEB服务器的Session机制,由于保存了客户端状态信息,因此横向扩展就成了问题。

再次,REST风格要求请求的响应本身支持缓存标记,如果响应被标记为可缓存的,则客户端后续会直接使用该响应,不会再次发起网络请求。该约束主要是为了减少组件交互次数,提升应用的整体性能。比较常见的例子是图片服务器,由于图片变化不频繁,通常而言,都会在响应头中标记本次响应可缓存和有效时长。

再次,REST风格要求组件之间通过统一的接口进行交互。统一的接口可以通过反例来理解,比方说有一个系统,其对外提供了Http API访问,也提供了Dubbo的接口访问;这对于客户端而言,就同时存在两种接口模式。这就违反了统一接口约束。实际上,由于HTTP协议的普遍性,其天然适合成为统一接口。因此,采用REST风格的系统,都采用HTTP协议作为统一接口对外开放能力。

再次,REST风格要求系统整体设计上,组件按照需要进行分层,每一层只能看到与其相邻的层的内容。分层的主要好处在于给内部组件划分了明确的边界,方便其独立演化和部署。通过分层,也降低了组件之间的耦合性。比如常见的流量网关Nginx,业务网关API Gateway,都是分层约束的产物。

最后,REST风格要求系统支持按需编码(编码下发)约束。以例子来描述,就是客户端可以通过下载代码或者脚本的形式,来扩展客户端的功能,比如Java Applet。这一点在前端开发中较为常见,加载不同的js脚本,实现不同的功能。通过对脚本的升级,应用在“客户端”侧的能力得到了扩展。

从约束合集的角度来看,REST风格包含了上面提到的6种基本约束。而从架构元素的角度来看,REST风格包含了三种元素:

  • 数据元素
  • 连接器
  • 组件

数据元素对整个架构的指导意义,放在后面重点说明,先说说说连接器和组件这两个元素。实际上,这两个元素在当今以Http协议为主的WEB应用中,基本已经约定俗成了。

首先来说下连接器,连接器是对资源的表示的获取和转移活动的封装,其代表的是组件通信活动的抽象表达。在REST中的连接器主要有:

连接器

示例

客户端

Libcurl

服务器

tomcat

缓存

浏览器缓存

解析器

DNS查找库

隧道

SSL

最常见的连接器就是客户端和服务器。两者的主要区别是客户端通过发起请求来获得资源的表示,而服务器则监听请求进而响应某一资源的表示。部分组件可能同时具备两种连接器类型,比如流量网关Nginx。

接着来看看组件。组件是根据在整个系统中的角色来定位的。总结来说有以下四种

组件

示例

来源服务器

tomcat

网关

nginx

代理

VPN

用户代理

浏览器

组件很好理解,就不展开了。下面来说下三大元素中对架构影响最大的数据元素。

数据元素细分之下可以分为2种:

  • 资源
  • 资源的表示

资源是一个概念,是对一个实体,一组实体,甚至一个服务的概念映射。一个具体的文档,一组文档,甚至于当前温度的在线查询服务都可以是一个资源。需要重点明确,资源是一个映射到实体的映射关系,而不是实体本身。举个例子,git或者svn中的最新版本,或者master版本,这个定义是一个资源;而这个资源所指向的具体代码版本是不固定的,可能每次获取都不同(因为其他人提交了),每次获取到的实体不同。而版本1.0指向的代码版本,则是静态的,无论何时访问,都会得到相同的响应。在这个例子中,master版本和1.0版本就是2个完全不同的资源。资源对应的值可能是会变化的,但是定义资源的语义本身,是静态的。诸如master版本,这个描述对应的含义是不会变化的,永远都是“主干版本”,而主干版本拉取的值本身则是不断变化的。

资源是一个概念,因此通过连接器传输的实际上资源的表示。表示是一组二进制数据以及描述这些二进制数据的元数据,或者说,表示是由数据,描述数据的元数据,以及描述元数据的元数据构成。REST风格的核心,就是在不同的组件之间,传输资源的表示。并且通过资源表示的传输来实现具体的功能。

在REST风格中,系统的状态体现在2个地方:

  • 资源的当前状态
  • 客户端的状态

资源的表示,可以被认为是对资源的当前状态的一种快照或捕获。因此获取资源的表示,相当于获取了资源的当前状态。而应用本身可以通过获取资源的表示来改变自身的状态。举个例子,浏览器通过访问网站,获取到了文档,图片等资源的表示(具体而言就是下载了html文本,css样式表,图片二进制数据),在全部资源表示获取完毕后,浏览器此时处于一个稳定状态。该稳定状态会一直维持直到下一次点击并且尝试获取新的资源表示。并且客户端可以通到将新的资源表示发送给远端要求更新远端的资源表示,进而更新资源的状态。

综上,REST风格是一组架构约束的合集,限定在基于网络的应用架构,是一种风格指导而非具体的实践。其核心架构要素在于对资源的描述和通过对资源表示的传输来改变系统整体的状态。

在WEB系统中应用REST风格

应用约束

在WEB系统中应用REST风格,首先从架构约束开始实践。这里面最重要,也影响如今设计最大的约束在于REST风格要求服务端不保存客户端状态,该约束是提高服务端扩展性的关键举措。HTTP协议本身是无状态的,在单机应用中,为了保存客户端状态都是采用容器内Session的方案,这样就成了有状态的服务端。去状态的做法也很简单,将容器内的会话信息保存至第三方的组件,比如说共享KV缓存上。客户端通过传输token标记来标识自身身份,业务处理服务器仅仅处理业务逻辑,而对客户端身份的校验,获取都可以由公共的身份校验服务完成,从而提升了业务处理服务器的横向扩展性。针对这一点,可以采用的方案很多,比如JWT或者单纯的将标识符存放在共享KV存储上。

以资源的形式描述系统

REST风格中,最重要的元素就是资源和资源的表示。将整个系统都按照资源的方式去规划并不简单,但是却是比较高效的一种抽象;这种规划方式要求将系统看成是不同的静态语义的集合,系统的功能通过静态语义的状态变化来提供。

资源不是一个动作,而是概念,这也是很多文章中提到的,REST是名词而非动词的原因。系统对外提供的功能总体上分为两类:

  1. 通过获取资源的当前表示来得到数据(读)
  2. 通过发送资源的表示来更新资源的状态(写)

读的动作可以包括:获取资源的表示,获取资源表示的元数据;写的动作可以包括:更新资源的某一表示的全部内容,更新资源的某一表示的部分内容,删除资源的某一表示。

以上这些动作,都可以由Http的方法动词来担任,分别是:

  • GET:获取资源的某一表示
  • HEAD:获取资源的表示的元数据
  • POST:更新资源的某一表示的部分内容
  • PUT:更新资源的某一表示的全部内容
  • DELETE:删除资源的某一表示

考虑到put方法代表的是更新资源的某一表示的全部内容,一般认为put方法是幂等的。而post方法只是表示的部分内容,因此一般被认为是非幂等方法。

资源的表示本身是静态语义,但是其值可能不断变化,因此针对表示本身,get方法是非幂等的。但是针对语义本身,get是幂等的。

delete方法,因为其作用,天然为幂等。

资源的具体定位,则可以通过URI来完成,或者确切的说,就是通过HTTP URL来完成。HTTP URL用于定位资源,显然,其应该是全名词性的描述。

RESTFUL的URL路径实践

上文说到,REST是一种风格,因此我们将WEB系统中URL规划符合REST风格的称之为restful。REST不是具体的架构,因此我们可以说某一个系统比另外的系统更restful,却难以说一个系统是restful而另外的系统不是。

回归到路径规划这一问题上,有一些最佳实践可供参考。

单一资源的路径制定

以用户信息为例,通过唯一标识符,比如userid来获取用户信息,可以将url制定为

/xxx/user/{userid}

此时可以通过get方法来获取该userid的用户信息,可以通过post更新用户的部分信息,而put方法,既可用于用户数据的全量更新(除了userid,因为它是定位信息的一部分),也可以用于新增用户,如果此时该用户id不存在。put方法是新增用户可以从这个角度去理解:put方法将具体userid的用户用户信息的表示从空变更到了其提交的表示数据。

复杂查询的路径制定

提交需要参数进行查询获得数据是一个常见的业务需求。一般认为查询都是get方法,而参数则是跟在?后面进行拼接,比如有这种url:/xxx/queryUser?name={name}&age={age}&address={address}。这种写法有两个地方违背了REST风格:

  1. url用于定位资源,其本身是不变的。而?后面跟参数显然不满足这个实践。
  2. url用于定位资源,其本身是名词性的,不应该出现动宾短语,如queryUser。

复杂查询的路径制定要从资源的角度去看待。首先,资源可以是一个服务,而查询服务显然是可以认为是一个资源的。其次,可以通过提交资源更新请求来获得资源的最新状态的表示。

以这两者为改造思路,首先将url改造为一个服务的名词路径,如/xxx/userQuery或者/xxx/userQueryService。然后通过post方法来提交资更新请求来获得查询服务的最新表示,换句话说,就是将查询参数通过post发送,来获得查询结果。

总结一下,复杂查询的路径制定首先是将查询本身看成是查询服务,进而以资源的方式表示,可以表示为xxQuery或者xxQueryService。而后通过post提交资源更新请求来获得查询服务资源更新后表示,也就是查询结果。

复数资源的路径制定

复数资源的获取其实和复杂查询本质是一样的,以用户为例,可以将路径规划为/xxx/users。将users看成是一个资源,通过post来提交要查询的id数组来更新该资源的状态进而获得更新后的表示,也就是复数情况下的查询结果。

动宾操作的路径制定

有一些场景,动宾结构或者说动词路径是存在已久,也符合直觉认知的。比如登录动作,常见的都会规划为/xxx/userlogin或者/xxx/login。

将动宾路径规划为rest路径的思路和复杂查询的思路一致,一个动词可以看成是对一个服务的更新或者新增要求,因此很容易从动词的效果上去抽象服务的资源标识。比如登录,其动词的效果是在线用户新增了,因此可以将路径规划为/xxx/onlineUsers。通过post方法,向该资源更新部分表示内容,更新的表示内容就是要登录的用户信息(可能是用户名和密码之类的)。

版本号位置

存在版本号的实体非常常见,而版本号的位置规划存在两种方式:

  1. 实体具备不同的版本,不同的版本可以认为是同一个资源的不同表示。而url路径是用来定位资源的,资源的不同表示不在URI定位的职责内,从这个角度出发,版本号应当存放在http header中。
  2. 将特定版本的实体认为是一个资源。而URI要定位该资源,则需要在URI中体现版本,此时可以规划如/xx/v1/user/{userid}的路径

具体选择哪一种方式并无优劣之别,只要整体系统保持一致即可。

公共参数位置

在系统设计中,除了业务参数外,往往还存在一些公共参数,用于表达身份,流量控制,鉴权等等非功能性需求,这部分需求的参数,不适合放在Http内容体中,因为往往在传输的过程中,网关等组件都需要根据公共参数调整自己的行为,因此公共参数应当放在Http header中传递。

代码指导

Java系的后端应用大多采用Spring作为框架,而SpringMVC本身对REST也提供了一定的支持,主要是动词原语和路径提取两方面。

动词原语支持

有两种方式,一种是使用org.springframework.web.bind.annotation.RequestMapping注解,该注解中有一个属性method,该属性就是代表对动词原语的支持。还有一种直接使用固定了原语的注解,诸如org.springframework.web.bind.annotation.PostMapping,org.springframework.web.bind.annotation.GetMapping等等。

路径参数提取

//路径中使用${}进行参数包围,方法入参中使用注解@PathVariable进行定位和提取

@GetMapping(path = "/user/id/${id}")

public UserInfo queryUser(@PathVariable("id") String Id)

//删除时使用DeleteMapping映射delete动词

@DeleteMapping(path = "/user/id/${id}")

public boolean deleteUser(@PathVariable("id") String Id)

//更新时,路径中包含ID信息,Http方法体则以json形式传递参数。方法入参中使用@RequestParam可以自动完成Json的解析(Spring内置功能),并且解析为一个实例对象。json中键值对的key的名称与对象的属性名称一致时,则完成转化

@PutMapping(path = "/user/id/${id}")

public boolean updateUser(@PathVariable("id") String Id,@RequestParam UserInfo userInfo)

//新增时,使用PostMapping来映射Post方法。与Put相同,Http方法体中以json形式传递参数。

@PostMapping(path = "/user/id/${id}")

public UserInfo addUser(@PathVariable("id") String Id, @RequestParam UserInfo userInfo)


文章原创首发于公众号:林斌说Java,转载请注明来源,谢谢。

欢迎扫码关注

以上是 REST架构指导方案 的全部内容, 来源链接: utcz.com/z/510700.html

回到顶部