作成背景
本プロジェクトの開発環境では、Swaggerライブラリを顧客企業のサービスに直接適用することが難しい制約がありました。一般的なSpringベースのサービスであれば、Springfoxまたはspringdoc-openapiを使用してControllerとDTOに基づくAPI仕様を自動生成できますが、顧客企業の環境では、サービスごとの依存関係の追加やランタイム連携を自由に行うことができませんでした。
それでも顧客は、API仕様の管理にSwaggerを使用したいと考えていました。特に、現在デプロイされているAPI一覧と詳細なrequest、response構造をSwagger UIで確認したいというニーズがありました。さらに、複数の開発チームがそれぞれ開発したサービスについて、APIドキュメントを手作業で作成・更新することの遅延に起因するVOCも発生していました。
PoCの出発点は、この制約を維持しながらSwaggerベースのドキュメント化体験を提供できるか検証することでした。つまり、各サービスにSwaggerライブラリを直接組み込まず、中央Swaggerサーバーを通じてデプロイ済みAPIの仕様を確認できる構成を作ることが目標でした。
問題の定義
このプロジェクトの核心は、顧客企業のすべての開発チームが1つのSwaggerサーバーに接続し、現在デプロイされているAPI仕様を確認できるようにすることでした。そのため、個別サービス内でSwaggerドキュメントを表示する方式ではなく、中央Swaggerサーバーが複数のAPI仕様を収集し、統合して提供する方式を計画しました。
-
API仕様はデプロイ時に収集し、別途管理する中央Swaggerサーバーにリアルタイムで反映することを目標としました。
-
仕様ファイルはSwagger UIで直接読み込めるYAML形式である必要がありました。
-
将来的には、各開発チームが私の提供するCustom Annotationを追加するだけで、仕様が自動生成される構成に拡張できる必要がありました。
PoCでは、この目標を2段階に分けて検証しました。まず、APIから収集したReq / Res情報をSwagger YAML形式で自動生成し、別サーバーで統合提供しました。その後、Custom Annotationを基にAPIとVOを分析してYAMLを生成する、ライブラリ化の方向性を検証しました。
PoC構成の概要
サービスは大きく3つの領域に分けました。swagger-apisは自動生成されたAPI仕様YAMLファイルを保管する外部ストレージの役割を担い、swagger-demoはこれらのYAMLファイルを読み込んでSwagger UIに公開するSpring Bootサーバーです。swagger-file-generatorは、Javaオブジェクトを基にSwagger YAMLファイルを生成できるか確認し、ライブラリ化して各開発チームに提供するためのプロジェクトです。
|
フォルダー |
役割 |
説明 |
|---|---|---|
|
swagger-apis |
仕様ファイルのストレージ |
APIごとのswagger.yamlファイルを保管する外部ディレクトリです。 |
|
swagger-demo |
Swaggerサーバー |
Spring BootとSpringfoxを使用して、複数のYAMLをSwagger UIに登録します。 |
|
swagger-file-generator |
YAML生成実験 |
JavaオブジェクトモデルをYAML文字列に変換し、ファイルとして保存します。 |
この構成の利点は、Swagger UIの提供責任とAPI仕様の生成責任を分離できる点です。サービスコードは仕様生成に集中し、中央SwaggerサーバーはYAMLファイルを基にUIを提供します。
中央Swaggerサーバーの構成
swagger-demoプロジェクトは、Spring BootベースのSwaggerサーバーです。このサーバーは自身のAPIをドキュメント化するのではなく、外部、つまり各開発チームから提供された複数のSwagger YAMLファイルを、Swagger UIで選択可能なリソースとして登録します。顧客企業内の各開発チームは、このサーバーのSwagger UIに接続し、現在デプロイされているAPI仕様を確認できます。
中核となる実装は、SwaggerResourcesProviderを再定義した部分です。Springfoxがデフォルトで提供するSwaggerリソース一覧をそのまま使用せず、外部ディレクトリからYAMLファイルを読み込み、SwaggerResource一覧を直接構成します。
@Primary
@Bean
public SwaggerResourcesProvider swaggerResourcesProvider(
InMemorySwaggerResourcesProvider defaultResourcesProvider
) {
return () -> {
List<SwaggerResource> resources = new ArrayList<>();
List<File> innerFiles = createFilesFromExternalDir();
innerFiles.forEach(file -> {
String path = "/" + file.getParentFile().getParentFile().getName()
+ "/" + file.getParentFile().getName()
+ "/" + file.getName();
String title = Files.readAllLines(file.toPath()).stream()
.filter(f -> f.contains("title"))
.findFirst()
.map(m -> m.split(":")[1].split("\"")[1])
.orElseGet(() -> "No Title");
resources.add(loadResource(path, title));
});
return resources;
};
}
上記コードのポイントは、Swagger UIに表示するリソースを動的に構成する点です。各YAMLファイルのパスはSpring Bootの静的リソースパスとして作成され、YAML内部のtitle値はSwagger UIのドロップダウンに表示される名前として使用されます。
この方式でサーバーを構成すると、個々の開発チームがSwagger UIサーバーを別途運用する必要はありません。中央Swaggerサーバーだけをデプロイしておけば、各API仕様がYAMLファイル単位で追加され、Swagger UIで選択可能な項目として公開されます。
外部YAMLの収集と静的リソースへの反映
swagger-demoは、ルートのswagger-apisディレクトリを外部仕様ストレージとして使用します。サーバー起動時に../swagger-apis/配下のAPIごとのフォルダーを巡回し、各フォルダーに存在する.yamlファイルをsrc/main/resources/static/swagger-apis/配下へコピーします。
private List<File> createFilesFromExternalDir() throws IOException {
String externalDir = "../swagger-apis/";
String innerDir = "src/main/resources/static/swagger-apis/";
List<String> externalFilePaths = Files.list(Paths.get(externalDir))
.filter(path -> Files.isDirectory(path))
.map(path -> Files.list(path)
.filter(f -> f.toString().contains(".yaml"))
.findFirst().get())
.map(path -> path.getParent().getFileName() + "/" + path.getFileName())
.toList();
return externalFilePaths.stream()
.map(filePath -> {
File newFile = new File(innerDir + filePath);
Files.createDirectories(newFile.toPath().getParent());
Files.copy(
Paths.get(externalDir + filePath),
Paths.get(newFile.getPath()),
StandardCopyOption.REPLACE_EXISTING
);
return newFile;
})
.toList();
}
静的リソース領域にコピーされたYAMLには、/swagger-apis/api2/swagger.yamlのようなURLでアクセスでき、Swagger UIはこのURLを読み込んでAPIドキュメントをレンダリングします。運用構成へ発展させる場合は、現在相対パスとして指定している外部仕様ストレージのパスを設定値として分離する補完が必要です。
Custom Annotationベースのライブラリ化の方向性
PoCの拡張方針は、このプロジェクトを共通ライブラリにすることでした。各開発チームは自分たちのプロジェクトにこのライブラリを依存関係として追加し、仕様化したいAPIにCustom Annotationを付与します。その後、プロジェクトがデプロイされる際にライブラリが動作し、annotationが付与されたAPIを探索します。
このアプローチは、顧客企業の環境上の制約を回避しながら、開発チームの使いやすさを高める方向性です。開発者は複雑なSwagger設定を直接扱わず、仕様化対象のAPIにannotationを追加するだけで済みます。ライブラリはControllerまたはHandlerの情報を基にrequestBodyとresponse VOを分析し、Swagger YAMLの生成に必要な情報を収集します。
-
APIの識別:Custom Annotationが付与されたAPIメソッドを探索します。
-
リクエストの分析:requestBodyの型とフィールド構造を巡回します。
-
レスポンスの分析:response VOの型とネストされたフィールド構造を巡回します。
-
YAMLの生成:Swagger 2.0形式に合わせて、paths、parameters、definitions、responsesを構成します。
-
サーバーへの反映:生成したYAMLファイルを、Swaggerサーバーが読み取れる場所に生成または送信します。
この構成が完成すれば、API仕様の更新はデプロイフローと自然に連携します。プロジェクトのデプロイ時に、ライブラリがannotationの付与されたAPIを基にYAMLを生成し、中央Swaggerサーバーが変更されたYAMLを読み込んでSwagger UIに反映します。
YAML生成プロジェクトの役割
swagger-file-generatorは、JavaオブジェクトモデルからSwagger YAMLファイルを生成できるか確認するための実験プロジェクトです。現在の実装では、Swaggerドキュメントの主要構成要素をVOクラスで表現し、各オブジェクトのtoString()メソッド内でYAML文字列を組み立てる方式を採用しています。
このプロジェクトは完成したライブラリというより、annotationベースの仕様生成ライブラリを作る前に必要となるYAML生成ロジックの可能性を検証したものです。requestBodyとresponse VOを巡回した結果をSwaggerDefinition、SwaggerProperty、SwaggerParameterなどのオブジェクトに格納すれば、最終的なSwagger YAMLファイルに変換できます。
SwaggerYamlContentFactory factory = SwaggerYamlContentFactory.builder()
.info(SwaggerInfo.builder()
.description("This is build test")
.version("1.0.0")
.title("TEST").build())
.host("localhost:8080")
.basePath("/execute")
.paths(Arrays.asList(
SwaggerPath.builder()
.url("/v1/tenants/KOREA/sample")
.httpMethod("post")
.tag("test")
.summary("For Sample Test")
.parameters(Arrays.asList(
SwaggerParameter.builder()
.name("SampleQdo")
.inType("body")
.required(true)
.type("object")
.description("sample qdo").build()))
.responses(Arrays.asList(
SwaggerResponse.builder()
.code("200")
.description("sample response").build()))
.build()))
.build();
String yamlFormat = factory.toString();
FileGenerator.writeObjectToFile(
yamlFormat,
FileGenerator.createInnerDirAndFile("test")
);
上記のテストコードは、Javaオブジェクトを組み立てた後、factory.toString()でYAML文字列を作成し、FileGeneratorを通じてswagger.yamlファイルとして保存します。実際にライブラリ化する段階では、このオブジェクト生成部分をreflectionベースの分析結果に置き換えることができます。
Swaggerドキュメントのモデル化方式
SwaggerYamlContentFactoryは、Swaggerドキュメント全体を組み立てる役割を担います。内部にはinfo、host、basePath、paths、commonParameters、commonDefinitionsが含まれており、各領域を順番に文字列へ変換します。
@Getter
@Builder
public class SwaggerYamlContentFactory {
private SwaggerInfo info;
private String host;
private String basePath;
private List<SwaggerPath> paths;
private List<SwaggerParameter> commonParameters;
private List<SwaggerDefinition> commonDefinitions;
@Override
public String toString() {
return getBasicFormat()
+ getPathsFormat()
+ getCommonParametersFormat()
+ getCommonDefinitionsFormat();
}
}
このモデルは、Swaggerドキュメントの構造とほぼ1対1で対応します。SwaggerPathはAPI URLとHTTP method、parameter、responseを表現し、SwaggerDefinitionはrequestまたはresponse VOのschema定義を表現します。SwaggerPropertyは、VO内部の1つのフィールドをSwagger propertyに変換する役割を担います。
|
クラス |
Swagger領域 |
主な役割 |
|---|---|---|
|
SwaggerInfo |
info |
ドキュメントの説明、バージョン、title情報を構成します。 |
|
SwaggerPath |
paths |
API URL、HTTPメソッド、parameter、responseを構成します。 |
|
SwaggerParameter |
parameters |
header、query、body parameterを表します。 |
|
SwaggerDefinition |
definitions |
request/response VOのschemaを表します。 |
|
SwaggerProperty |
properties |
VOフィールドの型、例、説明を表します。 |
|
SwaggerResponse |
responses |
レスポンスコードとschema参照を表します。 |
このようなモデリングは、ライブラリ化する際の重要な基盤となります。annotationが付与されたAPIを検索した後、requestBodyとresponse VOを走査すれば、その結果を上記のオブジェクトにマッピングできます。その後、オブジェクトをYAMLにシリアライズすれば、Swagger UIが読み取れる仕様ファイルを作成できます。
全体の動作フロー
最終的に目指した構成は、各開発チームのプロジェクト、共通Swagger生成ライブラリ、中央Swaggerサーバー、Swagger UIが連携するフローです。開発チームは自身のプロジェクトにライブラリを追加し、annotationを付与します。デプロイ時にライブラリがAPIとVOを分析し、YAMLファイルを生成します。中央Swaggerサーバーは生成されたYAMLを収集し、UIに反映します。
각 개발팀 프로젝트
custom annotation 적용
↓
공통 Swagger 생성 라이브러리
annotation 대상 API 분석
requestBody / response VO 순회
swagger.yaml 생성
↓
Swagger 서버
YAML 파일 수집 및 리소스 등록
↓
Swagger UI
전사 개발팀이 배포 API 명세 확인
このフローにより、サービスごとのSwagger設定を最小限に抑えながら、お客様が求めていたSwagger UIベースの仕様確認体験を提供できます。また、API仕様の生成責任を共通ライブラリに集約するため、開発チームごとの実装のばらつきを抑えられます。
期待される効果
1つ目の効果は、API仕様へのアクセス性の向上です。全社の開発チームは中央Swagger UIにアクセスし、現在デプロイされているAPI仕様を確認できます。APIを提供するチームと利用するチームが同じ画面を基準にコミュニケーションできるため、協業コストを削減できます。
2つ目の効果は、ドキュメントを最新化する負担の軽減です。Custom Annotationベースのライブラリ化が完了すれば、開発者は仕様化対象のAPIにannotationを付与するだけで、YAML生成フローに参加できます。デプロイ時にrequestBodyとresponse VOを基準として仕様が生成されるため、手作業によるドキュメント作成と比べて、記載漏れの可能性を低減できます。
3つ目の効果は、既存環境の制約を維持しながらSwagger導入の効果を得られる点です。各サービスがSwaggerライブラリを直接深く統合しなくても、中央Swaggerサーバーと共通ライブラリを通じて、Swagger UIベースのドキュメント化体験を提供できます。
-
全社共通のSwagger UIを通じて、API仕様を確認する場所を一元化できます。
-
開発チームごとのSwagger設定のばらつきを抑え、共通ライブラリを基準として仕様生成方式を標準化できます。
-
デプロイフローと仕様生成フローを連携させ、最新のAPIを基準としたドキュメント化を目指せます。
PoCで確認した限界
PoC段階の実装は可能性の検証に重点を置いていたため、運用に適用するには補完すべき点もあります。現在のswagger-demoは外部YAMLパスに相対パスを使用しているため、実行場所の影響を受けます。運用環境では、このパスを設定値として分離し、仕様ファイルの保存先を明確に定義する必要があります。
また、YAML生成プロジェクトは文字列を組み立てる方式でSwaggerドキュメントを生成します。この方式は理解しやすい一方で、ドキュメント構造が複雑になるほど、記載漏れやインデントエラーが発生する可能性があります。運用レベルのライブラリへ発展させるには、YAMLシリアライズライブラリやOpenAPIモデルオブジェクトを使用する方式も検討する必要があります。
まとめ
今回のPoCの核心は、お客様の既存の開発環境における制約を維持しながら、SwaggerベースのAPI仕様提供方式を検証したことです。お客様はSwaggerによるAPI仕様の確認を希望しており、開発チームには手作業によるドキュメント作成と仕様の最新化にかかる負担を軽減する必要がありました。
そのため、中央Swaggerサーバーを構成し、複数のSwagger YAMLファイルをSwagger UI上で選択して閲覧できるように開発しました。さらに、このプロジェクトをライブラリ化し、各開発チームがCustom Annotationを適用するだけで、デプロイ時にrequestBodyとresponse VOを走査し、Swagger YAMLを生成する構造を目標としました。
結論として、このプロジェクトはSwaggerを各サービスに直接組み込む方式ではなく、共通ライブラリと中央Swaggerサーバーを通じてAPI仕様を自動生成し、共有する方式を検証したPoCです。お客様の全社開発チームが同じSwagger UIでデプロイ済みAPIの仕様を確認できる基盤を整備した点に意義があります。
NZ