鐵匠鋪簡介
1. 簡介
Smithy是一種描述 API 的方法,它包含一組支援工具,使我們能夠根據此定義產生 API 用戶端和伺服器。它允許我們描述 API,然後根據定義產生客戶端和伺服器程式碼。
在本教程中,我們將快速了解 Smithy IDL(介面定義語言)及其相關工具。
2. 鐵匠鋪是什麼?
Smithy 是一種介面定義語言,它允許我們以與語言和協定無關的格式描述 API。亞馬遜最初設計 Smithy 是為了描述 AWS API,後來他們將其公開發布,以便其他服務也可以從相同的工具中受益。
我們可以使用提供的工具,根據這些 API 定義自動產生伺服器和客戶端 SDK 程式碼。這樣,我們就可以將 Smithy 檔案作為 API 運作方式的權威參考,確保所有客戶端和伺服器程式碼與此檔案相比都是正確的。
Smithy 旨在定義基於資源的 API。其標準用法是透過 HTTP 傳輸 JSON,但我們也可以使用其他序列化格式(例如 XML),甚至使用非 HTTP 傳輸協定(例如 MQTT)。
Smithy 在概念上可以被認為與 OpenAPI 和 RAML 等其他標準類似。然而,Smithy 對 API 的工作方式採取了更加固執己見的態度——期望它們基於資源。同時,Smithy 並沒有規定傳輸或序列化應該如何運作,這提供了更大的靈活性。
3. 史密斯檔案
我們透過編寫Smithy IDL 格式的.smithy
檔案來定義我們的 API。這些文件根據資源、對資源執行的操作以及代表整個 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
資源。它使用BookId
類型的id
字段進行唯一標識——我們將其單獨定義為符合給定格式的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
定義。這些服務代表了處理我們資料的實際伺服器。我們可以根據需要定義任意數量的服務,每個服務代表一種需要管理的不同資源。
我們使用service
關鍵字和服務名稱來定義我們的服務。然後,我們定義服務的版本以及它所使用的資源:
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
}
這個方法的輸入只有一個值- bookId
。成功後,輸出就是這本書的詳細資訊。如果這本書不存在,我們可能會收到錯誤。
值得注意的是,儘管看起來有些重複,但某些操作的輸入和輸出結構中定義的欄位必須與我們資源中的欄位完全匹配。然而,並非所有欄位都需要包含,正如我們從僅包含bookId
欄位的GetBookInput
結構中看到的那樣。
然後我們需要指出此read
操作適用於Book
資源:
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 作為建置工具。
客戶端和伺服器程式碼產生都對smithy-jar
和smithy-base
使用相同的 Gradle 插件依賴項,因此我們首先需要確保將其新增至我們的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'
}
接下來,我們需要為smithyBuild
範圍包含software.amazon.smithy.java.codegen:plugins
依賴項:
dependencies {
smithyBuild "software.amazon.smithy.java.codegen:plugins:0.0.1"
}
現在我們確保smithyBuild
任務在compileJava
任務之前執行:
tasks.named('compileJava') {
dependsOn 'smithyBuild'
}
最後,我們需要編寫一個smithy-build.json
檔案供插件使用。目前,這需要smithy-build
的版本(目前為「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 {
// ...
}
這裡,我們指明了GetBook
操作是透過GET
方法在/books/{bookId}
URI 下公開的bookId
路徑參數取自輸入結構。例如,一本 ID 為abc123
的書籍可以透過GET /books/abc123
存取。
4.2. 產生客戶端 SDK
要產生客戶端 SDK,我們需要在smithy-build.json
檔案中設定java-client-codegen
外掛程式:
{
...
"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 套件中,並為來自我們的 Smithy 檔案的com.baeldung.smithy.books
命名空間內的BookManagementService
執行此操作。
我們還需要將software.amazon.smithy.java:aws-client-restjson
相依性新增至我們的build.gradle
檔案中,以便為 AWS restJson1 協定建置客戶端 SDK:
dependencies {
// ...
implementation "software.amazon.smithy.java:aws-client-restjson:0.0.1"
}
這會導致建置在build
目錄下的某個區域中產生 Java 原始檔。為了編譯這些文件,我們需要將它們告知 Gradle:
afterEvaluate {
def clientPath = smithy.getPluginProjectionPath(smithy.sourceProjection.get(), "java-client-codegen")
sourceSets.main.java.srcDir clientPath
}
執行建置後,會產生一組可用於與 API 互動的 Java 類別。具體來說,我們將獲得一個同步客戶端和一個非同步客戶端,以及用於表示所有輸入和輸出的 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());
這裡我們使用客戶端呼叫運行在http://localhost:8888
上的服務並檢索 ID 為「 abc123
」的書名。
4.3. 產生伺服器存根
我們可以以非常類似的方式產生伺服器存根,而是使用java-server-codegen
外掛程式:
{
...
"plugins": {
"java-server-codegen": {
"service": "com.baeldung.smithy.books#BookManagementService",
"namespace": "com.baeldung.smithy.books.server"
},
}
}
與之前一樣,這會從我們的 Smithy 檔案產生com.baeldung.smithy.books
命名空間內的BookManagementService
程式碼到com.baeldung.smithy.books.server
Java 套件中。
我們還需要將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"
}
這會導致建置在build
目錄下產生 Java 原始檔。和之前一樣,為了編譯這些文件,我們需要將它們告知 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 時,它值得一看。
與往常一樣,本文中的所有範例都可以在 GitHub 上找到。