分布式架构设计之Rest API HAL

分布式架构设计之RestAPI HAL

 

在上一篇文章《分布式架构设计之Rest API》中,我对什么是Rest进行了详细的介绍,同时以书本的CRUD为例,实现了Rest API的基本操作。但是细心的读者可能会发现,所有接口返回的数据格式并不统一,没有一定的规范性,更重要的是接口通信的内容并未很明确地体现“资源”的概念,所以在这篇文章就来介绍下现在流行的HAL风格的数据格式,需要提及的是:HAL(Hypertext Application Language)是一种API数据格式风格,同时也能规范接口通信内容的格式,降低客户端与服务器端接口API的耦合度。

 

l  什么是HAL

l  实例的验证

 

 

一、什么是HAL

 

上面的拼图并不一个创新,是直接从HAL的作者Mike kelly的官方网站(http://stateless.co/hal_specification.html)上复制的一份,它包含三个标准部分:

 

1、状态(State)

指的是资源本身的固有属性,比如:书本的资源表述如下:

{

  …

   "id": 1,

   "name": "《Web进阶实战教材》",

   "tag": "编程语言",

   "price": 68.59

  …

}

 

2、链接(Links)

链接定义了与当前资源相关的一组资源的集合,比如:

"_links":{

        "self": {

            "href": "…"

        }

}

 

正如上所示,链接包含了三部分组成:链接名称、目标地址及访问的地址参数。

 

3、子资源(Embedded Resource)

指的是描述在当前资源的内部,其嵌套资源的定义。比如:

"_embedded":{

        "books": [

            {

                "id": 1,

                "name": "《Web进阶实战教材》",

                "tag": "编程语言",

                "price": 68.59,

                "updateTime": 1502556100000,

                "createTime":1502556100000,

                "_links": {

                    "ex:items": {

                        "href":"…"

                    },

                    "self": {

                        "href": "…"

                    },

                    "curies": [

                        {

                            "href":"…",

                            "name":"ex",

                           "templated": true

                        }

                    ]

                }

            },

            {

                "id": 2,

                "name": "《Ruby进阶实战教材》",

                "tag": "编程语言",

                "price": 68.59,

                "updateTime":1502691808000,

                "createTime": 1502691808000,

                "_links": {

                    "ex:items": {

                        "href":"http://localhost:8080/cwteam/2"

                    },

                    "self": {

                        "href":"http://localhost:8080/cwteam/books/2"

                    },

                    "curies": [

                        {

                            "href":"http://localhost:8080/cwteam/books/{rel}",

                            "name":"ex",

                           "templated": true

                        }

                    ]

                }

            }

        ]

    }

 

 

另外,HAL规范是围绕资源和链接两个概念展开的。资源的表达包含链接、嵌套的资源和状态。资源的状态一般指的是资源本身所包含的数据,链接则包含其指向的目标地址(URI),所表达的关系和其它可选的相关属性。正如上面所示json格式,资源的链接包含在_links属性对应的键值对中,而其中的键(key)是链接的关系,而值(value)则是另一个包含href等其它链接属性的对象或对象数组。当前所包含的资源,则由_embedded属性表示,其值是包含了其它资源的哈希对象或对象数组。

 

使用 URL 作为链接的关系带来的问题是 URL 作为属性名称来说显得过长,而且不同关系的 URL 的大部分内容是重复的。为了解决这个问题,可以使用 Curie,而Curie 可以作为链接关系 URL 的模板。链接的关系声明时使用 Curie 的名称作为前缀,不用提供完整的 URL。应用中声明的 Curie 出现在_links 属性中。代码中定义了 URI 模板为“http://localhost:8080/exlist/rels/{rel}”的名为 ex Curie。在使用了 Curie 之后,名为 items 的链接关系变成了包含前缀的“ex:items”的形式。这就表示该链接的关系实际上是“http://localhost:8080/exlist/rels/items”

 

 

二、实例的验证

我们以上一篇文章的例子为基础,对其实现HAL的规范化。可在此之前,有必要介绍下Spring的子项目HATEOAS对HAL的支持,也是Rest风格中最复杂的约束,现阶段,Spring  HATEOAS仅支持一种超媒体表达格式,同时我们也只需要在应用的配置类上使用

@EnableHypermediaSupport(type=EnableHypermediaSupport.HypermediaType.HAL)这个注解,就可以启用对超媒体类型的支持,如下所示:

@Configuration

@EnableWebMvc

@EnableHypermediaSupport(type=EnableHypermediaSupport.HypermediaType.HAL)

public class ServletConfigextends WebMvcConfigurerAdapter {

    …

}

 

在启用了该支持后,服务端输出的表达格式就会遵循HAL规范。当然,启用了超媒体支持后,会默认启用@EnableEntityLinks功能(下面介绍),同时应用还需要相关的定制表达,使HAL的表达更加友好,那么就要如下操作:

 

首先,内嵌在_embedded中的内容,是由RelProvider接口实现提供,对应我们的应用而言,只需要在内嵌资源对应的模型中添加Relation注解即可,如下所示:

@Relation(value="book", collectionRelation="books")

public class Book extends BaseModel {

    …

}

需要注意的是,当内嵌资源使用Book作为模型时,单个资源则使用book作为属性,而多个资源则使用books作为属性。

 

另外,如果需要添加Curie,那么需要提供CurieProvider接口实现。这里我们使用已有的DefaultCurieProvider类并提供Curie的前缀和URI模版,具体如下:

@Bean

public CurieProvider curieProvider() {

    return newDefaultCurieProvider("ex",

     new UriTemplate("http://localhost:8080/cwteam/books/{rel}"));

}

 

好了,有了上面的准备工作之后,我们就可以进入HAL主题实现了,具体技术实现如下所示:

 

1、Maven依赖

<dependency>

       <groupId>org.springframework.hateoas</groupId>

       <artifactId>spring-hateoas</artifactId>

       <version>0.23.RELEASE</version>

    </dependency>

 

建议使用最新版本。

 

2、基础模型

public class BaseModel implements Identifiable<Long>  {

    private Long id;

   

    public Long getId() {

       return id;

    }

   

    public booleanequals(Object o) {

       if (this == o) return true;

       if (o == null || getClass() != o.getClass()) return false;

 

       BaseModel that = (BaseModel) o;

 

       if (id != null ? !id.equals(that.id) : that.id != null) return false;

 

       return true;

    }

 

    public int hashCode() {

       return id != null ? id.hashCode() : 0;

    }

}

 

该类为所有资源模型类的父级,实现了资源标志接口Identifiable<Long> 。

 

3、书本模型

@Relation(value="book", collectionRelation="books")

public class Book extends BaseModel {

    private long id;

    private String name;

    private String tag;

    private double price;

    private Timestamp updateTime;

    private TimestampcreateTime;

    …

}

 

如果内嵌资源为单个,则返回键属性为book,如果内嵌资源为多个,那么返回键属性为books。

 

4、链接信息

为了把模型对象转为满足HATEOAS约束的资源,那么就需要添加链接信息。在Spring HATEOAS中,org.springframework.hateoas.Link类是用来表示和生成链接的,该类也遵循着Atom规范中对应链接的定义,包括rel和href两个属性。属性rel表示链接关系,href则表示链接指向的资源标志符,一般为URI。

在创建资源时,需要继承Spring HATEOAS提供的org.springframwork.hateoas.Resource类,该类提供了简单的方式创建资源链接。如下即为书本模型类对应的资源类BookResource的实现方式:

public class BookResource extends Resource {

    public BookResource(BaseModel list) {

        super(list);

       Long listId = list.getId();

       add(linkTo(getClass()).slash(listId).withRel("items"));

    }

}

 

该类主要的工作是构建资源对象,并为每个资源创建一个附加非self的资源链接。

 

5、组装资源

一般我们需要将模型类对象转换为对应的资源对象,比如:把Book类对象转换为BookResource类对象。一般的做法就是new BookResource(books)方式来转换。我们也可以(推荐)使用SpringHATEOAS提供的资源组装器把转换逻辑封装起来。该组装起可以自动创建rel属性和href链接,具体如下:

public classBookResourceAssembler extends   ResourceAssemblerSupport<BaseModel,BookResource> {

 

    public BookResourceAssembler(Class<?> sourceClass) {

       super(sourceClass,BookResource.class);

    }

 

    public BookResource toResource(BaseModel entity) {

       BookResourceresource = createResourceWithId(entity.getId(),entity);

       return resource;

    }

 

    protected BookResource instantiateResource(BaseModel entity) {

       return new BookResource(entity);

    }

}

 

创建此类时,需要指定使用资源的控制器(Class<?> sourceClass),以用来确定生成链接的地址信息。

ResourceAssemblerSupport 类的默认实现是通过反射来创建资源对象的。toResource 方法用来完成实际的转换。此处使用了 ResourceAssemblerSupport类的 createResourceWithId 方法来创建一个包含 self 链接的资源对象。

BookResourceAssembler 类的 instantiateResource 方法用来根据一个模型类 Book 的对象创建出 BookResource对象。

 

需要注意的是:

单个资源转换:

newBookResourceAssembler(getClass()).toResource(entity);

 

多个资源转换:

newResources<>(newBookResourceAssembler(getClass()).toResources(entities), link);

 

6、模型类创建链接

上面介绍的是通过 Spring MVC 控制器来创建链接,另外一种做法是从模型类中创建。这是因为控制器通常用来暴露某个模型类。如RestApiAction类直接暴露模型类Book,并提供了访问 Book 资源集合和单个 Book 资源的接口。对于这样的情况,并不需要通过控制器来创建相关的链接,而可以使用 EntityLinks。首先需要在控制器类中通过“@ExposesResourceFor”注解声明其所暴露的模型类,如下所示:

@RestController

@RequestMapping("/books")

@ExposesResourceFor(BaseModel.class)

public class RestApiAction extends BaseAction {

    …

}

 

另外在 Spring 用的配置类中需要通“@EnableEntityLinks”注解启用 EntityLinks 功能,如果上面有启用了超媒体支持,那么该注解自动启用。而此EntityLinks功能依赖spring-plugin-core组建包,maven依赖如下:

<dependency>

       <groupId>org.springframework.plugin</groupId>

       <artifactId>spring-plugin-core</artifactId>

       <version>1.1.0.RELEASE</version>

    </dependency>

 

那么如何使用EntityLinks?如下所示:

@Autowired

EntityLinks entityLinks;

entityLinks.linkForSingleResource(BaseModel.class, entity);

 

需要注意的是 linkForSingleResource 方法可以正常工作控制器类中需要包含访问单源的方法而且其“@RequestMapping”是类似“/{id}”样的形式

 

 

有了上面的准备之后,我们来看看书本Book的CRUD有何改进:

1、BaseAction类添加了构建单个或多个资源的方法:

// 构建单个资源对象

    public BookResource genResultListByCode(BaseModel entity) {

       return newBookResourceAssembler(getClass()).toResource(entity);

    }

   

    // 构建多个资源对象

    public Resources<BookResource>genResultList(List<BaseModel> entities) {

       Linklink = linkTo(getClass()).withSelfRel();

       return newResources<>(newBookResourceAssembler(getClass()).toResources(entities), link);

    }

 

同时改进了请求头的返回处理:

    @Autowired

    EntityLinks entityLinks;

 

// 返回请求头的信息

    public HttpHeaders genHeaders(BaseModel entity) {

       HttpHeadersheaders = new HttpHeaders();

       headers.setLocation(entityLinks.linkForSingleResource(BaseModel.class, entity).toUri());

       return headers;

    }

 

2、检索所有书籍

后端:

// 检索所有书本

    @RequestMapping(method=RequestMethod.GET,produces="application/hal+json")

    public Resources<BookResource>readBooksByHal() throws Exception {

       List<BaseModel>result = bookService.readBooks();

       if(null == result || result.size() == 0) {

           throw newDataNotFoundException();

       }

       return genResultList(result);

    }

 

 

前端:

// 检索所有书籍

           function readBooks(){

              $.ajax({

                      url:'/cwteam/books',

                      data:null,

                      type:"get",

                      dataType:'json',

                      contentType:'application/hal+json',

                      success:function(result){

                         var result = JSON.stringify(result);

                         $("#result").html(result);

                      }

              });

           }

 

需要注意的是ajax请求的contentType必须与服务接口相同,并且必须同时为application/hal+json时,才能支持HAL格式结果返回。同理,除HTTP请求头返回的信息外,json/xml格式的数据都需要配置内容类型为application/hal+json,否则非HAL风格格式。

 

 

请求结果:

 

 

3、检索指定书籍

后端:

// 根据书号检索一本书

    @RequestMapping(value="/{id}",method=RequestMethod.GET,produces="application/hal+json")

    public BookResource books(@PathVariable long id) throws Exception {

       Bookresult = bookService.readBook(id);

       if(null == result) {

           throw new DataNotFoundException(id,10001);

       }

       return genResultListByCode(result);

    }

 

 

前端:

// 根据书号检索一本书

           function readBook(){

              $.ajax({

                      url:'/cwteam/books/1',

                      data:null,

                      type:"get",

                      dataType:'json',

                      contentType:'application/hal+json',

                      success:function(result){

                         var result = JSON.stringify(result);

                         $("#result").html(result);

                      }

              });

           }

 

 

请求结果:

 

 

4、上架一本书

后端:

// 上架一本书

   @RequestMapping(method=RequestMethod.POST,produces="application/json")

    public ResponseEntity<?> createBook(@RequestBody Book book) throws Exception {

       Bookresult = bookService.createBook(book);

       if(null == result) {

           throw new DataNotFoundException();

       }

       return newResponseEntity<Object>(genHeaders(result),HttpStatus.CREATED);

    }

 

前端:

// 上架一本书

           function createBook(){

              var jsonStr = "{\"id\":3,\"name\":\"Ruby进阶实战教材》\",\"tag\":\"编程语言\",\"price\":68.59}";

              $.ajax({

                      url:'/cwteam/books',

                      data:jsonStr,

                      type:"post",

                      dataType:'json',

                      contentType:'application/json',

                      success:function(result){

                         var result = JSON.stringify(result);

                         $("#result").html(result);

                      }

              });

           }

 

请求结果:

 

 

5、更新一本书

后端:

// 更新一本书

    @RequestMapping(value="/{id}",method=RequestMethod.PUT,produces="application/json")

    public BookResource updateBook(@PathVariable long id,@RequestBody Book book) throws Exception {

       Bookresult = bookService.updateBook(book);

       if(null == result) {

           throw newDataNotFoundException();

       }

       return genResultListByCode(result);

    }

 

 

前端:

// 更新一本书

         function updateBook(){

            var jsonStr = "{\"id\":1,\"name\":\"Web进阶实战教材》\",\"tag\":\"编程语言\",\"price\":68.59}";

            $.ajax({

                   url:'/cwteam/books/1',

                   data:jsonStr,

                   type:"put",

                   dataType:'json',

                   contentType:'application/json',

                   success:function(result){

                      var result = JSON.stringify(result);

                      $("#result").html(result);

                   }

            });

         }

 

请求结果:

 

 

6、下架一本书

后端:

// 下架一本书

    @RequestMapping(value="/{id}",method=RequestMethod.DELETE,produces="application/json")

    public ResponseEntity<?> deleteBook(@PathVariable long id) throws Exception {

       int rows = bookService.deleteBook(id);

       if(rows <= 0) {

           throw newDataNotFoundException();

       }

       return newResponseEntity<Object>(HttpStatus.NO_CONTENT);

    }

 

 

前端:

// 下架一本书

           function deleteBook(){

              var jsonStr = "{\"id\":3,\"name\":\"WEB进阶实战教材》\",\"tag\":\"编程语言\",\"price\":68.59}";

              $.ajax({

                      url:'/cwteam/books/3',

                      data:jsonStr,

                      type:"delete",

                      dataType:'json',

                      contentType:'application/json',

                      success:function(result){

                         var result = JSON.stringify(result);

                         $("#result").html(result);

                      }

              });

           }

 

请求结果:

 

 

 

 

 

 

 

 

 

好了,由于作者水平有限,如有不正确或是误导的地方,请不吝指出讨论(技术交流群:497552060(新))

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云水之路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值