Một điều rất quan trọng trong quá trình phát triển phần mềm thường bị bỏ qua trong giai đoạn đầu của một dự án là tài liệu API. Một trong những giải pháp cho vấn đề này là các khuôn khổ để tạo tài liệu tự động.
Trong trường hợp chia dự án thành các microservices và sử dụng kiến trúc Event-Driven, sự tương tác giữa các dịch vụ được xây dựng bằng cách sử dụng các sự kiện được truyền qua message broker.
Để tạo tài liệu trong trường hợp kiến trúc Hướng sự kiện, có AsyncApi . AsyncAPI là một sáng kiến mã nguồn mở nhằm cải thiện trạng thái hiện tại của Kiến trúc theo hướng sự kiện (EDA). AsyncApi có một số công cụ Java cho phép bạn tạo tài liệu từ mã. Trong bài viết này , tôi đã mô tả cách thiết lập một trong những công cụ Springwolf này.
Trong bài viết này, tôi muốn cho bạn biết cách tôi đã giải quyết nhiệm vụ sau, cụ thể là việc tạo DTO bằng cách sử dụng tài liệu JSON mà springwolf tạo ra.
Cấu trúc tài liệu mà spring wolf tạo ra trông giống như sau:
{ "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 } } } } }
Vì jsonschema được sử dụng để mô tả các thành phần trong tài liệu, tôi quyết định sử dụng thư viện jsonschema2pojo để giải quyết vấn đề này. Tuy nhiên, trong quá trình cố gắng thực hiện kế hoạch của mình, tôi đã gặp phải một số vấn đề:
Tất cả những vấn đề này đã khiến tôi phải triển khai trình bao bọc của mình trên jsonschema2pojo, sẽ trích xuất thông tin cần thiết từ tài liệu, hỗ trợ đa hình và thêm chú thích Jackson. Kết quả là một plugin Gradle mà bạn có thể tạo các lớp DTO cho dự án của mình bằng cách sử dụng API springwolf. Tiếp theo, tôi sẽ cố gắng trình bày cách chú thích các lớp cho tài liệu và cách sử dụng plugin Springwolfdoc2dto .
Ở đây tôi muốn xem xét các chi tiết cụ thể về thời gian tạo cho các kiểu không nguyên thủy như Enum và Map. Và cũng mô tả các hành động cần thiết cho đa hình.
Hãy xem thông báo sau:
@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 cho một thông báo như vậy sẽ giống như sau:
{ "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 } } } } }
Khi tạo các lớp DTO, chúng ta sẽ nhận được cấu trúc lớp như sau. Bạn có thể thấy rằng Enum được xử lý như trong phiên bản gốc, tuy nhiên, tập hợp kiểu Map <String, Boolean> đã chuyển thành một lớp Flags riêng biệt và toàn bộ giá trị của bản thân tập hợp sẽ rơi vào trường 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 }
Và bây giờ hãy xem cách cung cấp tùy chọn đa hình. Điều này có liên quan khi chúng ta muốn gửi một số kiểu phụ thông báo đến một chủ đề môi giới và triển khai trình lắng nghe của chúng ta cho mỗi kiểu phụ.
Để làm điều này, chúng ta cần thêm một lớp cha vào danh sách các nhà cung cấp và thêm chú thích @Schema từ swagger vào nó.
@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; }
Trong trường hợp này, mô tả của các thành phần trong tài liệu sẽ thay đổi như sau:
"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 } ] } } }
Sau đó, plugin sẽ tính đến các liên kết từ khối oneOf và các yếu tố phân biệt được mô tả. Kết quả là, chúng tôi nhận được cấu trúc lớp sau.
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 }
Để kết nối plugin, bạn cần thêm nó vào tệp gradle.build và chỉ định các tham số:
thư mục được tạo DTO
gói các lớp học mới
URL tài liệu springwolf
tên gốc trong tài liệu, thường là tên của dịch vụ
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") }
Chạy tác vụ bằng lệnh bash:
./gradle -q generateDTO
Trong bài viết này, tôi đã mô tả cách bạn có thể sử dụng plugin springwolfdocs2dto để tạo các lớp DTO mới dựa trên tài liệu AsyncApi. Đồng thời, các lớp mới sẽ theo kế thừa ban đầu và chứa các chú thích Jackson để giải thích đúng. Tôi hy vọng bạn thấy plugin này hữu ích cho bạn.