プロジェクトの初期段階で見過ごされがちな、ソフトウェア開発プロセスで非常に重要なことの 1 つは、API ドキュメントです。この問題の解決策の 1 つは、ドキュメントを自動生成するためのフレームワークです。
プロジェクトをマイクロサービスに分割し、イベント ドリブン アーキテクチャを使用する場合、サービス間の相互作用は、メッセージ ブローカーを通じて送信されるイベントを使用して構築されます。
イベント駆動型アーキテクチャの場合にドキュメントを生成するには、 AsyncApiがあります。 AsyncAPI は、イベント駆動型アーキテクチャ (EDA) の現在の状態を改善しようとするオープンソース イニシアチブです。 AsyncApi には、コードからドキュメントを生成できる Java ツールがいくつかあります。この記事では、これらのspringwolfツールの 1 つをセットアップする方法について説明しました。
この記事では、次のタスク、つまり springwolf が生成する JSON ドキュメントを使用した DTO の生成をどのように解決したかを説明したいと思います。
spring wolf が生成するドキュメント構造は次のようになります。
{ "service": { "serviceVersion": "2.0.0", "info": { //block with service info }, "servers": { "kafka": { //describe of kafka connection } }, "channels": { "kafka-channel": { "subscribe": { //... "message": { "oneOf": [ { "name": "pckg.test.TestEvent", "title": "TestEvent", "payload": { "$ref": "#/components/schemas/TestEvent" } } ] } }, //... } }, "components": { "schemas": { "TestEvent": { //jsonschema of component } } } } }
jsonschema はドキュメント内のコンポーネントの記述に使用されるため、この問題を解決するためにjsonschema2pojoライブラリを使用することにしました。しかし、計画を実行しようとする過程で、いくつかの問題に遭遇しました。
これらすべての問題により、ドキュメントから必要な情報を抽出し、ポリモーフィズムをサポートし、Jackson アノテーションを追加する、jsonschema2pojo にラッパーを実装する必要が生じました。その結果、springwolf API を使用してプロジェクトの DTO クラスを生成できる Gradle プラグインが作成されます。次に、ドキュメント用にクラスに注釈を付ける方法と、 Springwolfdoc2dtoプラグインの使用方法を示します。
ここで、Enum や Map などの非プリミティブ型の生成の詳細について考えてみたいと思います。また、ポリモーフィズムに必要なアクションについても説明します。
次のメッセージを見てみましょう。
@Getter @Setter @NoArgsConstructor @AllArgsConstructor public class TestEvent implements Serializable { private String id; private LocalDateTime occuredOn; private TestEvent.ValueType valueType; private Map<String, Boolean> flags; private String value; public enum ValueType { STRING("STRING"), BOOLEAN("BOOLEAN"), INTEGER("INTEGER"), DOUBLE("DOUBLE"); private final String value; public ValueType(String value) { this.value = value; } } }
このようなメッセージの jsonschema は次のようになります。
{ "service": { //... "components": { "schemas": { "TestEvent": { "type": "object", "properties": { "id": { "type": "string", "exampleSetFlag": false }, "occuredOn": { "type": "string", "format": "date-time", "exampleSetFlag": false }, "valueType": { "type": "string", "exampleSetFlag": false, "enum": [ "STRING", "BOOLEAN", "INTEGER", "DOUBLE" ] }, "flags": { "type": "object", "additionalProperties": { "type": "boolean", "exampleSetFlag": false }, "exampleSetFlag": false }, "value": { "type": "string", "exampleSetFlag": false } }, "example": { "id": "string", "occuredOn": "2015-07-20T15:49:04", "valueType": "STRING", "flags": { "additionalProp1": true, "additionalProp2": true, "additionalProp3": true } }, "exampleSetFlag": true } } } } }
DTO クラスを生成すると、次のクラス構造が得られます。 Enum は元のバージョンと同じように処理されることがわかりますが、タイプMap<String, Boolean>のコレクションは別のクラス Flags になり、コレクション自体の値全体がFlags.additionalPropertiesフィールドに分類されます。
package pckg.test; // import @JsonInclude(JsonInclude.Include.NON_NULL) @JsonPropertyOrder({ "id", "occuredOn", "valueType", "flags", "value" }) @Generated("jsonschema2pojo") public class TestEvent implements Serializable { @JsonProperty("id") private String id; @JsonProperty("occuredOn") private LocalDateTime occuredOn; @JsonProperty("valueType") private TestEvent.ValueType valueType; @JsonProperty("flags") private Flags flags; @JsonProperty("value") private String value; @JsonIgnore private Map<String, Object> additionalProperties = new LinkedHashMap<String, Object>(); private final static long serialVersionUID = 7311052418845777748L; // Getters ans Setters @Generated("jsonschema2pojo") public enum ValueType { STRING("STRING"), BOOLEAN("BOOLEAN"), INTEGER("INTEGER"), DOUBLE("DOUBLE"); private final String value; private final static Map<String, TestEvent.ValueType> CONSTANTS = new HashMap<String, TestEvent.ValueType>(); static { for (TestEvent.ValueType c: values()) { CONSTANTS.put(c.value, c); } } ValueType(String value) { this.value = value; } @Override public String toString() { return this.value; } @JsonValue public String value() { return this.value; } @JsonCreator public static TestEvent.ValueType fromValue(String value) { TestEvent.ValueType constant = CONSTANTS.get(value); if (constant == null) { throw new IllegalArgumentException(value); } else { return constant; } } } } @JsonInclude(JsonInclude.Include.NON_NULL) @JsonPropertyOrder({ }) @Generated("jsonschema2pojo") public class Flags implements Serializable { @JsonIgnore private Map<String, Boolean> additionalProperties = new LinkedHashMap<String, Boolean>(); private final static long serialVersionUID = 7471055390730117740L; //getters and setters }
それでは、ポリモーフィズム オプションを提供する方法を見てみましょう。これは、複数のメッセージ サブタイプを 1 つのブローカー トピックに送信し、サブタイプごとにリスナーを実装する場合に関連します。
これを行うには、親クラスをプロバイダーのリストに追加し、swagger から @Schema アノテーションをそれに追加する必要があります。
@NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Getter @Setter(AccessLevel.PROTECTED) @EqualsAndHashCode @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true, defaultImpl = ChangedEvent.class ) @JsonSubTypes(value = { @JsonSubTypes.Type(name = ChangedEvent.type, value = ChangedEvent.class), @JsonSubTypes.Type(name = DeletedEvent.type, value = DeletedEvent.class) }) @JsonIgnoreProperties(ignoreUnknown = true) @Schema(oneOf = {ChangedEvent.class, DeletedEvent.class}, discriminatorProperty = "type", discriminatorMapping = { @DiscriminatorMapping(value = ChangedEvent.type, schema = ChangedEvent.class), @DiscriminatorMapping(value = DeletedEvent.type, schema = DeletedEvent.class), }) public abstract class DomainEvent { @Schema(required = true, nullable = false) private String id; @JsonSerialize(using = LocalDateTimeSerializer.class) @JsonDeserialize(using = LocalDateTimeDeserializer.class) @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime occuredOn = LocalDateTime.now(); public abstract String getType(); } /** * Subtype ChangedEvent */ public class ChangedEvent extends DomainEvent implements Serializable { public static final String type = "CHANGED_EVENT"; private String valueId; private String value; } /** * Subtype DeletedEvent */ public class DeletedEvent extends DomainEvent implements Serializable { public static final String type = "DELETED_EVENT"; private String valueId; }
この場合、ドキュメント内のコンポーネントの説明は次のように変更されます。
"components": { "schemas": { "ChangedEvent": { "type": "object", "properties": { "id": { "type": "string", "exampleSetFlag": false }, "occuredOn": { "type": "string", "format": "date-time", "exampleSetFlag": false }, "value": { "type": "string", "exampleSetFlag": false }, "valueId": { "type": "string", "exampleSetFlag": false }, "type": { "type": "string", "exampleSetFlag": false } }, "example": { "id": "string", "occuredOn": "2015-07-20T15:49:04", "value": "string", "valueId": "string", "type": "CHANGED_EVENT" }, "exampleSetFlag": true }, "DeletedEvent": { "type": "object", "properties": { "id": { "type": "string", "exampleSetFlag": false }, "occuredOn": { "type": "string", "format": "date-time", "exampleSetFlag": false }, "valueId": { "type": "string", "exampleSetFlag": false }, "type": { "type": "string", "exampleSetFlag": false } }, "example": { "id": "string", "occuredOn": "2015-07-20T15:49:04", "valueId": "string", "type": "DELETED_EVENT" }, "exampleSetFlag": true }, "DomainEvent": { "type": "object", "properties": { "id": { "type": "string", "exampleSetFlag": false }, "occuredOn": { "type": "string", "format": "date-time", "exampleSetFlag": false }, "type": { "type": "string", "exampleSetFlag": false } }, "example": { "id": "string", "occuredOn": "2015-07-20T15:49:04", "type": "string" }, "discriminator": { "propertyName": "type", "mapping": { "CHANGED_EVENT": "#/components/schemas/ChangedEvent", "DELETED_EVENT": "#/components/schemas/DeletedEvent" } }, "exampleSetFlag": true, "oneOf": [ { "$ref": "#/components/schemas/ChangedEvent", "exampleSetFlag": false }, { "$ref": "#/components/schemas/DeletedEvent", "exampleSetFlag": false } ] } } }
その後、プラグインは oneOf ブロックと記述された識別子からのリンクを考慮します。その結果、次のクラス構造が得られます。
package pckg.test; // import @JsonInclude(JsonInclude.Include.NON_NULL) @JsonPropertyOrder({ "id", "occuredOn", "type" }) @Generated("jsonschema2pojo") @JsonTypeInfo(property = "type", use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, visible = true) @JsonSubTypes({ @JsonSubTypes.Type(name = "CHANGED_EVENT", value = ChangedEvent.class), @JsonSubTypes.Type(name = "DELETED_EVENT", value = DeletedEvent.class) }) public class DomainEvent implements Serializable { @JsonProperty("id") protected String id; @JsonProperty("occuredOn") protected LocalDateTime occuredOn; @JsonProperty("type") protected String type; @JsonIgnore protected Map<String, Object> additionalProperties = new LinkedHashMap<String, Object>(); protected final static long serialVersionUID = 4691666114019791903L; //getters and setters } // import @JsonInclude(JsonInclude.Include.NON_NULL) @JsonPropertyOrder({ "id", "occuredOn", "valueId", "type" }) @Generated("jsonschema2pojo") public class DeletedEvent extends DomainEvent implements Serializable { @JsonProperty("id") private String id; @JsonProperty("occuredOn") private LocalDateTime occuredOn; @JsonProperty("valueId") private String valueId; @JsonProperty("type") private String type; @JsonIgnore private Map<String, Object> additionalProperties = new LinkedHashMap<String, Object>(); private final static long serialVersionUID = 7326381459761013337L; // getters and setters } package pckg.test; //import @JsonInclude(JsonInclude.Include.NON_NULL) @JsonPropertyOrder({ "id", "occuredOn", "value", "type" }) @Generated("jsonschema2pojo") public class ChangedEvent extends DomainEvent implements Serializable { @JsonProperty("id") private String id; @JsonProperty("occuredOn") private LocalDateTime occuredOn; @JsonProperty("value") private String value; @JsonProperty("type") private String type; @JsonIgnore private Map<String, Object> additionalProperties = new LinkedHashMap<String, Object>(); private final static long serialVersionUID = 5446866391322866265L; //getters and setters }
プラグインを接続するには、それを gradle.build ファイルに追加し、パラメーターを指定する必要があります。
フォルダーは DTO を生成するためのものでした
新しいクラスのパッケージ
springwolf ドキュメントの URL
ドキュメントのルート名、通常はサービスの名前
plugins { id 'io.github.stepanovd.springwolf2dto' version '1.0.1-alpha' } springWolfDoc2DTO{ url = 'http://localhost:8080/springwolf/docs' targetPackage = 'example.package' documentationTitle = 'my-service' targetDirectory = project.layout.getBuildDirectory().dir("generated-sources") }
bash コマンドを使用してタスクを実行します。
./gradle -q generateDTO
この記事では、 springwolfdocs2dtoプラグインを使用して、AsyncApi ドキュメントに基づいて新しい DTO クラスを生成する方法について説明しました。同時に、新しいクラスは元の継承に従い、正しい逆シリアル化のための Jackson アノテーションを含みます。このプラグインがお役に立てば幸いです。