知識庫 / REST RSS 訂閱

Smithy 介紹

REST
HongKong
8
03:33 AM · Dec 06 ,2025

1. 簡介

Smithy 是一種描述我們 API 的方法,以及一組配套工具,這些工具能夠幫助我們從該定義中生成 API 客户端和服務器。它允許我們描述 API,然後從我們的定義中生成客户端和服務器代碼。

在本教程中,我們將快速瞭解 Smithy IDL(接口定義語言)及其相關工具。

2. Smithy 是什麼?

Smithy 是一種接口定義語言,允許我們以一種語言和協議無關的格式描述我們的 API。

亞馬遜最初為描述 AWS API 而設計 Smithy,並將其公開發布,以便其他服務可以從中受益。

我們可以使用提供的工具自動從這些 API 定義中生成服務器和客户端 SDK 代碼。 這使得 Smithy 文件成為我們 API 工作方式的權威來源,確保所有客户端和服務器代碼與該文件正確匹配。

Smithy 旨在定義基於資源的 API。 這種 API 的標準用法是 JSON 上的 HTTP,但我們還可以使用其他序列化格式,例如 XML,甚至使用非 HTTP 傳輸,例如 MQTT。

Smithy 可以被認為是像 OpenAPI 和 RAML 這樣的其他標準概念上相似的。 但是,Smithy 對 API 的工作方式採取了更具主觀性的方法——期望它們是基於資源的。 同時,Smithy 不會規定傳輸或序列化方式,從而提供了更大的靈活性。

3. Smithy 文件

我們通過編寫 .smithy 文件來定義我們的 API。這些文件使用 Smithy IDL 格式 定義我們的 API,包括資源、我們對資源執行的操作以及代表整個 API 界限的服務。

我們的 Smithy 文件首先定義我們使用的 Smithy IDL 格式的版本以及 API 將存在的命名空間:

$version: "2"
// The namespace to use
namespace com.baeldung.smithy.books

我們在此文件中定義了資源、操作和服務,以及用於操作所需的其他必要元素,如數據結構。

3.1. 資源

首先我們需要定義我們的資源。這些代表我們將要處理的數據。它們使用 resource 關鍵字後跟資源的名稱進行定義。在資源中,我們定義如何識別資源並概述其關聯的屬性。稍後,我們還將添加可以對資源執行的操作。

例如,我們可以定義一個代表書籍的資源:

/// Represents a book in the bookstore
resource Book {
    identifiers: { bookId: BookId }
    properties: {
        title: String
        author: String
        isbn: String
        publishedYear: Integer
    }
}

@pattern("^[a-zA-Z0-9-_]+$")
string BookId

這裏有一個 Book 資源。它使用 id 字段進行唯一標識,該字段的類型為 BookId – 我們已將其定義為與給定格式匹配的 String。 我們的 Book  此外還具有 title、 author、 isbn 和 publishedYear 這些屬性。 因此,這是一個資源示例,格式為 JSON:

{
    "bookId": "abc123",
    "title": "Head First Java, 3rd Edition: A Brain-Friendly Guide",
    "author": "Kathy Sierra, Bert Bates, Trisha Gee",
    "isbn": "9781491910771",
    "publishedYear": 2022
}

3.2 服務

除了資源之外,我們的 API 還要求定義 服務。這些代表實際與我們的數據交互的服務器。

我們可以根據需要擁有多個這樣的服務,每個服務代表一個需要管理的資源。

我們使用 服務 關鍵字,後跟服務名稱來定義我們的服務。在此,我們還定義服務的版本以及它所處理的資源:

service BookManagementService {
    version: "1.0"
    resources: [
        Book
    ]
}

此時,Smithy 知道我們有一個 BookManagementService API,用於管理 Book 資源,但不知道如何操作。

3.3 生命週期操作

在擁有資源後,我們需要對其執行操作。Smithy 支持一組標準生命週期操作,我們可以執行這些操作:

  • create – 用於創建資源的全新實例,其中服務生成 ID
  • put – 用於創建資源的全新實例,客户端提供 ID
  • read – 用於通過 ID 檢索現有資源的實例
  • update – 用於通過 ID 更新現有資源的實例
  • delete – 用於通過 ID 刪除現有資源的實例
  • list – 用於列出資源的實例

每個操作都使用 operation 關鍵字定義,然後是操作的名稱。在此過程中,我們定義了預期的輸入、輸出和錯誤類型。

例如,通過 ID 獲取 Book 的操作可能如下所示:

/// Retrieves a specific book by ID
@readonly
operation GetBook {
    input: GetBookInput
    output: GetBookOutput
    errors: [
        BookNotFoundException
    ]
}

/// Input structure for getting a book
structure GetBookInput {
    @required
    bookId: BookId
}

