Java

[MSA] e-commerce API; 구현2.4 - order

ride-dev 2024. 6. 19. 16:58

[MSA] e-commerce API; 프로젝트 흐름

[MSA] e-commerce API; 구현1 - 설정(config-server, eureka-server, gateway)

[MSA] e-commerce API; 구현2 - customer, product, payment, order, notification

(앞선 게시글에 작성한 내용은 생략할 예정입니다)

 

작성한 ERD를 참조하여 구현합니다.

Order

e-commerce API 의 중심이 되는 도메인입니다.

고객(Customer)이 제품(Product)을 주문(Order)하고 구매(Payment)합니다.

주문에는 하위 항목(OrderLine)이 존재하며, 각 항목별로 결제(Purchase)가 요청됩니다.

order는 HttpClient를 통해 다른 서비스에게 요청하고 응답을 받아옵니다.

HttpClient의 종류는 다양합니다.

아주 기본적인 HttpURLConnection부터 ApacheHttpClient, RestTemplate, OpenFeign, WebClient, RestClient가 있습니다.

이 프로젝트에서는 Spring Cloud에서 주로 쓰이는 OpenFiegn과,

Spring이 가장 최근에 도입한 RestClient를 사용합니다.

 

중요한 것은 일관성을 유지하여 유지보수를 편하게 하는 것이지만,

학습을 위해 두 Http Client 모두 작성해보도록 하겠습니다.

 

일관성이 중요한 데이터이므로 RDBMS에 저장합니다.

(RDBMS는 postgreSQL을 사용했습니다)

 

아래는 order프로젝트의 의존성입니다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'io.micrometer:micrometer-tracing-bridge-brave'
    implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
    implementation 'org.springframework.cloud:spring-cloud-starter-config'
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
    implementation 'org.springframework.kafka:spring-kafka'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'org.postgresql:postgresql'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

주요 의존성은 openfeign으로, spring cloud에 특화된 HTTP Client입니다.

복잡한 기능을 구현하는 데에 제약이 있으나,

인터페이스를 활용하여 HTTP 통신을 선언형으로 간단하게 구현할 수 있습니다.

 

order의 설정파일인 application.yml은 아래와 같습니다.

spring:
  application:
    name: order-service
  config:
    import: optional:configserver:http://localhost:8888

config-server의 product-service.yml은 아래와 같습니다.

server:
  port: 8070
spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://localhost:5432/order
    username: ride
    password: password
  jpa:
    hibernate:
      ddl-auto: update
    database: postgresql
    database-platform: org.hibernate.dialect.PostgreSQLDialect
  kafka:
    producer:
      bootstrap-servers: localhost:9092
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
      properties:
        spring.json.type.mapping: orderConfirmation:com.ride.ecommerce.kafka.OrderConfirmation

application:
  config:
    customer-url: http://localhost:8222/api/v1/customers
    payment-url: http://localhost:8222/api/v1/payments
    product-url: http://localhost:8222/api/v1/products

application.config 설정을 통해 각 서비스의 url을 등록했습니다.order 프로젝트에서 이를 참조할 수 있습니다.

아래는 order 도메인이 사용하는 DB입니다.

 

OpenFeign을 사용하기 위해 main class에 @EnableFeignClients 를 추가합니다.

JpaAuditing을 사용하기 위해 @EnableJpaAuditing 을 추가합니다.

package com.ride.ecommerce;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableFeignClients
@EnableJpaAuditing
public class OrderApplication {

    public static void main(String[] args) {
       SpringApplication.run(OrderApplication.class, args);
    }

}

1.0 디렉토리 구조

order의 디렉토리 구조는 아래와 같습니다.

config에 RestClient를,

customer, payment, product 디렉토리에 각각 HttpClient관련 파일 및 DTO를 생성합니다.

order, orderLine 디렉토리에 order, order-line에대한 코드를 작성합니다.

1.1 config

1.1.1 RestClientConfig

RestClient, WebClient, RestTemplate의 HttpClient을 Spring Bean으로 관리하지 않으면 필요하지 않습니다.

package com.ride.ecommerce.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;

@Configuration
public class RestClientConfig {
    @Bean
    public RestClient restClient() {
        return RestClient.create();
    }
}

현재 인터셉터 설정이 따로 필요하지 않기 때문에 Spring Bean으로 등록하는 것 이외에 따로 작성한 코드는 없습니다.

