分布式架构设计之RestAPI
近几年,以资源为中心的表述性状态转移(Representational StateTransfer,REST)越来越受欢迎,它完美地替代了传统的基于SOAP的Web服务方案,同时它关注的是数据的处理,而后者则关注于动作行为的处理。对于REST,常有人错误的将其视为“基于URL的Web服务”,也就将REST认为是另一种类型的远程调用(Remote Procedure Call,RPC)机制。实际上,REST与RPC几乎并没有任何关系,RPC是面向服务的,关注于行为和动作;而REST是面向资源的,关注在数据的描述、状态。当然,REST中会有行为,它们是通过HTTP方法定义,也就是GET、POST、PUT、DELETE及PATCH等构成了REST的动作,它们是用来操作更新资源信息的工具。
一、什么是REST
正如上图,标准的REST是有四部分构成:资源、表述、状态转移及统一接口,它们的关系上图描述的也很清楚,REST是以资源为中心,前后端通信时,需要对通信的资源进行匹配两端合适的表述,同样,通信过程中,操作的资源的状态会发生改变转移,而上面的一切需要实际的操作接口来完成,REST支持统一的HTTP协议接口,具体各部分描述请往下细看。
1、资源中心
资源(Resource),是一个抽象概念,它可以是一段文字、一张图片、一首歌曲,也可以是数据库的一张表等。而每个资源都会有一个与之对应的URI作为识别标志,对某个资源感兴趣的一端应用,则可以通过这个URI与之进行交互,以达到需求。
2、如何表述
表述(Representation),更严格的说应该是资源的表述,它是对资源在某个特定时刻的具体描述,也是前后端通信时进行信息交换的实体,它可以有多种形式的表述。比如:文本可以是TXT,也可以是HTML、XML及JSON形式;图片可以是JPG、BMP及PNG等。而这些资源的表述格式需要在前后端通信时进行协商确定,通过请求-响应来实现。
3、状态转移
状态转移(State Transfer),指的是前后端通信过程中,客户端能够通过资源的描述,实现操作资源的目的。比如:我们在浏览器中打开一网址时,就代表了前后端产生了一个交互过程,而在这个过程中,势必会涉及到数据状态的变化。
4、统一接口
统一资源接口(Uniform Interface),是客户端操作资源数据的方式,通常是基于HTTP方式,它主要提供四种操作:GET、POST、PUT及DELETE。它们分别对应的操作方式如下:
GET:用来获取资源;
POST:用来新建资源;
PUT:用来更新资源;
DELETE:用来删除资源;
所以,不论客户端通过HTTP的哪种资源操作,不管操作的URI是什么,资源有什么不同,但操作资源的接口是统一的。
二、实例的验证
在这里,我会结合实际的例子来演示Rest API的实现方法,这里我以Spring REST为例讲解,以实现书籍的CRUD操作示例。
首先,我们需要一个控制器,并且使用了@RestController标签,它提供了很多功能,省去了传统需要@ResponseBody标签来指定返回给前端的数据格式,如下所示:
@RestController
@RequestMapping("/rest")
public class RestApiAction extends BaseAction {
// …
}
其次,我们最好定义一个BaseAction,把控制器请求的异常处理,以及返回请求头的代码封装在这里,具体原因大家应该明白,所以这里不赘述了,如下所示:
// 控制器异常处理
@ExceptionHandler(DataNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ApiError dataNotFound(DataNotFoundException e) {
long id = e.getId();
if(0 != id) {
return new ApiError(e.getCode(),"Data'Id ["+id+"] Not Found!");
} else {
return new ApiError(e.getCode(),"Data Is Not Found!");
}
}
// 返回请求头信息
public HttpHeaders genHeaders(UriComponentsBuilder ucb,long id,HttpServletRequest request) {
StringrootPath = request.getServletPath().split("/")[1];
HttpHeadersheaders = new HttpHeaders();
URI localUri = ucb
.path("/"+rootPath+"/")
.path(String.valueOf(id)).build().toUri();
headers.setLocation(localUri);
return headers;
}
说明下,控制器异常处理,主要是当前端请求接口时,后端接口返回数据为空时,直接返回NOT_FOUND异常数据给前端。而返回请求头信息,则主要是处理我们在创建一新资源并返回该资源时,一般建议在请求头HttpHeaders中将资源的URI同时返回,供前端灵活使用。
好了,下面就开始具体的业务操作了,具体如下:
1、检索所有书籍
后端:
// 检索所有书本
@RequestMapping(value="/IReadBooks",method=RequestMethod.GET,consumes="application/json")
public List<Book> readBooks() throws Exception {
List<Book>result = bookService.readBooks();
if(null == result || result.size() == 0) {
throw newDataNotFoundException();
}
return result;
}
前端:
// 检索所有书籍
function readBooks(){
$.ajax({
url:'IReadBooks',
data:null,
type:"get",
dataType:'json',
contentType:'application/json',
success:function(result){
var result = JSON.stringify(result);
$("#result").html(result);
}
});
}
结果:
说明:
接口为IReadBooks,这里并没有参数,请求协议方式为GET方式,请求内容格式为application/json,并且要求服务接口返回数据格式为json格式,返回的result结果通过JSON.stringify()转换为json格式,因为这样才能直接$("#result").html(result);
并显示下面的结果截图。另外,下面的说明部分与此类似,不再进行赘述说明。
2、检索指定书籍
后端:
// 根据书号检索一本书
@RequestMapping(value="/IReadBook/{id}",method=RequestMethod.GET,consumes="application/json")
public Book readBook(@PathVariable long id) throws Exception {
Bookresult = bookService.readBook(id);
if(null == result) {
throw newDataNotFoundException(id,10001);
}
return result;
}
这里使用了@PathVariable注解,来接收前端传递的书本号,并将其添加到资源URI中,实现接口查询功能。
前端:
// 根据书号检索一本书
function readBook(){
$.ajax({
url:'IReadBook/1',
data:null,
type:"get",
dataType:'json',
contentType:'application/json',
success:function(result){
var result = JSON.stringify(result);
$("#result").html(result);
}
});
}
结果:
3、上架一本书籍
后端:
// 上架一本书
@RequestMapping(value="/ICreateBook",method=RequestMethod.POST,produces="application/json")
public ResponseEntity<Book> createBook(@RequestBody Book book,UriComponentsBuilder ucb,HttpServletRequest request) throws Exception {
Bookresult = bookService.createBook(book);
if(null == result) {
throw newDataNotFoundException();
}
return newResponseEntity<Book>(book,genHeaders(ucb,book.getId(),request),HttpStatus.CREATED);
}
这里使用了@RequestBody注解,它的作用就是将前端传递过来的json内容通过消息转换器转变为对应的POJO实体,并调用DAO插入到数据库表中。如果刚插入的书本失败了,则返回的Book结果就为空,那么直接返回NOT_FOUND信息数据给前端处理。
前端:
// 上架一本书
function createBook(){
var jsonStr = "{\"name\":\"《Python进阶实战教材》\",\"tag\":\"编程语言\",\"price\":68.59}";
$.ajax({
url:'ICreateBook',
data:jsonStr,
type:"post",
dataType:'json',
contentType:'application/json',
success:function(result){
var result = JSON.stringify(result);
$("#result").html(result);
}
});
}
结果:
4、更新一本书籍
后端:
// 更新一本书
@RequestMapping(value="/IUpdateBook",method=RequestMethod.PUT,consumes="application/json")
public Book updateBook(@RequestBody Book book) throws Exception {
Bookresult = bookService.updateBook(book);
if(null == result) {
throw newDataNotFoundException();
}
return result;
}
前端:
// 更新一本书
function updateBook(){
var jsonStr = "{\"id\":11,\"name\":\"《Web进阶实战教材》\",\"tag\":\"编程语言\",\"price\":68.59}";
$.ajax({
url:'IUpdateBook',
data:jsonStr,
type:"put",
dataType:'json',
contentType:'application/json',
success:function(result){
var result = JSON.stringify(result);
$("#result").html(result);
}
});
}
结果:
5、下架一本书籍
后端:
// 下架一本书
@RequestMapping(value="/IDeleteBook",method=RequestMethod.DELETE,produces="application/json")
public Book deleteBook(@RequestBody Book book) throws Exception {
Bookresult = bookService.deleteBook(book.getId());
if(null == result) {
throw newDataNotFoundException();
}
return result;
}
前端:
// 下架一本书
function deleteBook(){
var jsonStr = "{\"id\":11,\"name\":\"《WEB进阶实战教材》\",\"tag\":\"编程语言\",\"price\":68.59";
$.ajax({
url:'IDeleteBook',
data:jsonStr,
type:"delete",
dataType:'json',
contentType:'application/json',
success:function(result){
var result = JSON.stringify(result);
$("#result").html(result);
}
});
}
结果:
三、REST优与劣
1、优势
借助于资源表述、状态转移及统一接口,REST能够将客户端的请求、服务端的响应基于资源形式联系在一起,形成了一种以资源为中心,以HTTP为操作方式的,语言无关、平台无关的通信方式。另外,因为HTTP本身无状态性,能够有效保持服务或应用的无状态性,利于水平拓展。
2、不足
随着业务不断增长,服务端响应的内容逐渐复杂,对于如何使资源标准结构化,以及如何处理资源的相关连接等问题,应该引起注意。当然,在后续的文章会继续介绍如何来弥补这些不足。
好了,由于作者水平有限,如有不正确或是误导的地方,请不吝指出讨论(技术交流群:497552060(新))