MicroProfile OpenAPI・文件在哪裡?
從事開發的時候,若需要使用 API,第一件事情就是去查詢 API 文件,然而在開發或呼叫 REST API 的時候,這事情卻覺得有點彆扭。可能是習慣了 Java 生態系下的完整,又或許是 REST 本身是個風格不是規範,每家廠商所寫出來的 API 文件、描述不盡相同,就連自己開發的產品也常常不知道怎麼描述比較好,有口頭告知、有寫在 Wiki,也有在 issue 的本文或討論中寫,但描述方法因為寫文件的人不同,就會有所不同。在研究了 MicroProfile OpenAPI(後簡稱 MPOpenAPI),才得知原來 REST API 的文件也是有規範可以遵循的,雖然規範能否被採用是一回事,然而,不得不承認自己脫節很久了......(該好好反省
MPOpenAPI 所遵循的規範是 OpenAPI v3,裡頭定義了 REST API 各種需要被描述的情境,而 MPOpenAPI 目的就是提供開發者能描述自己 REST API 的各種工具。
坦誠地說,雖然很清楚寫好 API 文件的重要性,但若文件不方便撰寫、維護很麻煩,就算用再大的管理力道,都會有反彈的時候。如果你也有這樣的困擾,或許可以考慮採用 MPOpenAPI。
它提供了各式各樣的 @annotation
,讓撰寫文件比較像在寫傳統的 Javadoc,雖然為了應對 REST 多變的風格導致數量繁多,但就像我們平常開發,多數的 API 不一定會用到,但也不會為此心煩,因為,在需要的時候反而會感激有它。
本文將不會詳述每個 @annotation
的用法,我和大家一樣都是剛入門,需要更一步了解,還是要自行閱讀官方文件跟 Javadoc。
API 文件的位置
在部署應用啟動 PayaraMicro 的同時,其實 MPOpenAPI 的機制就已經啟動了,可以在 console 看到以下的文字:
Payara Micro URLs:
http://192.168.0.104:8080/
'practice.mpopenapi-0.1' REST Endpoints:
GET /openapi/
GET /openapi/application.wadl
]]
而這個 /openapi/
就是我們要找的文件。以我的例子,只要用瀏覽器連到 http://192.168.0.104:8080/openapi/
就會看到以下 API 文件:
openapi: 3.0.0
info:
title: Deployed Resources
version: 1.0.0
servers:
- url: http://localhost:8080/
description: Default Server.
paths: {}
components: {}
是的,你沒看錯,產生的文件和設定 GitLab CI/CD 一樣使用的是 YAML 的檔案規範,依照 OpenAPI v3 裡的定義,產生的文件可以用 JSON 或 YAML 來呈現。
好啦,既然都知道文件放在哪,就來試試怎麼寫文件吧!
幫 REST API 加點說明
怎麼運用 JAX-RS
來開發 REST API 就不贅述了,接下來會把重點放在怎麼描述 API 上。
就從最簡單的 Hello World 開始!
// Java Code
@Path("hello")
public class HelloMPOpenAPI {
@GET
public String sayHello() {
return "Hello World!";
}
}
# API Doc
openapi: 3.0.0
info:
title: Deployed Resources
version: 1.0.0
servers:
- url: http://localhost:8080/
description: Default Server.
paths:
/api/hello:
get:
operationId: sayHello
responses:
default:
content:
'*/*':
schema:
type: string
description: Default Response.
components: {}
沒錯!什麼都還不需要做,框架就自動產生了一些不需要人為介入就能產生的文件,文件本身的結構性讓可讀性變得更高,真的太棒了!!!
接下來,來試試添加些說明上去,為了讓閱讀更方便,就只把變動的部分列出來:
// Java Code
@GET
@Operation(
summary = "say hello to MPOpenAPI",
description = "just return \"Hello World!\""
)
public String sayHello() { ... }
# API Doc
/api/hello:
get:
summary: say hello to MPOpenAPI
description: just return "Hello World!"
operationId: sayHello
responses:
default:
content:
'*/*':
schema:
type: string
description: Default Response.
如果我們想讓回傳更清楚些,還可以再調整一下!
// Java Code
@GET
@Produces(MediaType.TEXT_PLAIN)
@Operation(
summary = "say hello to MPOpenAPI",
description = "just return \"Hello World!\""
)
@APIResponse(
description = "a \"Hello World!\" string"
)
public String sayHello() { ... }
# API Doc
/api/hello:
get:
summary: say hello to MPOpenAPI
description: just return "Hello World!"
operationId: sayHello
responses:
default:
content:
text/plain:
schema:
type: string
description: a "Hello World!" string
是不是看起來很有個樣子了呢?
寫到這,可以看出 MPOpenAPI 框架為了減少開發者的困擾,會自動地去辨識是否使用了某些 JAX-RS
的 @annotation
,如 @GET
和 @Produces
就是自動辨識的範圍,少寫一點點都是種感動,真的是一如既往地貼心啊~~(好暖
來點有 Input/Output 格式的 API 吧
REST API 很少像 Hello World 這麼單純的,在有輸入輸出的情況下又是如何呢?讓我們來試試!
// User Pojo
public class User {
private Long id;
private String name;
private UserSex sex;
public User() {
}
public User(Long id, String name, UserSex sex) {
this.id = id;
this.name = name;
this.sex = sex;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public UserSex getSex() {
return sex;
}
public void setSex(UserSex sex) {
this.sex = sex;
}
}
// UserSex enum
public enum UserSex {
MALE, FEMAIL, UNKNOW
}
// New API to get and put User
@Path("user")
public class UserAPI {
private final AtomicLong counter = new AtomicLong(0);
private final HashMap<Long, User> users = new HashMap<>();
@Path("{id}")
@GET
@Produces(MediaType.APPLICATION_JSON)
public User getUser(@PathParam("id") Long id) {
User user = users.get(id);
return user;
}
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public User putUser(User toPut) {
User user = users.get(toPut.getId());
if (user != null) {
user.setName(toPut.getName());
user.setSex(toPut.getSex());
} else {
user = new User(counter.incrementAndGet(), toPut.getName(), toPut.getSex());
users.put(user.getId(), user);
}
return user;
}
}
那麼,新增了這些調整後,API 文件會有什麼變化呢?
# API Doc
...
/api/user:
put:
operationId: putUser
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
responses:
default:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
description: Default Response.
/api/user/{id}:
get:
operationId: getUser
parameters:
- name: id
in: path
required: true
style: simple
schema:
type: number
responses:
default:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
description: Default Response.
components:
schemas:
User:
type: object
properties:
id:
type: number
name:
type: string
sex:
$ref: '#/components/schemas/UserSex'
UserSex:
enum:
- MALE
- FEMAIL
- UNKNOW
文件中,自動產生了新 API 的說明,包括 path 上的參數都有預設的說明,同時,連回傳型態都有了明確的標示!
如果想描述清楚一點,我們也可以這麼做:
// Java Code
@Path("{id}")
@GET
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Get user by user id")
@APIResponse(description = "The user")
@APIResponse(responseCode = "204", description = "User not found")
public User getUser(@PathParam("id") Long id) { ... }
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Put a user")
@APIResponse(description = "The user after put")
@APIResponse(responseCode = "204", description = "Update a user not existed")
public User putUser(
@RequestBody(description = "The user to be put. If the user id is null means create, not null means update.", required = true) User toPut
) { ... }
# API Doc
/api/user:
put:
summary: Put a user
operationId: putUser
requestBody:
description: The user to be put. If the user id is null means create, not null means update.
content:
application/json:
schema:
$ref: '#/components/schemas/User'
required: true
responses:
"204":
description: Update a user not existed
default:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
description: The user after put
/api/user/{id}:
get:
summary: Get user by user id
operationId: getUser
parameters:
- name: id
in: path
required: true
style: simple
schema:
type: number
responses:
"204":
description: User not found
default:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
description: The user
真的是蠻便利的!
如果 想偷懶 呃呃...... 我的意思是忙不過來的話,其實什麼都不做,就已經可以讓使用 API 的人,有個大概的輪廓去推估使用效果。但若想要描述清楚,也不需要費太大的功夫。
同時,我個人覺得在 REST API 層次,若改採用 MPOpenAPI 描述,標準 Javadoc 略過不寫也沒問題。
幫 API 加個版本吧
開發 REST API 時,一不小心就會經歷數次改版後,不曉得該看哪份文件的問題。
MPOpenAPI 也能幫忙標註版本,好方便開發人員進行管理及調整,只要在 REST API 入口處加上下述 @annotation
即可:
// Java Code
@ApplicationPath("api")
@OpenAPIDefinition(
info = @Info(
title = "MicroProfile OpenAPI Practice APIs",
version = "0.0.1"
)
)
public class RestConfig extends Application {
}
在 API 文件的檔頭,就會顯示以下描述:
# API Doc
openapi: 3.0.0
info:
title: MicroProfile OpenAPI Practice APIs
version: 0.0.1
...
好啦~
這麼一來,API 文件就可以進行版本管理,讓 API 變更後不知所措的情況,少那麼一點點痛苦。
文末,做個簡單的結語,
MPOpenAPI 預設就可以自動產生有結構且可讀性極高的文件,若要撰寫得更完整也不會增加太多工作。同時,透過豐富的
@annotation
來描述 API,讓程式碼介面的可讀性提升。如此方便,有什麼理由不採用呢?by Arren Ping