Smithy 簡介

REST
Remote
1
02:19 AM · Dec 01 ,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 Files

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

我們的 Smithy 文件從定義 Smithy IDL 格式的版本和該 API 將存在的命名空間開始:

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

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

3.1. Resources

首先需要定義我們的資源。這些代表我們將要處理的數據。這些使用 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

這裏,我們有一個<em>Book</em>資源。它使用<em>id</em>字段進行唯一標識,類型為<em>BookId</em>;而<em>BookId</em>本身則定義為<em>String</em>;並符合給定的格式。我們的<em>Book</em>此外,還具有<em>title</em>、<em>author</em>、<em>isbn</em>;和<em>publishedYear</em>;這些屬性。因此,這是一個資源的 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. Services

除了資源之外,我們的 API 還需要服務定義。這些代表與我們的數據交互的實際服務器。我們可以有儘可能多的這些服務器,每個服務器代表一個不同的資源進行管理。

我們使用<em>service</em>關鍵字後跟服務的名稱來定義我們的服務。在其中,我們定義服務的版本和它所處理的資源:

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

此時,Smithy 知道我們有一個<em>BookManagementService</em> API,該 API 將管理<em>Book</em> 資源,但不知道如何執行此操作。

3.3. Lifecycle Operations

一旦我們定義了資源,我們還需要能夠對其執行操作。Smithy 支持一組標準生命週期操作,我們可以執行這些操作:

  • create – Used to create a new instance of the resource, where the service generates the IDs
  • put – Used to create a new instance of the resource, where the client provides the IDs
  • read – Used to retrieve an existing instance of the resource by ID
  • update – Used to update an existing instance of the resource by ID
  • delete – Used to delete an existing instance of the resource by ID
  • list – Used to list instances of the resource

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

例如,獲取<em>Book</em>;按 ID 的操作可能如下所示:

/// 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>;字段。

然後,我們需要指示此 read 操作應用於 Book 資源:

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

此時,Smithy 現在知道我們有一個<em>GetBook</em>;操作,我們可以對其資源執行,並且輸入和輸出的工作方式已知。

3.4. Non-Lifecycle Operations

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

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

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. Code Generation

Now that we’ve written our Smithy file to describe our API, we need to be able to build the API itself. Fortunately, Smithy provides tools that automatically generate both client-side SDKs and server-side application services from our Smithy file.

In this article, we’re going to generate Java code. For this, there’s a Gradle plugin that we can use. Unfortunately, there’s no Maven equivalent at the moment, so if we’re going to use Smithy to generate code for our projects, we need to use Gradle as the build tool.

Both client and server code generation use the same Gradle plugin dependencies for and , so we first need to ensure we add it to our settings.gradle file:

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

We also need to add the smithy-base plugin to our build.gradle file, as well as ensuring the java-library plugin is present:

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

Next, we need to include the dependency for the smithyBuild scope:

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

Now we ensure that the smithyBuild task runs before the compileJava task:

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

Finally, we need to write a smithy-build.json file for the plugin to use. For now, this needs the version of smithy-build – currently “1.0” – and the location of our Smithy files:

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

At this point, we’re ready to configure our build for client SDK and/or server code generation.

4.1. Configuring API Protocols

Before we can generate our code, we need to update our Smithy file to indicate the kind of API that we wish to generate. We’ll use the AWS restJson1 protocol.

To do this, we first need to tag our service definition to indicate this is the protocol to use:

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

Next, we tag each operation to specify the HTTP method and URI it uses:

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

Here, we’ve indicated that the GetBook operation is exposed using the GET method under the /books/{bookId} URI. The bookId path parameter is taken from the input structure. So, for example, a book with the ID abc123 will be accessed using GET /books/abc123.

4.2. Generating Client SDKs

To generate a client SDK, we need to configure the java-client-codegen plugin in our smithy-build.json file:

{
    ...
    "plugins": {
        "java-client-codegen": {
            "service": "com.baeldung.smithy.books#BookManagementService",
            "namespace": "com.baeldung.smithy.books.client",
            "protocol": "aws.protocols#restJson1"
        },
    }
}

This tells the build to generate code into the com.baeldung.smithy.books.client Java package, and to do so for the BookManagementService within the com.baeldung.smithy.books namespace from our Smithy files.

We also need to add the  dependency to our build.gradle file to build a client SDK for the AWS restJson1 protocol:

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

This then causes the build to generate Java source files in an area under the build directory. To compile these, we then need to tell Gradle about them:

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

Running our build now generates a set of Java classes that we can use to interact with the API. In particular, we’ll obtain both a synchronous and an asynchronous client, as well as DTOs to represent all of our inputs and outputs. These are then immediately ready to use to interact with the 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());

Here we’re using the client to make a call to the service running on http://localhost:8888 and retrieving the title of the book with ID “abc123“.

4.3. Generating Server Stubs

We can generate server stubs in a very similar manner, using the java-server-codegen plugin instead:

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

As before, this generates the code for the BookManagementService within the com.baeldung.smithy.books namespace from our Smithy files into the com.baeldung.smithy.books.server Java package.

We also need to add the  dependency to our build.gradle file to build server stubs for the AWS restJson1 protocol and the dependency for the actual HTTP server:

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

This then causes the build to generate Java source files into an area under the build directory. As before, to compile these, we then need to tell Gradle about them:

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

Running our build now generates a set of Java classes that we can use as stubs for our server. However, this isn’t yet a working server. We also need to set up the server itself and provide implementations of our operations.

Our generated code provides interfaces for each of the operations in our service, as well as classes for the inputs and outputs of these operations:

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

We need to write our own classes that implement each of these interfaces:

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();
    }
}

Once we’ve done this, we can then create and start our HTTP server:

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();

At this point, we have a fully functional API implementing the specification defined in our Smithy file.

5. 結論

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

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

發佈 評論

Some HTML is okay.