/// Output structure for getting a book
structure GetBookOutput {
    @required
    bookId: BookId

    @required
    title: String

    @required
    author: String

    @required
    isbn: String

    publishedYear: Integer
}

/// Exception thrown when a book is not found
@error("client")
structure BookNotFoundException {
    @required
    message: String
}

此輸入的值是一個單一值——即<em>bookId</em>。在成功的情況下,輸出將是我們的書的詳細信息。或者,如果書不存在,我們可能會收到錯誤。

值得注意的是,雖然可能看起來重複,但某些操作的輸入和輸出結構中定義的字段必須與我們的資源相匹配。但是,並非所有字段都需要包含,如<em>GetBookInput</em>結構所示,它僅包含<em>bookId</em>字段。

我們需要指示此<em>read</em>操作應用於<em>Book</em>資源:

resource Book {
    // ...
    read: GetBook
 }

此時,Smithy 已經知道這是一個可以對我們的資源執行的操作,以及輸入和輸出應該如何工作。

3.4. 非生命週期操作

有時,我們需要執行與資源生命週期無關的操作。例如,我們可能想要一個操作來推薦下一本閲讀的書。

我們以與生命週期操作相同的方式定義這些操作。但是,將它們附加到資源時,需要使用operations 關鍵字:

resource Book {
    // ...
    operations: [
        RecommendBook
    ]
}

/// Recommend a book
@readonly
operation RecommendBook {
    input: RecommendBookInput
    output: RecommendBookOutput
}

/// Input structure for recommending a book
structure RecommendBookInput {
    @required
    bookId: BookId
}

/// Output structure for recommending a book
structure RecommendBookOutput {
    @required
    bookId: BookId

    @required
    title: String

    @required
    author: String
}

這使得我們能夠對我們的資源執行這項新的操作。

4. 代碼生成

現在我們已經編寫了 Smithy 文件來描述我們的 API,我們需要能夠構建該 API 本身。 幸運的是,Smithy 提供了工具,可以自動從我們的 Smithy 文件生成客户端 SDK 和服務器端應用程序服務。

在本文中,我們將生成 Java 代碼。為此,我們有一個 Gradle 插件可以使用。 目前,沒有 Maven 等等效插件,因此如果我們使用 Smithy 為我們的項目生成代碼,則需要使用 Gradle 作為構建工具。

客户端和服務器端代碼生成都使用相同的 Gradle 插件依賴項,包括 smithy-jarsmithy-base,因此我們首先需要確保將其添加到我們的 settings.gradle 文件中:

pluginManagement {
    plugins {
        id 'software.amazon.smithy.gradle.smithy-jar' version "1.3.0"
        id 'software.amazon.smithy.gradle.smithy-base' version "1.3.0"
    }
}

我們還需要將 smithy-base插件添加到我們的 build.gradle文件中,並確保 java-library插件存在:

plugins {
    id 'java-library'
    id 'software.amazon.smithy.gradle.smithy-base'
}

接下來,我們需要添加 software.amazon.smithy.java.codegen:plugins 依賴項,用於 smithyBuild 範圍:

dependencies {
    smithyBuild "software.amazon.smithy.java.codegen:plugins:0.0.1"
}

現在我們確保 smithyBuild 任務在 compileJava 任務之前執行:

tasks.named('compileJava') {
    dependsOn 'smithyBuild'
}

最後,我們需要為插件編寫一個 smithy-build.json 文件。目前,這個文件需要 Smithy 的版本——當前是“1.0”,以及我們 Smithy 文件的位置:

{
  "version": "1.0",
  "sources": [
      "./smithy/"
  ]
}

此時,我們已準備好配置構建過程以生成客户端 SDK 和/或服務器端代碼。

4.1. 配置 API 協議

在我們可以生成代碼之前,我們需要更新我們的 Smithy 文件,以指示我們希望生成的 API 類型。我們將使用 AWS restJson1 協議

為此,我們首先需要標記我們的服務定義,以指示使用該協議:

@aws.protocols#restJson1
service BookManagementService {
    // ...
}

接下來,我們將為每個操作添加標籤,以指定其使用的 HTTP 方法和 URI:

@readonly
@http(method: "GET", uri: "/books/{bookId}")
operation GetBook {
    // ...
}

在此,我們指出  操作通過  方法在   URI 下暴露。  路徑參數來自輸入結構。因此,例如,ID 為  的書籍將通過  訪問。

<h3><strong>4.2. 生成客户端 SDK</strong></h3>
<p><strong>要生成客户端 SDK,我們需要在我們的 <em >java-client-codegen</em> 插件中配置 <em >smithy-build.json</em> 文件:</strong></p>
{
    ...
    "plugins": {
        "java-client-codegen": {
            "service": "com.baeldung.smithy.books#BookManagementService",
            "namespace": "com.baeldung.smithy.books.client",
            "protocol": "aws.protocols#restJson1"
        },
    }
}

