知識庫 / REST RSS 訂閱

利用資源類型和特性的優勢,消除RAML中的冗餘

REST
HongKong
4
03:59 AM · Dec 06 ,2025
<div>
 <a class="article-series-header" href="javascript:void(0);">該文章是系列的一部分</a>
 <div>
  <div>
   • 介紹 RAML – RESTful API 建模語言
   <br>
   <div>
    • 使用資源類型和特性的 RAML 消除冗餘(當前文章)
   </div>
   • 使用包含、庫、覆蓋和擴展進行模塊化 RAML
   <br>
   • 使用註解定義自定義 RAML 屬性
   <br>
  </div>
  <!-- end of article series inner -->
 </div>
<!-- .article-series-links -->
</div >
<!-- end of article-series section -->

1. 概述

在我們的 RAML 教程文章中,我們介紹了 RESTful API 建模語言 並基於一個名為 Foo 的單一實體創建了一個簡單的 API 定義。現在,想象一個真實世界的 API,其中您擁有多個實體類型的資源,所有這些資源都具有相同的或相似的 GET、POST、PUT 和 DELETE 操作。您可以看到您的 API 文檔會迅速變得冗長且重複。

在本文中,我們將展示如何使用 RAML 中的 資源類型特性 功能來 消除資源和方法定義中的冗餘,通過提取和參數化常見部分,從而消除複製粘貼錯誤,同時使您的 API 定義更加簡潔。

2. 我們的API

為了展示資源類型特性的優勢,我們將擴展我們原始的API,添加一個名為Bar的第二個實體類型的資源。以下是構成我們修訂後的API的資源:

  • GET /api/v1/foos
  • POST /api/v1/foos
  • GET /api/v1/foos/{fooId}
  • PUT /api/v1/foos/{fooId}
  • DELETE /api/v1/foos/{fooId}
  • GET /api/v1/foos/name/{name}
  • GET /api/v1/foos?name={name}&ownerName={ownerName}
  • GET /api/v1/bars
  • POST /api/v1/bars
  • GET /api/v1/bars/{barId}
  • PUT /api/v1/bars/{barId}
  • DELETE /api/v1/bars/{barId}
  • GET /api/v1/bars/fooId/{fooId}

3. 識別模式

隨着我們瀏覽 API 中提供的資源列表,我們開始觀察到一些模式的出現。例如,對於創建、讀取、更新和刪除單個實體,存在一種模式;對於檢索實體集合,也存在一種模式。集合和集合項模式是 RAML 定義中提取 資源類型 的一種常用模式。

讓我們來看 API 的幾個部分:

[注意:在下面的代碼片段中,僅包含三個點 (…) 的行表示為了簡潔,省略了部分行。]

/foos:
  get:
    description: |
      List all foos matching query criteria, if provided;
      otherwise list all foos
    queryParameters:
      name?: string
      ownerName?: string
    responses:
      200:
        body:
          application/json:
            type: Foo[]
  post:
    description: Create a new foo
    body:
      application/json:
        type: Foo
    responses:
      201:
        body:
          application/json:
            type: Foo
...
/bars:
  get:
    description: |
      List all bars matching query criteria, if provided;
      otherwise list all bars
    queryParameters:
      name?: string
      ownerName?: string
    responses:
      200:
        body:
          application/json:
            type: Bar[]
  post:
    description: Create a new bar
    body:
      application/json:
        type: Bar
    responses:
      201:
        body:
          application/json:
            type: Bar

當我們比較 /foos/bars 資源的 RAML 定義,包括使用的 HTTP 方法,我們可以看到各個屬性中存在冗餘,並且再次觀察到模式開始出現。

無論在資源或方法定義中是否存在模式,都有機會使用 RAML 的 資源類型特性

4. 資源類型

為了實現 API 中發現的模式,資源類型 使用被保留和用户自定義的參數,這些參數被雙角括號 (<< 和 >>) 包圍。

4.1. 預留參數

以下兩種預留參數可用於資源類型定義:

  • <<resourcePath>> 代表整個 URI(遵循 baseURI),
  • <<resourcePathName>> 代表 URI 中右側最右邊的正斜槓(/)之後的部分,忽略花括號 { }。