1.2 order

1.2.1 Order class, PaymentMethod enum

package com.ride.ecommerce.order;

import com.ride.ecommerce.orderLine.OrderLine;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;

import static jakarta.persistence.EnumType.STRING;

@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "customer_order")
public class Order {
    @Id
    @GeneratedValue
    private Integer id;
    private String reference;
    private BigDecimal totalAmount;
    @Enumerated(STRING)
    private PaymentMethod paymentMethod;
    private String customerId;
    @OneToMany(mappedBy = "order")
    private List<OrderLine> orderLines;
    @CreatedDate
    @Column(updatable = false, nullable = false)
    private LocalDateTime createdDate;
    @LastModifiedDate
    @Column(insertable = false)
    private LocalDateTime lastModifiedDate;
}
package com.ride.ecommerce.order;

public enum PaymentMethod {
    PAYPAL,
    CREDIT_CARD,
    VISA,
    MASTER_CARD,
    BITCOIN
}

1.2.2 OrderController class

package com.ride.ecommerce.order;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * REST controller for mapping orders
 */
@RestController
@RequestMapping("/api/v1/orders")
@RequiredArgsConstructor
public class OrderController {

    private final OrderService service;

    /**
     * POST /api/v1/orders : Create a new Order.
     * @param request the order request containing order details
     * @return the ResponseRntity with status 200 (OK) and the ID of the created order
     */
    @PostMapping
    public ResponseEntity<Integer> createOrder(
            @RequestBody @Valid OrderRequest request
    ) {
        return ResponseEntity.ok(service.createOrder(request));
    }

    /**
     * GET /api/v1/orders
     * 
     * @return the ResponseEntity with status 200 (OK) and the list of orders
     */
    @GetMapping
    public ResponseEntity<List<OrderResponse>> findAll() {
        return ResponseEntity.ok(service.findAll());
    }

    /**
     * GET /api/v1/orders/{order-id} : Get an order by ID.
     * 
     * @param orderId the ID of the order to retrieve
     * @return the ResponseEntity with status 200 (OK) and the order details
     */
    @GetMapping("/{order-id}")
    public ResponseEntity<OrderResponse> findById(
            @PathVariable("order-id") Integer orderId
    ) {
        return ResponseEntity.ok(service.findById(orderId));
    }
}

1.2.3 OrderRequest, OrderResponse record

package com.ride.ecommerce.order;

import com.ride.ecommerce.product.PurchaseRequest;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;

import java.math.BigDecimal;
import java.util.List;

public record OrderRequest(
        Integer id,
        String reference,
        @Positive(message = "Order amount should be positive")
        BigDecimal amount,
        @NotNull(message = "Payment method should be precised")
        PaymentMethod paymentMethod,
        @NotNull(message = "Customer should be present")
        @NotEmpty(message = "Customer should be present")
        @NotBlank(message = "Customer should be present")
        String customerId,
        @NotEmpty(message = "You should at least purchase one product")
        List<PurchaseRequest> products
) {
}
package com.ride.ecommerce.order;

import java.math.BigDecimal;

public record OrderResponse(
        Integer id,
        String reference,
        BigDecimal amount,
        PaymentMethod paymentMethod,
        String customerId
) {
}

1.2.4 OrderService, OrderMapper class

package com.ride.ecommerce.order;

import com.ride.ecommerce.customer.CustomerClient;
import com.ride.ecommerce.exeption.BusinessException;
import com.ride.ecommerce.orderLine.OrderLineRequest;
import com.ride.ecommerce.orderLine.OrderLineService;
import com.ride.ecommerce.payment.PaymentClient;
import com.ride.ecommerce.payment.PaymentRequest;
import com.ride.ecommerce.product.ProductClient;
import com.ride.ecommerce.product.PurchaseRequest;
import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

/**
 * Service class for managing orders
 */
@Service
@RequiredArgsConstructor
public class OrderService {
    
    private final OrderRepository repository;
    private final CustomerClient customerClient;
    private final ProductClient productClient;
    private final OrderMapper mapper;
    private final OrderLineService orderLineService;

    private final PaymentClient paymentClient;

