分佈式架構設計之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(新))

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章