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