    /**
     * Creates a new Order.
     * @param request the order request containing order details
     * @return the ID of the created order
     */
    public Integer createOrder(OrderRequest request) {
        // check the customer --> openFeign
        var customer = this.customerClient.findCustomerById(request.customerId())
                .orElseThrow(() -> new BusinessException("Cannot create order:: No Customer exists with the provided ID"));
        // purchase the products --> product-ms (RestTemplate, RestClient)
        var purchasedProducts = this.productClient.purchaseProducts(request.products());
        // persist order
        var order = this.repository.save(mapper.toOrder(request));
        //persist order lines
        for (PurchaseRequest purchaseRequest : request.products()) {
            orderLineService.saveOrderLine(
                    new OrderLineRequest(
                            null,
                            order.getId(),
                            purchaseRequest.productId(),
                            purchaseRequest.quantity()
                    )
            );
        }
        // start payment process
        var paymentRequest = new PaymentRequest(
                request.amount(),
                request.paymentMethod(),
                order.getId(),
                order.getReference(),
                customer
        );
        paymentClient.requestOrderPayment(paymentRequest);

        // send the order confirmation --> notification-ms (kafka)

        return order.getId();
    }

    /**
     * Finds all orders.
     * 
     * @return the list of order responses containing details of all orders
     */
    public List<OrderResponse> findAll() {
        return repository.findAll()
                .stream()
                .map(mapper::fromOrder)
                .collect(Collectors.toList());
    }

    /**
     * Finds an order by its ID.
     * 
     * @param orderId the ID of the order to retrieve
     * @return the order response containing order details
     * @throws EntityNotFoundException if no order is found with the given ID
     */
    public OrderResponse findById(Integer orderId) {
        return repository.findById(orderId)
                .map(mapper::fromOrder)
                .orElseThrow(() -> new EntityNotFoundException(String.format("No order found with the provided ID: %d", orderId)));
    }
}
package com.ride.ecommerce.order;

import org.springframework.stereotype.Service;

/**
 * Mapper class for converting between OrderRequest/OrderResponse DTOs and Order entity.
 */
@Service
public class OrderMapper {

    /**
     * Converts an OrderRequest DTO to an Order entity
     * 
     * @param request the order request with customer details
     * @return the Order entity
     */
    public Order toOrder(OrderRequest request) {
        return Order.builder()
                .id(request.id())
                .customerId(request.customerId())
                .reference(request.reference())
                .totalAmount(request.amount())
                .paymentMethod(request.paymentMethod())
                .build();
    }

    /**
     * Converts an Order entity to an OrderResponse DTO.
     * 
     * @param order the Order entity
     * @return the OrderResponse DTO
     */
    public OrderResponse fromOrder(Order order) {
        return new OrderResponse(
                order.getId(),
                order.getReference(),
                order.getTotalAmount(),
                order.getPaymentMethod(),
                order.getCustomerId()
        );
    }
}

1.2.5 OrderRepository interface

package com.ride.ecommerce.order;

import org.springframework.data.jpa.repository.JpaRepository;

public interface OrderRepository extends JpaRepository<Order, Integer> {
}

1.3 orderLine

1.3.1 OrderLine class

package com.ride.ecommerce.orderLine;

import com.ride.ecommerce.order.Order;
import jakarta.persistence.*;
import lombok.*;

@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
@Entity
public class OrderLine {
    @Id
    @GeneratedValue
    private Integer id;
    @ManyToOne
    @JoinColumn(name = "order_id")
    private Order order;
    private Integer productId;
    private double quantity;
}

Order와 다대일 관계로, OrderLine이 연관관계의 주인입니다.

1.3.2 OrderLineController class

package com.ride.ecommerce.orderLine;

import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * REST controller for managing order lines.
 */
@RestController
@RequestMapping("/api/v1/order-line")
@RequiredArgsConstructor
public class OrderLineController {
    
    private final OrderLineService service;

    /**
     * GET /api/v1/order-line/order/{order-id} : Get all order lines by order ID.
     * 
     * @param orderId the ID of the order to retrieve order lines for
     * @return the ResponseEntity with status 200 (OK) and the list of order lines
     */
    @GetMapping("/order/{order-id}")
    public ResponseEntity<List<OrderLineResponse>> findByOrderId(
            @PathVariable("order-id") Integer orderId
    ) {
        return ResponseEntity.ok(service.findAllByOrderId(orderId));
    }
}

1.3.3 OrderLineRequest, OrderLineResponse record

package com.ride.ecommerce.orderLine;

