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を作ることができるので、おすすめです!