Skip to content

Commit 4e79044

Browse files
authored
Merge pull request #2 from helpdeveloper/gz-openapi-asyncapi
feat: expose openapi and asyncapi
2 parents 7ecdcb2 + f22eb2b commit 4e79044

File tree

10 files changed

+170
-8
lines changed

10 files changed

+170
-8
lines changed

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,28 @@ This approach enhances deployment reliability and maintains a clean separation o
158158

159159
You see the sample how to execute in: [application docker-compose file](.docker-compose-local/application/docker-compose.yml).
160160

161+
### **OpenAPI**
162+
This project uses **Springdoc OpenAPI** to automatically document REST endpoints.
163+
164+
🔗 [Official OpenAPI site](https://swagger.io/specification/)
165+
166+
#### How to access OpenAPI documentation
167+
After starting the application, access:
168+
169+
- **Swagger UI**: [http://localhost:8080/swagger-ui.html](http://localhost:8080/swagger-ui.html)
170+
- **OpenAPI specification in JSON**: [http://localhost:8080/v3/api-docs](http://localhost:8080/v3/api-docs)
171+
172+
### **AsyncAPI**
173+
This project uses **Springwolf** to document asynchronous events (Kafka, RabbitMQ, etc.) with **AsyncAPI**.
174+
175+
🔗 [Official AsyncAPI site](https://www.asyncapi.com/)
176+
177+
#### How to access AsyncAPI documentation
178+
After starting the application, access:
179+
180+
- **AsyncAPI UI**: [http://localhost:8080/springwolf/asyncapi-ui.html](http://localhost:8080/springwolf/asyncapi-ui.html)
181+
- **AsyncAPI specification in JSON**: [http://localhost:8080/springwolf/docs](http://localhost:8080/springwolf/docs)
182+
161183
### **Available Infrastructure**
162184

163185
The local stack also includes infrastructure services to support the application. These services are accessible on `localhost` and provide essential
@@ -200,4 +222,4 @@ _Good software design, as Robert C. Martin emphasizes in his book *Clean Archite
200222
providing a comprehensive perspective on building durable, maintainable architectures._
201223

202224
> *"The only way to go fast, is to go well."*
203-
> **Robert C. Martin**
225+
> **Robert C. Martin**

README.pt.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,31 @@ Essa abordagem melhora a confiabilidade da implantação e mantém uma separaç
146146

147147
Você pode ver um exemplo de como executar em: [arquivo docker-compose da aplicação](.docker-compose-local/application/docker-compose.yml).
148148

149+
### **OpenAPI**
150+
Este projeto utiliza o **Springdoc OpenAPI** para documentar automaticamente os endpoints REST.
151+
152+
🔗 [Site oficial da OpenAPI](https://swagger.io/specification/)
153+
154+
#### Como acessar a documentação OpenAPI
155+
Após iniciar a aplicação, acesse:
156+
157+
- **Swagger UI**: [http://localhost:8080/swagger-ui.html](http://localhost:8080/swagger-ui.html)
158+
- **Especificação OpenAPI em JSON**: [http://localhost:8080/v3/api-docs](http://localhost:8080/v3/api-docs)
159+
160+
### **AsyncAPI**
161+
Este projeto utiliza o **Springwolf** para documentar eventos assíncronos (Kafka, RabbitMQ, etc.) com **AsyncAPI**.
162+
163+
🔗 [Site oficial da AsyncAPI](https://www.asyncapi.com/)
164+
165+
#### Como acessar a documentação AsyncAPI
166+
Após iniciar a aplicação, acesse:
167+
168+
- **AsyncAPI UI**: [http://localhost:8080/springwolf/asyncapi-ui.html](http://localhost:8080/springwolf/asyncapi-ui.html)
169+
- **Especificação AsyncAPI em JSON**: [http://localhost:8080/springwolf/docs](http://localhost:8080/springwolf/docs)
170+
149171
### **Available Infrastructure**
150172

151-
A pilha local também inclui serviços de infraestrutura para dar suporte ao aplicativo. Esses serviços são acessíveis em `localhost` e fornecem funcionalidades
173+
A stack local também inclui serviços de infraestrutura para dar suporte ao aplicativo. Esses serviços são acessíveis em `localhost` e fornecem funcionalidades
152174
essenciais:
153175

154176
- **Grafana**: Visualization and monitoring dashboard, available at [http://localhost:3000](http://localhost:3000).

application/pom.xml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
<spring-boot-dependencies.version>3.4.1</spring-boot-dependencies.version>
1919
<archunit.version>1.3.0</archunit.version>
2020
<mysql-connector-j.version>8.4.0</mysql-connector-j.version>
21+
<springdoc-openapi-starter-webmvc-ui.version>2.8.1</springdoc-openapi-starter-webmvc-ui.version>
22+
<springwolf.version>1.9.0</springwolf.version>
2123
</properties>
2224

2325
<dependencies>
@@ -31,6 +33,25 @@
3133
<artifactId>spring-boot-starter-web</artifactId>
3234
</dependency>
3335

36+
<!-- OpenAPI Dependencies -->
37+
<dependency>
38+
<groupId>org.springdoc</groupId>
39+
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
40+
<version>${springdoc-openapi-starter-webmvc-ui.version}</version>
41+
</dependency>
42+
43+
<!-- AsyncApi Dependencies -->
44+
<dependency>
45+
<groupId>io.github.springwolf</groupId>
46+
<artifactId>springwolf-kafka</artifactId>
47+
<version>${springwolf.version}</version>
48+
</dependency>
49+
<dependency>
50+
<groupId>io.github.springwolf</groupId>
51+
<artifactId>springwolf-ui</artifactId>
52+
<version>${springwolf.version}</version>
53+
</dependency>
54+
3455
<!-- Resilience Dependencies -->
3556
<dependency>
3657
<groupId>org.springframework.cloud</groupId>

application/src/main/java/br/com/helpdev/sample/adapters/input/kafka/UserEventListener.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
import java.util.UUID;
44

5+
import io.github.springwolf.bindings.kafka.annotations.KafkaAsyncOperationBinding;
6+
import io.github.springwolf.core.asyncapi.annotations.AsyncListener;
7+
import io.github.springwolf.core.asyncapi.annotations.AsyncMessage;
8+
import io.github.springwolf.core.asyncapi.annotations.AsyncOperation;
9+
510
import org.slf4j.Logger;
611
import org.slf4j.LoggerFactory;
712
import org.springframework.kafka.annotation.KafkaListener;
@@ -18,6 +23,8 @@
1823
@Controller
1924
class UserEventListener {
2025

26+
private static final String TOPIC_NAME = "user-events";
27+
2128
private final Logger logger = LoggerFactory.getLogger(UserEventListener.class);
2229

2330
private final ObjectMapper objectMapper;
@@ -29,7 +36,21 @@ class UserEventListener {
2936
this.userEnricherPort = userEnricherPort;
3037
}
3138

32-
@KafkaListener(topics = "user-events")
39+
@AsyncListener(operation = @AsyncOperation(
40+
channelName = TOPIC_NAME,
41+
description = "Listen for user events",
42+
message = @AsyncMessage(
43+
name = "UserEventDto",
44+
contentType = "application/json",
45+
messageId = "uuid"
46+
),
47+
headers = @AsyncOperation.Headers(
48+
notUsed = true
49+
),
50+
payloadType = UserEventDto.class
51+
))
52+
@KafkaAsyncOperationBinding(bindingVersion = "1.0.0")
53+
@KafkaListener(topics = TOPIC_NAME)
3354
@RetryableTopic(attempts = "3", backoff = @Backoff(delay = 1000, maxDelay = 10000, multiplier = 2), autoCreateTopics = "true")
3455
public void listen(final String message) throws JsonProcessingException {
3556
final var userEventDto = objectMapper.readValue(message, UserEventDto.class);

application/src/main/java/br/com/helpdev/sample/adapters/input/kafka/dto/UserEventDto.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package br.com.helpdev.sample.adapters.input.kafka.dto;
22

3-
public record UserEventDto(String event, String uuid) {
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
5+
public record UserEventDto(
6+
@Schema(title = "Event", example = "CREATED|UPDATED") String event,
7+
@Schema(title = "UUID", example = "uuid") String uuid)
8+
{
49

510
public static final String EVENT_CREATED = "CREATED";
611

application/src/main/java/br/com/helpdev/sample/adapters/input/rest/UserController.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import java.net.URI;
44
import java.util.UUID;
55

6+
import io.swagger.v3.oas.annotations.Operation;
7+
import io.swagger.v3.oas.annotations.tags.Tag;
8+
69
import org.slf4j.Logger;
710
import org.slf4j.LoggerFactory;
811
import org.springframework.http.ResponseEntity;
@@ -20,6 +23,7 @@
2023
import jakarta.validation.Valid;
2124

2225
@RestController
26+
@Tag(name = "User", description = "User operations")
2327
class UserController {
2428

2529
private final Logger logger = LoggerFactory.getLogger(UserController.class);
@@ -34,13 +38,15 @@ class UserController {
3438
}
3539

3640
@PostMapping("/user")
41+
@Operation(summary = "Create a new user", description = "Create a new user")
3742
public ResponseEntity<?> createUser(@RequestBody @Valid final UserRequestDto userRequestDto) {
3843
final var user = userCreatorPort.createUser(UserRestMapper.toDomain(userRequestDto));
3944
logger.info("User created: {}", user.uuid());
4045
return ResponseEntity.created(URI.create("/user/" + user.uuid())).build();
4146
}
4247

4348
@GetMapping("/user/{uuid}")
49+
@Operation(summary = "Get user by UUID", description = "Get user by UUID")
4450
public ResponseEntity<UserResponseDto> getUser(@PathVariable final UUID uuid) {
4551
final var user = userGetterPort.getUser(uuid);
4652
return ResponseEntity.ok(UserRestMapper.toDto(user));
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package br.com.helpdev.sample.adapters.input.rest.config;
2+
3+
import io.swagger.v3.oas.models.OpenAPI;
4+
import io.swagger.v3.oas.models.info.Contact;
5+
import io.swagger.v3.oas.models.info.Info;
6+
7+
import org.springframework.context.annotation.Bean;
8+
import org.springframework.context.annotation.Configuration;
9+
10+
@Configuration
11+
public class OpenApiConfig {
12+
13+
@Bean
14+
public OpenAPI customOpenAPI() {
15+
return new OpenAPI().info(new Info()
16+
.title("Java Architecture Template")
17+
.version("1.0.0")
18+
.description("""
19+
The application is designed to adhere to Hexagonal Architecture (also known as Ports and Adapters) or Clean Architecture,
20+
ensuring a clear separation of concerns, maintainability, and testability.
21+
Additionally, it includes a dedicated acceptance-test module for integration testing to validate the application's behavior
22+
from an external perspective, independent of its internal implementation.""")
23+
.contact(new Contact().name("Guilherme Biff Zarelli").email("[email protected]")));
24+
25+
}
26+
}

application/src/main/java/br/com/helpdev/sample/adapters/output/kafka/UserEvent.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
import java.util.UUID;
44

5-
public record UserEvent(String event, String uuid) {
5+
import io.swagger.v3.oas.annotations.media.Schema;
6+
7+
public record UserEvent(
8+
@Schema(title = "Event", example = "CREATED|UPDATED") String event,
9+
@Schema(title = "UUID", example = "uuid") String uuid) {
610

711
public static final String EVENT_CREATED = "CREATED";
812

application/src/main/java/br/com/helpdev/sample/adapters/output/kafka/UserEventDispatcher.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package br.com.helpdev.sample.adapters.output.kafka;
22

3+
import io.github.springwolf.bindings.kafka.annotations.KafkaAsyncOperationBinding;
4+
import io.github.springwolf.core.asyncapi.annotations.AsyncMessage;
5+
import io.github.springwolf.core.asyncapi.annotations.AsyncOperation;
6+
import io.github.springwolf.core.asyncapi.annotations.AsyncPublisher;
7+
38
import org.springframework.kafka.core.KafkaTemplate;
49
import org.springframework.stereotype.Service;
510

@@ -19,12 +24,29 @@ class UserEventDispatcher implements UserEventDispatcherPort {
1924

2025
@Override
2126
public void sendUserCreatedEvent(final User user) {
22-
kafkaProducer.send(USER_EVENTS_TOPIC, user.uuid().toString(), UserEvent.ofCreated(user.uuid()).toJson());
27+
publish(user, UserEvent.ofCreated(user.uuid()));
2328
}
2429

2530
@Override
2631
public void sendUserAddressUpdatedEvent(final User user) {
27-
kafkaProducer.send(USER_EVENTS_TOPIC, user.uuid().toString(), UserEvent.ofUpdated(user.uuid()).toJson());
32+
publish(user, UserEvent.ofUpdated(user.uuid()));
33+
}
34+
35+
@AsyncPublisher(
36+
operation = @AsyncOperation(
37+
channelName = USER_EVENTS_TOPIC,
38+
description = "Publish user events",
39+
message = @AsyncMessage(
40+
name = "UserEvent",
41+
contentType = "application/json",
42+
messageId = "uuid"
43+
),
44+
payloadType = UserEvent.class
45+
)
46+
)
47+
@KafkaAsyncOperationBinding(bindingVersion = "1.0.0")
48+
void publish(User user, UserEvent userEvent) {
49+
kafkaProducer.send(USER_EVENTS_TOPIC, user.uuid().toString(), userEvent.toJson());
2850
}
2951

3052
}

application/src/main/resources/application.properties

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
spring.application.name=Java Architecture Template
2+
server.port=${PORT:8080}
23
# Serialization
34
spring.jackson.date-format=yyyy-MM-dd
45
spring.jackson.time-zone=UTC
6+
# OpenApi
7+
springdoc.api-docs.enabled=${SWAGGER_ENABLED:true}
8+
springdoc.swagger-ui.enabled=${SWAGGER_UI_ENABLED:true}
9+
# AsyncApi
10+
springwolf.enabled=${SPRINGWOLF_ENABLED:true}
11+
12+
springwolf.docket.base-package=br.com.helpdev.sample
13+
springwolf.docket.info.title=${spring.application.name}
14+
springwolf.docket.info.version=1.0.0
15+
springwolf.docket.servers.kafka.protocol=kafka
16+
springwolf.docket.servers.kafka.host=${spring.kafka.bootstrap-servers}
17+
518
# Database
619
spring.datasource.url=${DATABASE_URL:jdbc:mysql://localhost:3306/sampledb}
720
spring.datasource.username=${DATABASE_USER:user}
@@ -25,7 +38,7 @@ management.otlp.tracing.endpoint=${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:http://loc
2538
# Kafka
2639
spring.kafka.bootstrap-servers=${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
2740
# Kafka Consumer
28-
spring.kafka.consumer.group-id=${KAFKA_CONSUMER_GROUP_ID:sample-group}
41+
spring.kafka.consumer.group-id=${KAFKA_CONSUMER_GROUP_ID:java-architecture-template@user-events}
2942
spring.kafka.consumer.auto-offset-reset=${KAFKA_CONSUMER_AUTO_OFFSET_RESET:earliest}
3043
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
3144
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer

0 commit comments

Comments
 (0)