X Tutup
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,28 @@ This approach enhances deployment reliability and maintains a clean separation o

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

### **OpenAPI**
This project uses **Springdoc OpenAPI** to automatically document REST endpoints.

🔗 [Official OpenAPI site](https://swagger.io/specification/)

#### How to access OpenAPI documentation
After starting the application, access:

- **Swagger UI**: [http://localhost:8080/swagger-ui.html](http://localhost:8080/swagger-ui.html)
- **OpenAPI specification in JSON**: [http://localhost:8080/v3/api-docs](http://localhost:8080/v3/api-docs)

### **AsyncAPI**
This project uses **Springwolf** to document asynchronous events (Kafka, RabbitMQ, etc.) with **AsyncAPI**.

🔗 [Official AsyncAPI site](https://www.asyncapi.com/)

#### How to access AsyncAPI documentation
After starting the application, access:

- **AsyncAPI UI**: [http://localhost:8080/springwolf/asyncapi-ui.html](http://localhost:8080/springwolf/asyncapi-ui.html)
- **AsyncAPI specification in JSON**: [http://localhost:8080/springwolf/docs](http://localhost:8080/springwolf/docs)

### **Available Infrastructure**

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

> *"The only way to go fast, is to go well."*
> – **Robert C. Martin**
> – **Robert C. Martin**
24 changes: 23 additions & 1 deletion README.pt.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,31 @@ Essa abordagem melhora a confiabilidade da implantação e mantém uma separaç

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

### **OpenAPI**
Este projeto utiliza o **Springdoc OpenAPI** para documentar automaticamente os endpoints REST.

🔗 [Site oficial da OpenAPI](https://swagger.io/specification/)

#### Como acessar a documentação OpenAPI
Após iniciar a aplicação, acesse:

- **Swagger UI**: [http://localhost:8080/swagger-ui.html](http://localhost:8080/swagger-ui.html)
- **Especificação OpenAPI em JSON**: [http://localhost:8080/v3/api-docs](http://localhost:8080/v3/api-docs)

### **AsyncAPI**
Este projeto utiliza o **Springwolf** para documentar eventos assíncronos (Kafka, RabbitMQ, etc.) com **AsyncAPI**.

🔗 [Site oficial da AsyncAPI](https://www.asyncapi.com/)

#### Como acessar a documentação AsyncAPI
Após iniciar a aplicação, acesse:

- **AsyncAPI UI**: [http://localhost:8080/springwolf/asyncapi-ui.html](http://localhost:8080/springwolf/asyncapi-ui.html)
- **Especificação AsyncAPI em JSON**: [http://localhost:8080/springwolf/docs](http://localhost:8080/springwolf/docs)

### **Available Infrastructure**

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
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
essenciais:

- **Grafana**: Visualization and monitoring dashboard, available at [http://localhost:3000](http://localhost:3000).
Expand Down
21 changes: 21 additions & 0 deletions application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
<spring-boot-dependencies.version>3.4.1</spring-boot-dependencies.version>
<archunit.version>1.3.0</archunit.version>
<mysql-connector-j.version>8.4.0</mysql-connector-j.version>
<springdoc-openapi-starter-webmvc-ui.version>2.8.1</springdoc-openapi-starter-webmvc-ui.version>
<springwolf.version>1.9.0</springwolf.version>
</properties>

<dependencies>
Expand All @@ -31,6 +33,25 @@
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- OpenAPI Dependencies -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc-openapi-starter-webmvc-ui.version}</version>
</dependency>

<!-- AsyncApi Dependencies -->
<dependency>
<groupId>io.github.springwolf</groupId>
<artifactId>springwolf-kafka</artifactId>
<version>${springwolf.version}</version>
</dependency>
<dependency>
<groupId>io.github.springwolf</groupId>
<artifactId>springwolf-ui</artifactId>
<version>${springwolf.version}</version>
</dependency>

<!-- Resilience Dependencies -->
<dependency>
<groupId>org.springframework.cloud</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

import java.util.UUID;

import io.github.springwolf.bindings.kafka.annotations.KafkaAsyncOperationBinding;
import io.github.springwolf.core.asyncapi.annotations.AsyncListener;
import io.github.springwolf.core.asyncapi.annotations.AsyncMessage;
import io.github.springwolf.core.asyncapi.annotations.AsyncOperation;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.kafka.annotation.KafkaListener;
Expand All @@ -18,6 +23,8 @@
@Controller
class UserEventListener {

private static final String TOPIC_NAME = "user-events";

private final Logger logger = LoggerFactory.getLogger(UserEventListener.class);

private final ObjectMapper objectMapper;
Expand All @@ -29,7 +36,21 @@ class UserEventListener {
this.userEnricherPort = userEnricherPort;
}

@KafkaListener(topics = "user-events")
@AsyncListener(operation = @AsyncOperation(
channelName = TOPIC_NAME,
description = "Listen for user events",
message = @AsyncMessage(
name = "UserEventDto",
contentType = "application/json",
messageId = "uuid"
),
headers = @AsyncOperation.Headers(
notUsed = true
),
payloadType = UserEventDto.class
))
@KafkaAsyncOperationBinding(bindingVersion = "1.0.0")
@KafkaListener(topics = TOPIC_NAME)
@RetryableTopic(attempts = "3", backoff = @Backoff(delay = 1000, maxDelay = 10000, multiplier = 2), autoCreateTopics = "true")
public void listen(final String message) throws JsonProcessingException {
final var userEventDto = objectMapper.readValue(message, UserEventDto.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package br.com.helpdev.sample.adapters.input.kafka.dto;

public record UserEventDto(String event, String uuid) {
import io.swagger.v3.oas.annotations.media.Schema;

public record UserEventDto(
@Schema(title = "Event", example = "CREATED|UPDATED") String event,
@Schema(title = "UUID", example = "uuid") String uuid)
{

public static final String EVENT_CREATED = "CREATED";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import java.net.URI;
import java.util.UUID;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
Expand All @@ -20,6 +23,7 @@
import jakarta.validation.Valid;

@RestController
@Tag(name = "User", description = "User operations")
class UserController {

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

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

@GetMapping("/user/{uuid}")
@Operation(summary = "Get user by UUID", description = "Get user by UUID")
public ResponseEntity<UserResponseDto> getUser(@PathVariable final UUID uuid) {
final var user = userGetterPort.getUser(uuid);
return ResponseEntity.ok(UserRestMapper.toDto(user));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package br.com.helpdev.sample.adapters.input.rest.config;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OpenApiConfig {

@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI().info(new Info()
.title("Java Architecture Template")
.version("1.0.0")
.description("""
The application is designed to adhere to Hexagonal Architecture (also known as Ports and Adapters) or Clean Architecture,
ensuring a clear separation of concerns, maintainability, and testability.
Additionally, it includes a dedicated acceptance-test module for integration testing to validate the application's behavior
from an external perspective, independent of its internal implementation.""")
.contact(new Contact().name("Guilherme Biff Zarelli").email("contato@helpdev.com.br")));

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

import java.util.UUID;

public record UserEvent(String event, String uuid) {
import io.swagger.v3.oas.annotations.media.Schema;

public record UserEvent(
@Schema(title = "Event", example = "CREATED|UPDATED") String event,
@Schema(title = "UUID", example = "uuid") String uuid) {

public static final String EVENT_CREATED = "CREATED";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package br.com.helpdev.sample.adapters.output.kafka;

import io.github.springwolf.bindings.kafka.annotations.KafkaAsyncOperationBinding;
import io.github.springwolf.core.asyncapi.annotations.AsyncMessage;
import io.github.springwolf.core.asyncapi.annotations.AsyncOperation;
import io.github.springwolf.core.asyncapi.annotations.AsyncPublisher;

import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;

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

@Override
public void sendUserCreatedEvent(final User user) {
kafkaProducer.send(USER_EVENTS_TOPIC, user.uuid().toString(), UserEvent.ofCreated(user.uuid()).toJson());
publish(user, UserEvent.ofCreated(user.uuid()));
}

@Override
public void sendUserAddressUpdatedEvent(final User user) {
kafkaProducer.send(USER_EVENTS_TOPIC, user.uuid().toString(), UserEvent.ofUpdated(user.uuid()).toJson());
publish(user, UserEvent.ofUpdated(user.uuid()));
}

@AsyncPublisher(
operation = @AsyncOperation(
channelName = USER_EVENTS_TOPIC,
description = "Publish user events",
message = @AsyncMessage(
name = "UserEvent",
contentType = "application/json",
messageId = "uuid"
),
payloadType = UserEvent.class
)
)
@KafkaAsyncOperationBinding(bindingVersion = "1.0.0")
void publish(User user, UserEvent userEvent) {
kafkaProducer.send(USER_EVENTS_TOPIC, user.uuid().toString(), userEvent.toJson());
}

}
15 changes: 14 additions & 1 deletion application/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
spring.application.name=Java Architecture Template
server.port=${PORT:8080}
# Serialization
spring.jackson.date-format=yyyy-MM-dd
spring.jackson.time-zone=UTC
# OpenApi
springdoc.api-docs.enabled=${SWAGGER_ENABLED:true}
springdoc.swagger-ui.enabled=${SWAGGER_UI_ENABLED:true}
# AsyncApi
springwolf.enabled=${SPRINGWOLF_ENABLED:true}

springwolf.docket.base-package=br.com.helpdev.sample
springwolf.docket.info.title=${spring.application.name}
springwolf.docket.info.version=1.0.0
springwolf.docket.servers.kafka.protocol=kafka
springwolf.docket.servers.kafka.host=${spring.kafka.bootstrap-servers}

# Database
spring.datasource.url=${DATABASE_URL:jdbc:mysql://localhost:3306/sampledb}
spring.datasource.username=${DATABASE_USER:user}
Expand All @@ -25,7 +38,7 @@ management.otlp.tracing.endpoint=${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:http://loc
# Kafka
spring.kafka.bootstrap-servers=${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
# Kafka Consumer
spring.kafka.consumer.group-id=${KAFKA_CONSUMER_GROUP_ID:sample-group}
spring.kafka.consumer.group-id=${KAFKA_CONSUMER_GROUP_ID:java-architecture-template@user-events}
spring.kafka.consumer.auto-offset-reset=${KAFKA_CONSUMER_AUTO_OFFSET_RESET:earliest}
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
Expand Down
X Tutup