Hexagonal Architecture with Spring Boot
This commit is contained in:
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
#Maven
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
release.properties
|
||||
|
||||
# Eclipse
|
||||
.project
|
||||
.classpath
|
||||
.settings/
|
||||
bin/
|
||||
|
||||
# IntelliJ
|
||||
.idea
|
||||
*.ipr
|
||||
*.iml
|
||||
*.iws
|
||||
|
||||
# NetBeans
|
||||
nb-configuration.xml
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode
|
||||
.factorypath
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# Vim
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# patch
|
||||
*.orig
|
||||
*.rej
|
||||
|
||||
# Local environment
|
||||
.env
|
||||
84
Hexagonal Architecture.postman_collection.json
Normal file
84
Hexagonal Architecture.postman_collection.json
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "5cddaba1-07d1-4b8e-8574-da160697c2f6",
|
||||
"name": "Hexagonal Architecture",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "Create product",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"name": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\r\n\t\"name\": \"MacBook Pro\",\r\n\t\"description\": \"14-inch MacBook Pro model\"\r\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/v1/products",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"v1",
|
||||
"products"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Find product",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"name": "Content-Type",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "Accept",
|
||||
"value": "application/json",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "http://localhost:8080/v1/products/:id",
|
||||
"protocol": "http",
|
||||
"host": [
|
||||
"localhost"
|
||||
],
|
||||
"port": "8080",
|
||||
"path": [
|
||||
"v1",
|
||||
"products",
|
||||
":id"
|
||||
],
|
||||
"variable": [
|
||||
{
|
||||
"key": "id",
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
}
|
||||
95
pom.xml
Normal file
95
pom.xml
Normal file
@@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.6.3</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>es.rbailen.sample</groupId>
|
||||
<artifactId>hexagonalarchitecture</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>poc-hexagonal_architecture</name>
|
||||
<description>Demo project for Spring Boot</description>
|
||||
<properties>
|
||||
<java.version>11</java.version>
|
||||
<org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
|
||||
<lombok.version>1.18.22</lombok.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct</artifactId>
|
||||
<version>${org.mapstruct.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct-processor</artifactId>
|
||||
<version>${org.mapstruct.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
<compilerArgs>
|
||||
<compilerArg>
|
||||
-Amapstruct.suppressGeneratorTimestamp=true
|
||||
</compilerArg>
|
||||
<compilerArg>
|
||||
-Amapstruct.suppressGeneratorVersionInfoComment=true
|
||||
</compilerArg>
|
||||
<compilerArg>
|
||||
-Amapstruct.defaultComponentModel=spring
|
||||
</compilerArg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,13 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class HexagonalArchitectureApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(HexagonalArchitectureApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture.application.ports.input;
|
||||
|
||||
import es.rbailen.sample.hexagonalarchitecture.domain.model.Product;
|
||||
|
||||
public interface CreateProductUseCase {
|
||||
|
||||
Product createProduct(Product product);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture.application.ports.input;
|
||||
|
||||
import es.rbailen.sample.hexagonalarchitecture.domain.model.Product;
|
||||
|
||||
public interface GetProductUseCase {
|
||||
|
||||
Product getProductById(Long id);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture.application.ports.output;
|
||||
|
||||
import es.rbailen.sample.hexagonalarchitecture.domain.event.ProductCreatedEvent;
|
||||
|
||||
public interface ProductEventPublisher {
|
||||
|
||||
void publishProductCreatedEvent(ProductCreatedEvent event);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture.application.ports.output;
|
||||
|
||||
import es.rbailen.sample.hexagonalarchitecture.domain.model.Product;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ProductOutputPort {
|
||||
|
||||
Product saveProduct(Product product);
|
||||
|
||||
Optional<Product> getProductById(Long id);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture.domain.event;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ProductCreatedEvent {
|
||||
|
||||
private Long id;
|
||||
|
||||
private LocalDateTime date;
|
||||
|
||||
public ProductCreatedEvent(Long id) {
|
||||
this.id = id;
|
||||
this.date = LocalDateTime.now();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture.domain.exception;
|
||||
|
||||
public class ProductNotFound extends RuntimeException {
|
||||
|
||||
public ProductNotFound(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture.domain.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class Product {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture.domain.service;
|
||||
|
||||
import es.rbailen.sample.hexagonalarchitecture.application.ports.input.GetProductUseCase;
|
||||
import es.rbailen.sample.hexagonalarchitecture.application.ports.output.ProductEventPublisher;
|
||||
import es.rbailen.sample.hexagonalarchitecture.application.ports.output.ProductOutputPort;
|
||||
import es.rbailen.sample.hexagonalarchitecture.domain.event.ProductCreatedEvent;
|
||||
import es.rbailen.sample.hexagonalarchitecture.domain.exception.ProductNotFound;
|
||||
import es.rbailen.sample.hexagonalarchitecture.domain.model.Product;
|
||||
import es.rbailen.sample.hexagonalarchitecture.application.ports.input.CreateProductUseCase;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@AllArgsConstructor
|
||||
public class ProductService implements CreateProductUseCase, GetProductUseCase {
|
||||
|
||||
private final ProductOutputPort productOutputPort;
|
||||
|
||||
private final ProductEventPublisher productEventPublisher;
|
||||
|
||||
@Override
|
||||
public Product createProduct(Product product) {
|
||||
product = productOutputPort.saveProduct(product);
|
||||
productEventPublisher.publishProductCreatedEvent(new ProductCreatedEvent(product.getId()));
|
||||
return product;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Product getProductById(Long id) {
|
||||
Optional<Product> product = productOutputPort.getProductById(id);
|
||||
|
||||
if(product.isEmpty()) {
|
||||
throw new ProductNotFound("Product not found with id " + id);
|
||||
}
|
||||
|
||||
return product.get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.config;
|
||||
|
||||
import es.rbailen.sample.hexagonalarchitecture.domain.service.ProductService;
|
||||
import es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.output.eventpublisher.ProductEventPublisherAdapter;
|
||||
import es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.output.persistence.ProductPersistenceAdapter;
|
||||
import es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.output.persistence.mapper.ProductPersistenceMapper;
|
||||
import es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.output.persistence.repository.ProductRepository;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class BeanConfiguration {
|
||||
|
||||
@Bean
|
||||
public ProductPersistenceAdapter productPersistenceAdapter(ProductRepository productRepository, ProductPersistenceMapper productPersistenceMapper) {
|
||||
return new ProductPersistenceAdapter(productRepository, productPersistenceMapper);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ProductEventPublisherAdapter productEventPublisherAdapter(ApplicationEventPublisher applicationEventPublisher) {
|
||||
return new ProductEventPublisherAdapter(applicationEventPublisher);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ProductService productService(ProductPersistenceAdapter productPersistenceAdapter, ProductEventPublisherAdapter productEventPublisherAdapter) {
|
||||
return new ProductService(productPersistenceAdapter, productEventPublisherAdapter);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.input.eventlistener;
|
||||
|
||||
import es.rbailen.sample.hexagonalarchitecture.domain.event.ProductCreatedEvent;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class ProductEventListenerAdapter {
|
||||
|
||||
@EventListener
|
||||
public void handle(ProductCreatedEvent event){
|
||||
log.info("Product created with id " + event.getId() + " at " + event.getDate());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.input.rest;
|
||||
|
||||
import es.rbailen.sample.hexagonalarchitecture.application.ports.input.GetProductUseCase;
|
||||
import es.rbailen.sample.hexagonalarchitecture.domain.model.Product;
|
||||
import es.rbailen.sample.hexagonalarchitecture.application.ports.input.CreateProductUseCase;
|
||||
import es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.input.rest.data.request.ProductCreateRequest;
|
||||
import es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.input.rest.data.response.ProductCreateResponse;
|
||||
import es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.input.rest.data.response.ProductQueryResponse;
|
||||
import es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.input.rest.mapper.ProductRestMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/v1")
|
||||
@RequiredArgsConstructor
|
||||
public class ProductRestAdapter {
|
||||
|
||||
private final CreateProductUseCase createProductUseCase;
|
||||
|
||||
private final GetProductUseCase getProductUseCase;
|
||||
|
||||
private final ProductRestMapper productRestMapper;
|
||||
|
||||
@PostMapping(value = "/products")
|
||||
public ResponseEntity<ProductCreateResponse> createProduct(@RequestBody @Valid ProductCreateRequest productCreateRequest){
|
||||
// Request to domain
|
||||
Product product = productRestMapper.toProduct(productCreateRequest);
|
||||
|
||||
product = createProductUseCase.createProduct(product);
|
||||
|
||||
// Domain to response
|
||||
return new ResponseEntity<>(productRestMapper.toProductCreateResponse(product), HttpStatus.CREATED);
|
||||
}
|
||||
|
||||
@GetMapping(value = "/products/{id}")
|
||||
public ResponseEntity<ProductQueryResponse> getProduct(@PathVariable Long id){
|
||||
Product product = getProductUseCase.getProductById(id);
|
||||
return new ResponseEntity<>(productRestMapper.toProductQueryResponse(product), HttpStatus.OK);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.input.rest.data.request;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ProductCreateRequest {
|
||||
|
||||
@NotEmpty(message = "Name may not be empty")
|
||||
private String name;
|
||||
|
||||
@NotEmpty(message = "Description may not be empty")
|
||||
private String description;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.input.rest.data.response;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ProductCreateResponse {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.input.rest.data.response;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ProductQueryResponse {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.input.rest.mapper;
|
||||
|
||||
import es.rbailen.sample.hexagonalarchitecture.domain.model.Product;
|
||||
import es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.input.rest.data.response.ProductQueryResponse;
|
||||
import es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.input.rest.data.request.ProductCreateRequest;
|
||||
import es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.input.rest.data.response.ProductCreateResponse;
|
||||
import org.mapstruct.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ProductRestMapper {
|
||||
|
||||
Product toProduct(ProductCreateRequest productCreateRequest);
|
||||
|
||||
ProductCreateResponse toProductCreateResponse(Product product);
|
||||
|
||||
ProductQueryResponse toProductQueryResponse(Product product);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.output.customizedexception;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import es.rbailen.sample.hexagonalarchitecture.domain.exception.ProductNotFound;
|
||||
import es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.output.customizedexception.data.response.ExceptionResponse;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.context.request.WebRequest;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
|
||||
|
||||
@ControllerAdvice
|
||||
@RestController
|
||||
public class CustomizedExceptionAdapter extends ResponseEntityExceptionHandler {
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public final ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) {
|
||||
ExceptionResponse exceptionResponse = new ExceptionResponse(LocalDateTime.now(), ex.getMessage(), Arrays.asList(request.getDescription(false)));
|
||||
|
||||
return new ResponseEntity(exceptionResponse, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
@ExceptionHandler(ProductNotFound.class)
|
||||
public final ResponseEntity<Object> handleUserNotFoundException(ProductNotFound ex, WebRequest request) {
|
||||
ExceptionResponse exceptionResponse = new ExceptionResponse(LocalDateTime.now(), ex.getMessage(), Arrays.asList(request.getDescription(false)));
|
||||
|
||||
return new ResponseEntity(exceptionResponse, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
|
||||
List<String> errors = new ArrayList<String>();
|
||||
ex.getBindingResult().getAllErrors().stream().forEach(error -> {
|
||||
errors.add(error.getDefaultMessage());
|
||||
});
|
||||
|
||||
ExceptionResponse exceptionResponse = new ExceptionResponse(LocalDateTime.now(), "Validation Failed", errors);
|
||||
|
||||
return new ResponseEntity(exceptionResponse, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.output.customizedexception.data.response;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ExceptionResponse {
|
||||
|
||||
private LocalDateTime date;
|
||||
|
||||
private String message;
|
||||
|
||||
private List<String> details;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.output.eventpublisher;
|
||||
|
||||
import es.rbailen.sample.hexagonalarchitecture.application.ports.output.ProductEventPublisher;
|
||||
import es.rbailen.sample.hexagonalarchitecture.domain.event.ProductCreatedEvent;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class ProductEventPublisherAdapter implements ProductEventPublisher {
|
||||
|
||||
private final ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
@Override
|
||||
public void publishProductCreatedEvent(ProductCreatedEvent event) {
|
||||
applicationEventPublisher.publishEvent(event);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.output.persistence;
|
||||
|
||||
import es.rbailen.sample.hexagonalarchitecture.application.ports.output.ProductOutputPort;
|
||||
import es.rbailen.sample.hexagonalarchitecture.domain.model.Product;
|
||||
import es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.output.persistence.entity.ProductEntity;
|
||||
import es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.output.persistence.mapper.ProductPersistenceMapper;
|
||||
import es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.output.persistence.repository.ProductRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class ProductPersistenceAdapter implements ProductOutputPort {
|
||||
|
||||
private final ProductRepository productRepository;
|
||||
|
||||
private final ProductPersistenceMapper productPersistenceMapper;
|
||||
|
||||
@Override
|
||||
public Product saveProduct(Product product) {
|
||||
ProductEntity productEntity = productPersistenceMapper.toProductEntity(product);
|
||||
productEntity = productRepository.save(productEntity);
|
||||
return productPersistenceMapper.toProduct(productEntity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Product> getProductById(Long id) {
|
||||
Optional<ProductEntity> productEntity = productRepository.findById(id);
|
||||
|
||||
if(productEntity.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Product product = productPersistenceMapper.toProduct(productEntity.get());
|
||||
return Optional.of(product);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.output.persistence.entity;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public class ProductEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.output.persistence.mapper;
|
||||
|
||||
import es.rbailen.sample.hexagonalarchitecture.domain.model.Product;
|
||||
import es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.output.persistence.entity.ProductEntity;
|
||||
import org.mapstruct.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ProductPersistenceMapper {
|
||||
|
||||
ProductEntity toProductEntity(Product product);
|
||||
|
||||
Product toProduct(ProductEntity productEntity);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.output.persistence.repository;
|
||||
|
||||
import es.rbailen.sample.hexagonalarchitecture.infrastructure.adapters.output.persistence.entity.ProductEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface ProductRepository extends JpaRepository<ProductEntity, Long> {
|
||||
|
||||
}
|
||||
17
src/main/resources/application.yml
Normal file
17
src/main/resources/application.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
logging:
|
||||
level:
|
||||
es.rbailen.sample.hexagonalarchitecture: info
|
||||
|
||||
spring:
|
||||
jpa:
|
||||
show-sql: false
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
datasource:
|
||||
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=- 1;DB_CLOSE_ON_EXIT=FALSE
|
||||
username: test
|
||||
password: test
|
||||
@@ -0,0 +1,13 @@
|
||||
package es.rbailen.sample.hexagonalarchitecture;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class HexagonalArchitectureApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user