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