這會指示構建過程將代碼生成到 com.baeldung.smithy.books.client Java 包中,並且是在 com.baeldung.smithy.books 命名空間內的 BookManagementService 中進行生成,來自我們的 Smithy 文件。

我們還需要將 software.amazon.smithy.java:aws-client-restjson 依賴項添加到我們的 build.gradle 文件中,以便為 AWS restJson1 協議構建客户端 SDK:

dependencies {
    // ...
    implementation "software.amazon.smithy.java:aws-client-restjson:0.0.1"
}

這會導致構建生成 Java 源代碼文件,位於 build 目錄下的一個區域。要編譯這些文件,我們需要告訴 Gradle 它們的存在:

afterEvaluate {
    def clientPath = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "java-client-codegen")
    sourceSets.main.java.srcDir clientPath
}

現在運行構建過程會生成一組 Java 類,我們可以使用它們與 API 進行交互。特別是,我們將獲取一個同步客户端和一個異步客户端,以及 DTO(數據傳輸對象)來表示所有輸入和輸出。這些 DTO 立即可用,可以用來與 API 交互。

BookManagementServiceClient client = BookManagementServiceClient.builder()
  .endpointResolver(EndpointResolver.staticEndpoint("http://localhost:8888"))
  .build();

GetBookOutput output = client.getBook(GetBookInput.builder().bookId("abc123").build());
assertEquals("Head First Java, 3rd Edition: A Brain-Friendly Guide", output.title());

我們正在使用客户端向<em>http://localhost:8888</em> 運行的服務發起請求,並獲取 ID 為“

4.3. 生成服務器樁

我們可以以類似的方式生成服務器樁,使用 java-server-codegen 插件代替:

{
    ...
    "plugins": {
        "java-server-codegen": {
            "service": "com.baeldung.smithy.books#BookManagementService",
            "namespace": "com.baeldung.smithy.books.server"
        },
    }
}

如前所述,這會生成來自 Smithy 文件的 BookManagementService代碼,並將其放入 com.baeldung.smithy.books命名空間內的 com.baeldung.smithy.books.serverJava 包中。

我們還需要將 software.amazon.smithy.java:aws-server-restjson依賴項添加到 build.gradle文件中,以便為 AWS restJson1 協議構建服務器樁,以及為實際 HTTP 服務器添加 software.amazon.smithy.java:server-netty 依賴項

dependencies {
    // ...
    implementation "software.amazon.smithy.java:server-netty:0.0.1"
    implementation "software.amazon.smithy.java:aws-server-restjson:0.0.1"
}

這會導致構建生成 Java 源代碼文件,存儲在 build 目錄下的一個區域內。 類似於之前,為了編譯這些文件,我們需要告知 Gradle 它們的存在:

afterEvaluate {
    def serverPath = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "java-server-codegen")
    sourceSets.main.java.srcDir serverPath
}

現在運行構建生成了一組我們可以用作服務器樁的 Java 類。但是,這尚未成為一個可運行的服務器。我們還需要設置服務器本身並提供我們操作的實現。

我們的生成代碼為我們服務中的每個操作提供接口,以及這些操作的輸入和輸出類:

/**
 * Retrieves a specific book by ID
 */
@SmithyGenerated
@FunctionalInterface
public interface GetBookOperation {
    GetBookOutput getBook(GetBookInput input, RequestContext context);
}

我們需要編寫自己的類,以實現這些接口:

class GetBookOperationImpl implements GetBookOperation {
    public GetBookOutput getBook(GetBookInput input, RequestContext context) {
        return GetBookOutput.builder()
          .bookId(input.bookId())
          .title("Head First Java, 3rd Edition: A Brain-Friendly Guide")
          .author("Kathy Sierra, Bert Bates, Trisha Gee")
          .isbn("9781491910771")
          .publishedYear(2022)
          .build();
    }
}

一旦我們完成了這些,我們就可以創建並啓動我們的 HTTP 服務器:

Server server = Server.builder()
  .endpoints(URI.create("http://localhost:8888"))
  .addService(
    BookManagementService.builder()
      .addCreateBookOperation(new CreateBookOperationImpl())
      .addGetBookOperation(new GetBookOperationImpl())
      .addListBooksOperation(new ListBooksOperationImpl())
      .addRecommendBookOperation(new RecommendBookOperationImpl())
      .build()
  )
  .build();

server.start();

此時,我們已經擁有一個完全功能完善的 API,它實現了我們 Smithy 文件中定義的規範。

5. 結論

在本文中,我們對 Smithy 以及我們可以利用它所做的事情進行了簡要的介紹。我們看到了如何使用 IDL 語言來描述我們的 API,以及如何從這些 API 定義中生成客户端 SDK 和服務器端樁。 藉助這個框架,我們可以實現更多,因此,當您需要創建 API 時,它絕對值得一探究竟。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.