public record OrderLineRequest(
        Integer id,
        Integer orderId,
        Integer productId,
        double quantity
) {
}
package com.ride.ecommerce.orderLine;

public record OrderLineResponse(
        Integer id,
        double quantity
) {
}

1.3.4 OrderLineService, OrderLineMapper class

package com.ride.ecommerce.orderLine;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

import static java.util.stream.Collectors.toList;

/**
 * Service class for managing order lines.
 */
@Service
@RequiredArgsConstructor
public class OrderLineService {
    
    private final OrderLineRepository repository;
    private final OrderLineMapper mapper;

    /**
     * Saves a new order line.
     * 
     * @param request the order line request containing order line details
     * @return the ID of the saved order line
     */
    public Integer saveOrderLine(OrderLineRequest request) {
        var order = mapper.toOrderLine(request);
        return repository.save(order).getId();
    }

    /**
     * Finds all order lines by order ID.
     * 
     * @param orderId the ID of the order to retrieve order lines for
     * @return the list of order line responses
     */
    public List<OrderLineResponse> findAllByOrderId(Integer orderId) {
        return repository.findAllByOrderId(orderId)
                .stream()
                .map(mapper::toOrderLineResponse)
                .collect(toList());
    }
}
package com.ride.ecommerce.orderLine;

import com.ride.ecommerce.order.Order;
import org.springframework.stereotype.Service;

/**
 * Mapper class for converting between OrderLineRequest/OrderLineResponse DTOs and OrderLine entity.
 */
@Service
public class OrderLineMapper {

    /**
     * Converts an OrderLineRequest DTO to and OrderLine entity.
     * 
     * @param request request the order line request containing order line details
     * @return the OrderLine entity
     */
    public OrderLine toOrderLine(OrderLineRequest request) {
        return OrderLine.builder()
                .id(request.id())
                .order(
                        Order.builder()
                                .id(request.orderId())
                                .build())
                .productId(request.productId())
                .quantity(request.quantity())
                .build();
    }

    /**
     * Converts an OrderLine entity to and OrderLineResponse DTO.
     * 
     * @param orderLine the OrderLine entity
     * @return the OrderLineResponse DTO
     */
    public OrderLineResponse toOrderLineResponse(OrderLine orderLine) {
        return new OrderLineResponse(
                orderLine.getId(),
                orderLine.getQuantity());
    }
}

1.3.5 OrderLineRepository interface

package com.ride.ecommerce.customer;

import org.springframework.data.mongodb.repository.MongoRepository;

public interface CustomerRepository extends MongoRepository<Customer, String > {
}

1.4 customer

1.4.1 CustomerResponse record

package com.ride.ecommerce.customer;

public record CustomerResponse(
        String id,
        String firstname,
        String lastname,
        String email
) {
}

1.4.2 CustomerClient interface

package com.ride.ecommerce.customer;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import java.util.Optional;

@FeignClient(
        name = "customer-service",
        url = "${application.config.customer-url}"
)
public interface CustomerClient {

    @GetMapping("/{customer-id}")
    Optional<CustomerResponse> findCustomerById(@PathVariable("customer-id") String customerId);
}

 OpenFeign을 활용하여 HTTP 통신합니다.

@FeignClient 어노테이션을 입력합니다.

config-server의 order-service.yml에 설정한 데이터를 참조할 수 있도록 합니다.

Optional<CustomerResponse> 형식으로 반환받습니다.

아래는 CustomerClient가 통신할 customer-service의 Get 메서드입니다.

/**
 * GET /api/v1/customers/{customer-id} : Get a customer by id.
 *
 * @param customerId the id of the customer to retrieve
 * @return the ResponseEntity with status 200 (OK) and the customer details
 */
@GetMapping("/{customer-id}")
public ResponseEntity<CustomerResponse> findById(
        @PathVariable("customer-id") String customerId
) {
    return ResponseEntity.ok(service.findById(customerId));
}

1.5 product

1.5.1 PurchaseRequest, PurchaseResponse record

package com.ride.ecommerce.product;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;

public record PurchaseRequest(
        @NotNull(message = "Product is mandatory")
        Integer productId,
        @Positive(message = "Quantity is mandatory")
        double quantity
) {
}
package com.ride.ecommerce.product;

import java.math.BigDecimal;

