見出し画像

JHipsterでOASからAPIコード作ってみた

みなさんこんにちは、@ultaroです!

JHipsterはEntityからScaffoldでモダンなアプリケーションが出来て素敵な、みんなにおすすめできるツールの一つです!でも時代はAPI、APIファーストで作りたい!と思っている人も多いのではないでしょうか?そしてAPI設計書は、OpenAPI仕様(Open API Specification /OAS)で作っているんじゃなでしょうか?

ということで、今回はJHipsterでOASからAPIコードを作成してみようと思います!(ちなみにOpenAPI Generatorの自動生成を使ってもAPIが生成できますが、その方法との違いはまた今度紹介したいと思います・・・)

下記のページの通り、JHipsterではOASからAPIを作成することができます。公式ドキュメントに従って実施すればよさそうです。では、実際にやってみましょう!



前提条件

今回の検証では以下のバージョンを利用しています。

  • Java 17.0.10

  • JHipster 8.3.0

1.JHipsterプロジェクトの作成

JHipsterプロジェクトを以下のように構築します。 重要な点は? Which other technologies would you like to use?の質問にAPI first development using OpenAPI-generatorと答えることです。

なお ?What is your default Java package name?への回答は本来は”com.mycompany.myapp”のようなパッケージ名になります。本記事ではわかりやすくするために[my_package_name]と記載しています。

? What is the base name of your application? --> openapisample
? Which *type* of application would you like to create? --> Microservice application
? Besides Junit, which testing frameworks would you like to use? -->  (skip)
? Do you want to make it reactive with Spring WebFlux? --> No
? As you are running in a microservice architecture, on which port would like your server to run? It should be unique to
 avoid port conflicts. --> 8081
? What is your default Java package name? --> [my_package_name]
? Which service discovery server do you want to use? --> No service discovery
? Which *type* of authentication would you like to use? --> JWT authentication (stateless, with a token)
? Which *type* of database would you like to use? --> SQL (H2, PostgreSQL, MySQL, MariaDB, Oracle, MSSQL)
? Which *production* database would you like to use? --> PostgreSQL
? Which *development* database would you like to use? --> PostgreSQL
? Which cache do you want to use? (Spring cache abstraction) --> Ehcache (local cache, for a single node)
? Do you want to use Hibernate 2nd level cache? --> No
? Would you like to use Maven or Gradle for building the backend? --> Maven
? Which other technologies would you like to use? --> API first development using OpenAPI-generator
? Which *Framework* would you like to use as microfrontend? --> No client
? Would you like to enable internationalization support? --> No
? Please choose the native language of the application --> Japanese

2. OpenAPI仕様書を作成する

まずは、src/main/resources/swagger/api.ymlにOpenAPI仕様を定義します。

docker compose -f src/main/docker/swagger-editor.yml up -d 

でSwagger Editorを起動できます。 必要に応じてSwagger Editorを使用しながらOpenAPI仕様を定義します。

こちらのリンク先のサンプルを参考にしてOpenAPI仕様書は以下のように定義しました。

# API-first development with OpenAPI
# This file will be used at compile time to generate Spring-MVC endpoint stubs using openapi-generator
openapi: '3.0.1'
info:
  title: 'openapisample'
  version: 0.0.1
servers:
  - url: http://localhost:8081/api
    description: Development server
  - url: https://localhost:8081/api
    description: Development server with TLS Profile
tags:
  - name: pet
    description: Everything about your pet
