paint-brush
Architecture pilotée par les événements : génération automatique de DTO à partir de la documentation des événementspar@dstepanov
2,779 lectures
2,779 lectures

Architecture pilotée par les événements : génération automatique de DTO à partir de la documentation des événements

par Stepanov Dmitrii20m2022/09/22
Read on Terminal Reader
Read this story w/o Javascript

Trop long; Pour lire

AsyncAPI est une initiative open source qui vise à améliorer l'état actuel de l'architecture pilotée par les événements (EDA). AsyncApi dispose de plusieurs outils qui vous permettent de générer de la documentation à partir du code. Dans cet article, je voudrais vous dire comment j'ai résolu la tâche suivante, à savoir la génération de DTO à l'aide de la documentation JSON générée par Springwolf.
featured image - Architecture pilotée par les événements : génération automatique de DTO à partir de la documentation des événements
Stepanov Dmitrii HackerNoon profile picture


Une chose très importante dans le processus de développement logiciel qui est souvent négligée dans les premières étapes d'un projet est la documentation de l'API. L'une des solutions à ce problème réside dans les frameworks pour la génération automatique de documentation.


Dans le cas de la division du projet en microservices et de l'utilisation de l'architecture Event-Driven, l'interaction entre les services est construite à l'aide d'événements transmis via le courtier de messages.


Pour générer de la documentation dans le cas d'une architecture Event-Driven, il existe AsyncApi . AsyncAPI est une initiative open source qui vise à améliorer l'état actuel de l'architecture pilotée par les événements (EDA). AsyncApi dispose de plusieurs outils Java qui vous permettent de générer de la documentation à partir de code. Dans cet article , j'ai décrit comment configurer l'un de ces outils Springwolf .


Dans cet article, je voudrais vous dire comment j'ai résolu la tâche suivante, à savoir la génération de DTO à l'aide de la documentation JSON générée par Springwolf.

Problème

La structure de documentation générée par Spring Wolf ressemble à ceci :


 { "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 } } } } }


Puisque jsonschema est utilisé pour décrire les composants dans la documentation, j'ai décidé d'utiliser la bibliothèque jsonschema2pojo pour résoudre ce problème. Cependant, en essayant de mettre en œuvre mon plan, j'ai rencontré plusieurs problèmes :


  • vous devez également analyser le document JSON pour extraire les objets qui décrivent les composants. Étant donné que jsonschema2pojo prend des objets jsonschema en entrée, ils se trouvent dans le bloc de composants.
  • jsonschema2pojo ne fonctionne pas bien avec le polymorphisme et ne gère pas les références standard du bloc oneOf qui se trouvent dans AsyncAPI. La description de l'héritage nécessite des champs spéciaux dans le schéma (extends.javaType), qui ne peuvent pas être simplement ajoutés à la documentation AsyncAPI.
  • étant donné que les classes générées dans notre cas doivent être utilisées pour désérialiser les messages du courtier, il est nécessaire d'ajouter des annotations Jackson décrivant les descripteurs et les sous-types.


Tous ces problèmes m'ont amené à la nécessité d'implémenter mon wrapper sur jsonschema2pojo, qui extraira les informations nécessaires de la documentation, prendra en charge le polymorphisme et ajoutera des annotations Jackson. Le résultat est un plugin Gradle avec lequel vous pouvez générer des classes DTO pour votre projet en utilisant l'API springwolf. Ensuite, je vais essayer de montrer comment annoter des classes pour la documentation et comment utiliser le plugin Springwolfdoc2dto .

Configuration de la documentation

Ici, je voudrais examiner les spécificités de la génération de types non primitifs tels que Enum et Map. Et décrivez également les actions nécessaires pour le polymorphisme.


Regardons le message suivant :


 @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; } } }


Le jsonschema pour un tel message ressemblerait à ceci :


 { "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 } } } } }


Lors de la génération des classes DTO, nous obtiendrons la structure de classe suivante. Vous pouvez voir que Enum est traité comme dans la version originale, cependant, la collection de type Map<String, Boolean> s'est transformée en une classe distincte Flags et la valeur entière de la collection elle-même tombera dans le champ 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 }

Polymorphisme

Et maintenant, regardons comment fournir une option de polymorphisme. Ceci est pertinent lorsque nous voulons envoyer plusieurs sous-types de messages à un sujet de courtier et implémenter notre écouteur pour chaque sous-type.


Pour ce faire, nous devons ajouter une classe parente à la liste des fournisseurs et y ajouter l'annotation @Schema de swagger.


 @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; }


Dans ce cas, la description des composants dans la documentation changera comme suit :


 "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 } ] } } }


Après cela, le plugin prendra en compte les liens du bloc oneOf et les discriminateurs décrits. En conséquence, nous obtenons la structure de classe suivante.


 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 }


Configuration du plug-in

Pour connecter le plugin, vous devez l'ajouter au fichier gradle.build et spécifier les paramètres :

  • dossier devait générer DTO

  • paquet de nouvelles classes

  • URL de la documentation springwolf

  • le nom racine dans la documentation, généralement le nom du service


 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") }


Exécutez la tâche à l'aide de la commande bash :


 ./gradle -q generateDTO

Conclusion

Dans cet article, j'ai décrit comment vous pouvez utiliser le plugin springwolfdocs2dto pour générer de nouvelles classes DTO basées sur la documentation AsyncApi. Dans le même temps, les nouvelles classes seront conformes à l'héritage d'origine et contiendront des annotations Jackson pour une désérialisation correcte. J'espère que vous trouverez ce plugin utile pour vous.