public record PurchaseResponse(
        Integer productId,
        String name,
        String description,
        BigDecimal price,
        double quantity
) {
}

1.5.2 ProductClient class(OpenFeign 구현 시 interface)

위의 customerClient와 다르게 RestClient를 적용했습니다.

product-service와 통신하여 List<PurchaseResponse> 객체를 받아옵니다.

HttpInterface를 활용하여 선언형으로 사용하는 것은 최적화 포스팅에서 다루겠습니다.

package com.ride.ecommerce.product;

import com.ride.ecommerce.exeption.BusinessException;
import lombok.RequiredArgsConstructor;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

import java.util.List;

import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

@Service
@RequiredArgsConstructor
@Slf4j
public class ProductClient {
    @Value("${application.config.product-url}")
    private String productUrl;
    private final RestClient restClient;

    public List<PurchaseResponse> purchaseProducts(List<PurchaseRequest> requestBody) {
        HttpHeaders headers = new HttpHeaders();
        headers.set(CONTENT_TYPE, APPLICATION_JSON_VALUE);
        ParameterizedTypeReference<List<PurchaseResponse>> responseType =
                new ParameterizedTypeReference<>() {
                };
        ResponseEntity<List<PurchaseResponse>> entity = restClient.post()
                .uri(productUrl + "/purchase")
                .headers(httpHeaders -> httpHeaders.addAll(headers))
                .body(requestBody)
                .retrieve()
                .onStatus(HttpStatusCode::isError, (request, response) -> {
                    log.info("request uri : {}", request.getURI());
                    throw new BusinessException("An error occurred while processing the products purchase: " + response.getStatusCode());
                })
                .toEntity(responseType);
        return entity.getBody();
    }
}

1.6 payment

1.6.1 PaymentRequest record

package com.ride.ecommerce.payment;

import com.ride.ecommerce.customer.CustomerResponse;
import com.ride.ecommerce.order.PaymentMethod;

import java.math.BigDecimal;

public record PaymentRequest(
        BigDecimal amount,
        PaymentMethod paymentMethod,
        Integer orderId,
        String orderReference,
        CustomerResponse customer
) {
}

1.6.2 PaymentClient interface

package com.ride.ecommerce.payment;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@FeignClient(
        name = "product-service",
        url = "${application.config.payment-url}"
)
public interface PaymentClient {

    @PostMapping
    Integer requestOrderPayment(@RequestBody PaymentRequest request);
}

1.7 exception

1.7.1 BusinessException class

package com.ride.ecommerce.exeption;

import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * Custom exception class for handling business logic related exceptions.
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class BusinessException extends RuntimeException {
    /**
     * The message describing the exception.
     */
    private final String msg;
}

1.8 handler

1.8.1 GlobalExeptionHandler class

package com.ride.ecommerce.handler;

import com.ride.ecommerce.exeption.BusinessException;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;

import static org.springframework.http.HttpStatus.BAD_REQUEST;

/**
 * Global exception handler for handling various exceptions across the application.
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<String> handle(BusinessException exp) {
        return ResponseEntity
                .status(BAD_REQUEST)
                .body(exp.getMsg());
    }

    /**
     * Handles BusinessException and returns a 400 BAD REQUEST response.
     *
     * @param exp the BusinessException instance
     * @return a ResponseEntity with a 400 status and the exception message
     */
    @ExceptionHandler(EntityNotFoundException.class)
    public ResponseEntity<String> handle(EntityNotFoundException exp) {
        return ResponseEntity
                .status(BAD_REQUEST)
                .body(exp.getMessage());
    }

    /**
     * Handles EntityNotFoundException and returns a 400 BAD REQUEST response.
     *
     * @param exp the EntityNotFoundException instance
     * @return a ResponseEntity with a 400 status and the exception message
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handle(MethodArgumentNotValidException exp) {
        var errors = new HashMap<String, String>();
        exp.getBindingResult().getAllErrors()
                .forEach(error -> {
                    var fieldName = ((FieldError)error).getField();
                    var errorMessage = error.getDefaultMessage();
                    errors.put(fieldName, errorMessage);
                });

        return ResponseEntity
                .status(BAD_REQUEST)
                .body(new ErrorResponse(errors));
    }
}

1.8.1 ErrorResponse record

package com.ride.ecommerce.handler;

import java.util.Map;

public record ErrorResponse(
        Map<String, String> errors
) {
}
728x90