paths:
  /pet:
    get:
      tags:
        - pet
      description: |
        Returns all pets from the system that the yser has access to
      operationId: findPets
      parameters:
        - name: tags
          in: query
          description: tags to filter by
          required: false
          style: form
          schema:
            type: array
            items:
              type: string
        - name: limit
          in: query
          description: maximum number of results to return
          required: false
          schema:
            type: integer
            format: int32
      responses:
        '200':
          description: pet response
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Pet'
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    post:
      tags:
        - pet
      description: Creates a new pet in the store. Duplicates are allowed
      operationId: addPet
      requestBody:
        description: Pet to add to the store
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewPet'
      responses:
        '200':
          description: pet response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /pets/{id}:
    get:
      tags:
        - pet
      description: >-
        Returns a user based on a single ID, if the user does not have access to
        the pet
      operationId: find pet by id
      parameters:
        - name: id
          in: path
          description: ID of pet to fetch
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: pet response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      tags:
        - pet
      description: deletes a single pet based on the ID supplied
      operationId: deletePet
      parameters:
        - name: id
          in: path
          description: ID of pet to delete
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '204':
          description: pet deleted
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    Pet:
      allOf:
        - $ref: '#/components/schemas/NewPet'
        - type: object
          required:
            - id
          properties:
            id:
              type: integer
              format: int64
      example:
        name: Chessie Cat
        id: 1
        tag: cat
    NewPet:
      type: object
      required:
        - name
      properties:
        name:
          type: string
        tag:
          type: string
    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: integer
          format: int32
        message:
          type: string
  responses:
    Problem:
      description: error occurred - see status code and problem object for more information.
      content:
        application/problem+json:
          schema:
            type: object
            properties:
              type:
                type: string
                format: uri
                description: |
                  An absolute URI that identifies the problem type.  When dereferenced,
                  it SHOULD provide human-readable documentation for the problem type
                  (e.g., using HTML).
                default: 'about:blank'
                example: 'https://www.jhipster.tech/problem/constraint-violation'
              title:
                type: string
                description: |
                  A short, summary of the problem type. Written in english and readable
                  for engineers (usually not suited for non technical stakeholders and
                  not localized); example: Service Unavailable
              status:
                type: integer
                format: int32
                description: |
                  The HTTP status code generated by the origin server for this occurrence
                  of the problem.
                minimum: 100
                maximum: 600
                exclusiveMaximum: true
                example: 503
              detail:
                type: string
                description: |
                  A human readable explanation specific to this occurrence of the
                  problem.
                example: Connection to database timed out
              instance:
                type: string
                format: uri
                description: |
                  An absolute URI that identifies the specific occurrence of the problem.
                  It may or may not yield further information if dereferenced.

  securitySchemes:
    jwt:
      type: http
      description: JWT Authentication
      scheme: bearer
      bearerFormat: JWT
security:
  - jwt: []

3. APIを自動生成する

./mvnw generate-sources

を実行します。すると、target/generated-sources/openapi/src 以下にソースコード(DTO/コントローラ類)が生成されます。

今回生成されたのは以下のようなファイルです。 OpenAPI定義のschemasがdtoとして生成され、APIのコントローラなどが生成されています。

target/generated-sources/openapi/src/main/java
└──  [my_package_name]
        ├── service
        │   └── api
        │       └── dto
        │           ├── Error.java
        │           ├── NewPet.java
        │           └── Pet.java
        └── web
            └── api
                ├── ApiUtil.java
                ├── PetsApi.java
                ├── PetsApiController.java
                └── PetsApiDelegate.java

さて、ここまで出来たらいったんAPIの動作確認をしてみます。

