paint-brush
Arquitectura impulsada por eventos: generación automática de DTO a partir de documentación de eventospor@dstepanov
2,758 lecturas
2,758 lecturas

Arquitectura impulsada por eventos: generación automática de DTO a partir de documentación de eventos

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

Demasiado Largo; Para Leer

AsyncAPI es una iniciativa de código abierto que busca mejorar el estado actual de la Arquitectura Dirigida por Eventos (EDA) AsyncApi cuenta con varias herramientas que permiten generar documentación a partir de código. En este artículo, me gustaría contarles cómo resolví la siguiente tarea, es decir, la generación de DTO utilizando la documentación JSON que genera springwolf.
featured image - Arquitectura impulsada por eventos: generación automática de DTO a partir de documentación de eventos
Stepanov Dmitrii HackerNoon profile picture


Una cosa muy importante en el proceso de desarrollo de software que a menudo se pasa por alto en las primeras etapas de un proyecto es la documentación de la API. Una de las soluciones a este problema son los frameworks para la generación automática de documentación.


En el caso de dividir el proyecto en microservicios y usar arquitectura Event-Driven, la interacción entre servicios se construye usando eventos transmitidos a través del intermediario de mensajes.


Para generar documentación en el caso de una arquitectura Event-Driven, existe AsyncApi . AsyncAPI es una iniciativa de código abierto que busca mejorar el estado actual de la arquitectura dirigida por eventos (EDA). AsyncApi tiene varias herramientas Java que le permiten generar documentación a partir del código. En este artículo , describí cómo configurar una de estas herramientas springwolf .


En este artículo, me gustaría contarles cómo resolví la siguiente tarea, es decir, la generación de DTO utilizando la documentación JSON que genera springwolf.

Problema

La estructura de documentación que genera spring wolf se ve así:


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


Dado que jsonschema se usa para describir los componentes en la documentación, decidí usar la biblioteca jsonschema2pojo para resolver este problema. Sin embargo, en el proceso de intentar implementar mi plan, me encontré con varios problemas:


  • necesita analizar adicionalmente el documento JSON para extraer objetos que describen los componentes. Dado que jsonschema2pojo toma objetos jsonschema como entrada, están en el bloque de componentes.
  • jsonschema2pojo no funciona bien con el polimorfismo y no maneja las referencias estándar del bloque oneOf que están en AsyncAPI. La descripción de la herencia requiere campos especiales en el esquema (extends.javaType), que no se pueden agregar simplemente a la documentación de AsyncAPI.
  • dado que las clases generadas en nuestro caso deberían usarse para deserializar mensajes del intermediario, es necesario agregar anotaciones Jackson que describan descriptores y subtipos.


Todos estos problemas me llevaron a la necesidad de implementar mi contenedor sobre jsonschema2pojo, que extraerá la información necesaria de la documentación, admitirá polimorfismo y agregará anotaciones de Jackson. El resultado es un complemento de Gradle con el que puede generar clases DTO para su proyecto utilizando la API de springwolf. A continuación, intentaré demostrar cómo anotar clases para la documentación y cómo usar el complemento Springwolfdoc2dto .

Configuración de la documentación

Aquí me gustaría considerar los detalles de cuándo generar tipos no primitivos como Enum y Map. Y también describir las acciones necesarias para el polimorfismo.


Veamos el siguiente mensaje:


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


El jsonschema para dicho mensaje se vería así:


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


Al generar clases DTO, obtendremos la siguiente estructura de clases. Puede ver que Enum se procesa como en la versión original, sin embargo, la colección de tipo Map<String, Boolean> se ha convertido en una clase separada Flags y el valor completo de la colección en sí se incluirá en el campo 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 }

Polimorfismo

Y ahora veamos cómo proporcionar una opción de polimorfismo. Esto es relevante cuando queremos enviar varios subtipos de mensajes a un tema de intermediario e implementar nuestro oyente para cada subtipo.


Para hacer esto, necesitamos agregar una clase principal a la lista de proveedores y agregarle la anotación @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; }


En este caso, la descripción de los componentes en la documentación cambiará de la siguiente manera:


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


Después de eso, el complemento tendrá en cuenta los enlaces del bloque oneOf y los discriminadores descritos. Como resultado, obtenemos la siguiente estructura de clases.


 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 }


Configuración del complemento

Para conectar el complemento, debe agregarlo al archivo gradle.build y especificar los parámetros:

  • carpeta era para generar DTO

  • paquete de nuevas clases

  • URL de documentación de springwolf

  • el nombre raíz en la documentación, generalmente el nombre del servicio


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


Ejecute la tarea usando el comando bash:


 ./gradle -q generateDTO

Conclusión

En este artículo, describí cómo puede usar el complemento springwolfdocs2dto para generar nuevas clases de DTO basadas en la documentación de AsyncApi. Al mismo tiempo, las nuevas clases estarán de acuerdo con la herencia original y contendrán anotaciones de Jackson para una correcta deserialización. Espero que encuentre útil este complemento.