當在資源定義中進行處理時,它們的取值將根據所定義的資源進行計算。

例如,對於資源 /foos<<resourcePath>> 將評估為 “/foos” 且 <<resourcePathName>> 將評估為 “foos”。

對於資源 /foos/{fooId}<<resourcePath>> 將評估為 “/foos/{fooId}” 且 <<resourcePathName>> 將評估為 “foos”。

4.2. 用户自定義參數

一個 <em resource type</em> 定義也可能包含用户自定義參數。與保留參數不同,其值是根據定義的資源動態確定,而用户自定義參數必須在 <em resource type</em> 包含它們的地方賦值,並且這些值不會改變。

用户自定義參數可以在 <em resource type</em> 定義的開頭聲明,儘管這樣做並非必需,也不常見,因為讀者通常可以根據它們的名稱和它們被使用的上下文來推斷其預期用途。

4.3 參數函數

有若干有用的文本函數可供使用,用於在參數被在資源定義中處理時,轉換擴展後的參數值。

以下是用於參數轉換的函數列表:

  • !singularize
  • !pluralize
  • !uppercase
  • !lowercase
  • !uppercamelcase
  • !lowercamelcase
  • !upperunderscorecase
  • !lowerunderscorecase
  • !upperhyphencase
  • !lowerhyphencase

使用以下構造來應用函數:

<<parameterName | !functionName>>

如果需要使用多個函數來達到所需的轉換效果,則使用管道符號 (“|”) 分隔每個函數名稱,並在每個使用到的函數前添加感嘆號 (!)。

例如,給定資源 /foos,其中 <<resourcePathName>> 評估為 “foos”:

  • <<resourcePathName | !singularize>> ==> “foo”
  • <<resourcePathName | !uppercase>> ==> “FOOS”
  • <<resourcePathName | !singularize | !uppercase>> ==> “FOO”

給定資源 /bars/{barId},其中 <<resourcePathName>> 評估為 “bars”:

  • <<resourcePathName | !uppercase>> ==> “BARS”
  • <<resourcePathName | !uppercamelcase>> ==> “Bar”

5. 從集合中提取資源類型

讓我們重構上述所示的 /foos/bars 資源定義,使用 資源類型 以捕獲常見的屬性。我們將使用保留參數 <<resourcePathName>> 和用户定義的參數 <<typeName>> 來表示所使用的數據類型。

5.1. 定義

以下是一個 資源類型 的定義,代表一組項目:

resourceTypes:
  collection:
    usage: Use this resourceType to represent any collection of items
    description: A collection of <<resourcePathName>>
    get:
      description: Get all <<resourcePathName>>, optionally filtered 
      responses:
        200:
          body:
            application/json:
              type: <<typeName>>[]
    post:
      description: Create a new <<resourcePathName|!singularize>> 
      responses:
        201:
          body:
            application/json:
              type: <<typeName>>

請注意,在我們的API中,由於我們的數據類型僅為基本資源的資本化單數形式,我們本可以對<<resourcePathName>>參數應用函數,從而實現與引入用户定義的<<typeName>>參數相同的結果,針對此API的部分功能:

resourceTypes:
  collection:
  ...
    get:
      ...
            type: <<resourcePathName|!singularize|!uppercamelcase>>[]
    post:
      ...
            type: <<resourcePathName|!singularize|!uppercamelcase>>

5.2. 應用

利用上述定義,其中包含 <typeName> 參數,以下是如何將“collection”資源類型應用於 /foos/bars 資源:

/foos:
  type: { collection: { "typeName": "Foo" } }
  get:
    queryParameters:
      name?: string
      ownerName?: string
...
/bars:
  type: { collection: { "typeName": "Bar" } }

請注意,我們仍然能夠整合兩個資源之間的差異——在本例中,queryParameters 部分——同時又能充分利用 資源類型 定義所提供的所有優勢。

6. 從集合中提取單個項目的資源類型

現在,我們將重點關注我們 API 中處理集合中單個項目的部分:/foos/{fooId}/bars/{barId} 資源。以下是 /foos/{fooId} 的代碼:

/foos:
...
  /{fooId}:
    get:
      description: Get a Foo
      responses:
        200:
          body:
            application/json:
              type: Foo
        404:
          body:
            application/json:
              type: Error
              example: !include examples/Error.json
    put:
      description: Update a Foo
      body:
        application/json:
          type: Foo
      responses:
        200:
          body:
            application/json:
              type: Foo
        404:
          body:
            application/json:
              type: Error
              example: !include examples/Error.json
    delete:
      description: Delete a Foo
      responses:
        204:
        404:
          body:
            application/json:
              type: Error
              example: !include examples/Error.json

/bars/{barId}/ 資源定義也具有 GET、PUT 和 DELETE 方法,與 /foos/{fooId} 資源定義相同,除了“foo”和“bar”字符串及其對應的複數形式和/或大寫形式。

6.1. 定義

提取我們剛剛識別出的模式,以下是如何為集合中的單個項目定義一個 資源類型

resourceTypes:
...
  item:
    usage: Use this resourceType to represent any single item
    description: A single <<typeName>>
    get:
      description: Get a <<typeName>>
      responses:
        200:
          body:
            application/json:
              type: <<typeName>>
        404:
          body:
            application/json:
              type: Error
              example: !include examples/Error.json
    put:
      description: Update a <<typeName>>
      body:
        application/json:
          type: <<typeName>>
      responses:
        200:
          body:
            application/json:
              type: <<typeName>>
        404:
          body:
            application/json:
              type: Error
              example: !include examples/Error.json
    delete:
      description: Delete a <<typeName>>
      responses:
        204:
        404:
          body:
            application/json:
              type: Error
              example: !include examples/Error.json

6.2. 應用

以下是如何應用“item”資源類型:

And here is how we apply the “item” resource type:

/foos:
...
  /{fooId}:
    type: { item: { "typeName": "Foo" } }
... 
/bars: 
... 
  /{barId}: 
    type: { item: { "typeName": "Bar" } }

7. 特性

資源類型 用於從資源定義中提取模式,特性 用於從跨資源中常見的元方法定義中提取模式。

7.1. 參數

除了 <em resourcePath</em>><em resourcePathName</em>>, trait 定義中還提供了一個額外的預留參數:<em methodName</em>> 會評估為該 <em trait> 被定義的 HTTP 方法(GET、POST、PUT、DELETE 等)。 用户定義的參數也可以在 trait 定義中出現,並且在應用時,它們會取用所在資源的價值。

7.2. 定義

請注意,“item”的資源類型仍然存在冗餘。讓我們看看特質如何幫助消除它們。我們首先將提取一個特質,用於任何包含請求正文的方法:

traits:
  hasRequestItem:
    body:
      application/json:
        type: <<typeName>>

現在我們提取那些正常響應包含身體的方法中的traits

  hasResponseItem:
    responses:
      200:
        body:
          application/json:
            type: <<typeName>>
  hasResponseCollection:
    responses:
      200:
        body:
          application/json:
            type: <<typeName>>[]

最後,這是一個適用於任何可能返回 404 錯誤響應的方法的trait

  hasNotFound:
    responses:
      404:
        body:
          application/json:
            type: Error
            example: !include examples/Error.json

7.3. 應用

我們隨後將該 特性 應用到我們的 資源類型

resourceTypes:
  collection:
    usage: Use this resourceType to represent any collection of items
    description: A collection of <<resourcePathName|!uppercamelcase>>
    get:
      description: |
        Get all <<resourcePathName|!uppercamelcase>>,
        optionally filtered
      is: [ hasResponseCollection: { typeName: <<typeName>> } ]
    post:
      description: Create a new <<resourcePathName|!singularize>>
      is: [ hasRequestItem: { typeName: <<typeName>> } ]
  item:
    usage: Use this resourceType to represent any single item
    description: A single <<typeName>>
    get:
      description: Get a <<typeName>>
      is: [ hasResponseItem: { typeName: <<typeName>> }, hasNotFound ]
    put:
      description: Update a <<typeName>>
      is: | [ hasRequestItem: { typeName: <<typeName>> }, hasResponseItem: { typeName: <<typeName>> }, hasNotFound ]
    delete:
      description: Delete a <<typeName>>
      is: [ hasNotFound ]
      responses:
        204:

我們還可以將 特性 應用於資源內定義的類方法。這在“一次性”場景中尤其有用,即資源方法組合與一個或多個 特性 匹配,但與任何已定義的 資源類型 不匹配:

/foos:
...
  /name/{name}:
    get:
      description: List all foos with a certain name
      is: [ hasResponseCollection: { typeName: Foo } ]

8. 結論

在本教程中,我們展示瞭如何顯著減少甚至消除 RAML API 定義中的冗餘內容。

首先,我們識別了資源中的冗餘部分,識別了它們的模式,並提取了 資源類型。然後,我們對跨資源中使用的常見方法也進行了相同的操作,以提取 特性。然後,我們通過將 特性 應用於我們的 資源類型 以及到“一次性”資源-方法組合(這些組合嚴格不匹配我們定義的 資源類型)進一步減少了冗餘。

以下是我們的最終 RAML API 的完整內容:

#%RAML 1.0
title: Baeldung Foo REST Services API
version: v1
protocols: [ HTTPS ]
baseUri: http://rest-api.baeldung.com/api/{version}
mediaType: application/json
securedBy: basicAuth
securitySchemes:
  basicAuth:
    description: |
      Each request must contain the headers necessary for
      basic authentication
    type: Basic Authentication
    describedBy:
      headers:
        Authorization:
          description: |
            Used to send the Base64 encoded "username:password"
            credentials
            type: string
      responses:
        401:
          description: |
            Unauthorized. Either the provided username and password
            combination is invalid, or the user is not allowed to
            access the content provided by the requested URL.
types:
  Foo:   !include types/Foo.raml
  Bar:   !include types/Bar.raml
  Error: !include types/Error.raml
resourceTypes:
  collection:
    usage: Use this resourceType to represent a collection of items
    description: A collection of <<resourcePathName|!uppercamelcase>>
    get:
      description: |
        Get all <<resourcePathName|!uppercamelcase>>,
        optionally filtered
      is: [ hasResponseCollection: { typeName: <<typeName>> } ]
    post:
      description: |
        Create a new <<resourcePathName|!uppercamelcase|!singularize>>
      is: [ hasRequestItem: { typeName: <<typeName>> } ]
  item:
    usage: Use this resourceType to represent any single item
    description: A single <<typeName>>
    get:
      description: Get a <<typeName>>
      is: [ hasResponseItem: { typeName: <<typeName>> }, hasNotFound ]
    put:
      description: Update a <<typeName>>
      is: [ hasRequestItem: { typeName: <<typeName>> }, hasResponseItem: { typeName: <<typeName>> }, hasNotFound ]
    delete:
      description: Delete a <<typeName>>
      is: [ hasNotFound ]
      responses:
        204:
traits:
  hasRequestItem:
    body:
      application/json:
        type: <<typeName>>
  hasResponseItem:
    responses:
      200:
        body:
          application/json:
            type: <<typeName>>
  hasResponseCollection:
    responses:
      200:
        body:
          application/json:
            type: <<typeName>>[]
  hasNotFound:
    responses:
      404:
        body:
          application/json:
            type: Error
            example: !include examples/Error.json
/foos:
  type: { collection: { typeName: Foo } }
  get:
    queryParameters:
      name?: string
      ownerName?: string
  /{fooId}:
    type: { item: { typeName: Foo } }
  /name/{name}:
    get:
      description: List all foos with a certain name
      is: [ hasResponseCollection: { typeName: Foo } ]
/bars:
  type: { collection: { typeName: Bar } }
  /{barId}:
    type: { item: { typeName: Bar } }
  /fooId/{fooId}:
    get:
      description: Get all bars for the matching fooId
      is: [ hasResponseCollection: { typeName: Bar } ]
<div>
  <div>
    <div>
      下一頁 <strong»</strong>
    </div>
    模塊化 RAML 使用包含、庫、覆蓋和擴展
    </div>
    <div>
      <div>
        <strong»</strong> 上一頁
      </div>
      介紹 RAML – RESTful API 建模語言
    </div>
  </div>
</div>
user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.