最初のプロジェクト生成の際、authentication の種類で JWT authenticationを選択しましたが、今回は検証なので認証工程はすっ飛ばしていきたいと思います。
src/main/java/[my_package_name]/config/SecurityConfiguration.javaのfilterChainメソッドを以下のように編集し、 /api/** エンドポイントを誰でもアクセス可にします。( + が追加した行、 - が削除した行です。)

(前略)
@Configuration
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfiguration {

    private final JHipsterProperties jHipsterProperties;

    public SecurityConfiguration(JHipsterProperties jHipsterProperties) {
        this.jHipsterProperties = jHipsterProperties;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(
                authz ->
                    // prettier-ignore
                authz
                    .requestMatchers(mvc.pattern(HttpMethod.POST, "/api/authenticate")).permitAll()
                    .requestMatchers(mvc.pattern(HttpMethod.GET, "/api/authenticate")).permitAll()
                    .requestMatchers(mvc.pattern("/api/admin/**")).hasAuthority(AuthoritiesConstants.ADMIN)
 -                  .requestMatchers(mvc.pattern("/api/**")).authenticated()
 +                  .requestMatchers(mvc.pattern("/api/**")).permitAll()
                    .requestMatchers(mvc.pattern("/v3/api-docs/**")).hasAuthority(AuthoritiesConstants.ADMIN)
                    .requestMatchers(mvc.pattern("/management/health")).permitAll()
                    .requestMatchers(mvc.pattern("/management/health/**")).permitAll()
                    .requestMatchers(mvc.pattern("/management/info")).permitAll()
                    .requestMatchers(mvc.pattern("/management/prometheus")).permitAll()
                    .requestMatchers(mvc.pattern("/management/**")).hasAuthority(AuthoritiesConstants.ADMIN)
            )
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .exceptionHandling(
                exceptions ->
                    exceptions
                        .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
                        .accessDeniedHandler(new BearerTokenAccessDeniedHandler())
            )
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults()));
        return http.build();
    }
(後略)


次に開発環境DBにPostgreSQLを選択したので、以下のコマンドででpostgresql入りのdockerを立ち上げます。

docker compose -f src/main/docker/postgresql.yml up -d 

mvnwを実行しアプリを起動します。

 ./mvnw

では、curlコマンドでエンドポイントhttp://localhost:8081/pets/{id}に対してリクエストを投げてみます。

HTTPレスポンスヘッダを取得してみましょう。するとステータス 501(Not Implemented)が返ってきました。
それはそうです、まだAPIの実装は完了していないのですから・・・!

>curl -I http://localhost:8081/api/pets/1
HTTP/1.1 501 Not Implemented
Expires: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
X-XSS-Protection: 0
Pragma: no-cache
X-Frame-Options: DENY
Date: Thu, 22 Aug 2024 07:41:17 GMT
Connection: keep-alive
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
Content-Length: 0


4. APIを実装する

ということでAPIの実装をしていきましょう。

自動生成されたソースの構成(delegateパターン)


target/generated-sources/openapi/src/main/java/ [my_package_name]/web/api/以下には、自動生成された以下のファイルが存在します。PetsApiDelegate.javaがあることから分かるように、JHipsterではdelegateパターンを採用しています。 各クラス・インターフェースの役割は以下の通りです。

  • PetsApiインターフェース(PetsApi.java)

    • API定義に対応して設定する(リクエストマッピングなど)。

  • PetsApiDelegateインターフェース(PetsApiDelegate.java)

    • 各メソッドのdefaultメソッドでNOT IMPLEMENTEDを返す。

  • PetsApiControllerクラス(PetsApiController.java)

    • PetApiインターフェースを実装する。

    • PetsApiDelegateインターフェースを実装したBeanがあれば注入する。

このような構成になっているので、PetsApiDelegateが実装されていればその内容が返却されます。 実装されていなければPetsApiDelegateインターフェースのdefaultメソッドから501 Not Implementedが返却されます。

Delegateクラスの実装


PetsApiDelegateを実装してみましょう。
PetsApiDelegateの実装クラス(PetsApiDelegateImpl.java)はsrcディレクトリ以下に作成します。
自動生成されたソースコードは前述のとおりtargetディレクトリ以下に配置されていますので、targetディレクトリ以下には手動で手を加えない、が大前提です。target以下はAPIの再生成を行うと上書きされてしまいますからね。

[project_directory]
    ├── target/generated-sources/openapi/src/main/java ※自動生成されたソース
    │       └──  [my_package_name]
    │               ├── service
    │               │   └── api
    │               │       └── dto
    │               │           ├── Error.java
    │               │           ├── NewPet.java
    │               │           └── Pet.java
    │               └── web
    │                   └── api
    │                       ├── ApiUtil.java
    │                       ├── PetsApi.java
    │                       ├── PetsApiController.java
    │                       └── PetsApiDelegate.java
    └──src/main/java ※手動で実装するソース
            └──  [my_package_name]
                   └──  service
                            └── PetsApiDelegateImpl.java

さてPetsApiDelegateインターフェースを実装する際に該当クラスをインポートする必要がありますが、IDEやエディタはtargetディレクトリ以下に生成されたファイルをインポート対象として認識しません。 以下のようなコマンドを実行して、該当クラスを認識できるようにします。

  • EclipseまたはVSCode

./mvnw eclipse:clean eclipse:eclipse
  • InteliJ

./mvnw idea:idea

サービス層に、以下のような実装クラスを作成します。 今回は検証なのでクラスに直書きでPetを返却しています。

package  [my_package_name].service;

import java.util.ArrayList;
import java.util.List;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

import  [my_package_name].service.api.dto.Pet;
import  [my_package_name].web.api.PetsApiDelegate;


@Service
public class PetsApiDelegateImpl implements PetsApiDelegate{

    private final List<Pet> pets = new ArrayList<>();

    @Override
    public ResponseEntity<Pet> findPetById(Long id) {
        Pet pet = getPets().stream().filter(p -> id.equals(p.getId())).findAny().orElse(null);
        if (pet != null) {
            return ResponseEntity.ok(pet);
        }
        return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
    }

    private List<Pet> getPets() {
        Pet pet0 = new Pet();
        pet0.setId(1L);
        pet0.setName("Chessie Cat chan");
        pet0.setTag("cat");
        pets.add(pet0);
        return pets;
    }
}


動作確認


この状態でもう一度HTTPレスポンスヘッダを取得してみると、ステータスが200(OK)になりました!

>curl -I http://localhost:8081/api/pets/1
HTTP/1.1 200 OK
Expires: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
X-XSS-Protection: 0
Pragma: no-cache
X-Frame-Options: DENY
Date: Thu, 22 Aug 2024 07:46:19 GMT
Connection: keep-alive
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
Transfer-Encoding: chunked
Content-Type: application/json

次にGETを実行してみると、さきほどPetsApiDelegateImpl.javaに直書きしたデータが返されることが確認できました!

>curl -X GET "http://localhost:8081/api/pets/1" -H "accept: application/json"
{
  "name" : "Chessie Cat chan",
  "tag" : "cat",
  "id" : 1
}


まとめ

このように公式の案内に従って実装すれば迅速にOASからAPIを作成することができます。

簡単にAPIを作ることができるので、おすすめです!