public insterface Publisher<T>{
public void subscribe(Subscriber<? super T> s);
}
public interface Subscriber<T> {
public void onSubscribe(Subscription s);
public void onNext(T t);
public void onError(Throwable t);
public void onComplete();
}
public interface Subscription{
public void request(long n);
public void cancel();
}
public interface Process<T, R> extends Subscriber<T>, Publisher<R> {
}
RaouterFunction<ServerResponse> route = route()
.GET("/v1/api/orders/{id}", accept(APPLICATION_JSON), handler::getOrderById)
.POST("/v1/api/orders", handler::addOrder)
.build();
{"library": "spring-boot",
...
"reactive":true,
...
}
return mono.error(() -> new RuntimeException("Error Msg"));
Mono<List<String>> ids = itemRepo.findById(id)
.switchIfEmpty(Mono.error(new ResourceNotFoundException("Error Msg")))
.map(i -> i.getId())
.collectList();
return service.getCartByCustomId(id)
.onErrorReturn(notFound().build())
@Component
public class ApiErrorAttributes extends DefaultErrorAttributes {
private HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
private String message = ErrorCode.GENERIC_ERROR.getErrMsgKey();
@Override
public Map<String, Object> getErrorAttributes(ServerRequest request,
ErrorAttributeOptions options) {
var attributes = super.getErrorAttributes(request, options);
attributes.put("status", status);
attributes.put("message", message);
attributes.put("code", ErrorCode.GENERIC_ERROR.getErrCode());
return attributes;
}
public HttpStatus getStatus() {
return status;
}
public ApiErrorAttributes setStatus(HttpStatus status) {
this.status = status;
return this;
}
public String getMessage() {
return message;
}
public ApiErrorAttributes setMessage(String message) {
this.message = message;
return this;
}
}
@Component
@Order(-2)
public class ApiErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
public ApiErrorWebExceptionHandler(ApiErrorAttributes errorAttributes,
ApplicationContext applicationContext,
ServerCodecConfigurer serverCodecConfigurer) {
super(errorAttributes, new WebProperties().getResources(), applicationContext);
super.setMessageWriters(serverCodecConfigurer.getWriters());
super.setMessageReaders(serverCodecConfigurer.getReaders());
}
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
}
private Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
Map<String, Object> errorPropertiesMap = getErrorAttributes(request,
ErrorAttributeOptions.defaults());
Throwable throwable = (Throwable) request
.attribute("org.springframework.boot.web.reactive.error.DefaultErrorAttributes.ERROR")
.orElseThrow(
() -> new IllegalStateException("Missing exception attribute in ServerWebExchange"));
throwable.printStackTrace();
ErrorCode errorCode = ErrorCode.GENERIC_ERROR;
if (throwable instanceof IllegalArgumentException
|| throwable instanceof DataIntegrityViolationException
|| throwable instanceof ServerWebInputException) {
errorCode = ILLEGAL_ARGUMENT_EXCEPTION;
} else if (throwable instanceof CustomerNotFoundException) {
errorCode = CUSTOMER_NOT_FOUND;
}
...
switch (errorCode) {
case ILLEGAL_ARGUMENT_EXCEPTION -> {
errorPropertiesMap.put("status", HttpStatus.BAD_REQUEST);
errorPropertiesMap.put("code", ILLEGAL_ARGUMENT_EXCEPTION.getErrCode());
errorPropertiesMap.put("error", ILLEGAL_ARGUMENT_EXCEPTION);
errorPropertiesMap.put("message", String
.format("%s %s", ILLEGAL_ARGUMENT_EXCEPTION.getErrMsgKey(),
throwable.getMessage()));
return ServerResponse.status(HttpStatus.BAD_REQUEST)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(errorPropertiesMap));
}
...
default -> ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(errorPropertiesMap));
}
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(errorPropertiesMap));
}
}
public interface HateoasSupport {
default UriComponentsBuilder getUriComponentBuilder(@Nullable ServerWebExchange exchange) {
if (exchange == null) {
return UriComponentsBuilder.fromPath("/");
}
ServerHttpRequest request = exchange.getRequest();
PathContainer contextPath = request.getPath().contextPath();
return UriComponentsBuilder.fromHttpRequest(request)
.replacePath(contextPath.toString())
.replaceQuery("");
}
}
@Component
public class AddressRepresentationModelAssembler implements
ReactiveRepresentationModelAssembler<AddressEntity, Address>, HateoasSupport {
private static String serverUri = null;
private String getServerUri(@Nullable ServerWebExchange exchange) {
if (Strings.isBlank(serverUri)) {
serverUri = getUriComponentBuilder(exchange).toUriString();
}
return serverUri;
}
@Override
public Mono<Address> toModel(AddressEntity entity, ServerWebExchange exchange) {
return Mono.just(entityToModel(entity, exchange));
}
public Address entityToModel(AddressEntity entity, ServerWebExchange exchange) {
Address resource = new Address();
if(Objects.isNull(entity)) {
return resource;
}
BeanUtils.copyProperties(entity, resource);
resource.setId(entity.getId().toString());
String serverUri = getServerUri(exchange);
resource.add(Link.of(String.format("%s/api/v1/addresses", serverUri)).withRel("addresses"));
resource.add(
Link.of(String.format("%s/api/v1/addresses/%s", serverUri, entity.getId())).withSelfRel());
return resource;
}
public Address getModel(Mono<Address> m) {
AtomicReference<Address> model = new AtomicReference<>();
m.cache().subscribe(i -> model.set(i));
return model.get();
}
public Flux<Address> toListModel(Flux<AddressEntity> entities, ServerWebExchange exchange) {
if (Objects.isNull(entities)) {
return Flux.empty();
}
return Flux.from(entities.map(e -> entityToModel(e, exchange)));
}
}
@Table("ecomm.orders")
public class OrderEntity {
@Id
@Column("id")
private UUID id;
@Column("customer_id")
private UUID customerId;
@Column("address_id")
private UUID addressId;
@Column("card_id")
private UUID cardId;
@Column("order_date")
private Timestamp orderDate;
@Column("total")
private BigDecimal total;
@Column("payment_id")
private UUID paymentId;
@Column("shipment_id")
private UUID shipmentId;
@Column("status")
private StatusEnum status;
private UUID cartId;
private UserEntity userEntity;
private AddressEntity addressEntity;
private PaymentEntity paymentEntity;
private List<ShipmentEntity> shipments = new ArrayList<>();
private CardEntity cardEntity;
private List<ItemEntity> items = new ArrayList<>();
private AuthorizationEntity authorizationEntity;
}
@Repository
public interface OrderRepository extends ReactiveCrudRepository<OrderEntity, UUID>,
OrderRepositoryExt {
@Query("select o.* from ecomm.orders o join ecomm.\"user\" u on o.customer_id = u.id where u.id = :custId")
Flux<OrderEntity> findByCustomerId(UUID custId);
}
public interface OrderRepositoryExt {
Mono<OrderEntity> insert(Mono<NewOrder> m);
Mono<OrderEntity> updateMapping(OrderEntity orderEntity);
}
@Repository
public class OrderRepositoryExtImpl implements OrderRepositoryExt {
private ConnectionFactory connectionFactory;
private DatabaseClient dbClient;
private ItemRepository itemRepo;
private CartRepository cartRepo;
private OrderItemRepository oiRepo;
private OrderEntity toEntity(NewOrder order, CartEntity c) {
OrderEntity orderEntity = new OrderEntity();
BeanUtils.copyProperties(order, orderEntity);
orderEntity.setUserEntity(c.getUser());
orderEntity.setCartId(c.getId());
orderEntity.setItems(c.getItems())
.setCustomerId(UUID.fromString(order.getCustomerId()))
.setAddressId(UUID.fromString(order.getAddress().getId()))
.setOrderDate(Timestamp.from(Instant.now()))
.setTotal(c.getItems().stream().collect(Collectors.toMap(k -> k.getProductId(),
v -> BigDecimal.valueOf(v.getQuantity()).multiply(v.getPrice())))
.values().stream().reduce(BigDecimal::add).orElse(BigDecimal.ZERO));
return orderEntity;
}
@Override
public Mono<OrderEntity> insert(Mono<NewOrder> mdl) {
R2dbcEntityTemplate template = new R2dbcEntityTemplate(connectionFactory);
AtomicReference<UUID> orderId = new AtomicReference<>();
Mono<List<ItemEntity>> itemEntities = mdl
.flatMap(m -> itemRepo.findByCustomerId(UUID.fromString(m.getCustomerId())).collectList().cache());
Mono<CartEntity> cartEntity = mdl
.flatMap(m -> cartRepo.findByCustomerId(UUID.fromString(m.getCustomerId()))).cache();
cartEntity = Mono.zip(cartEntity, itemEntities, (c, i) -> {
if (i.size() < 1) {
throw new ResourceNotFoundException(String
.format("There is no item found in customer's (ID:%s) cart.", c.getUser().getId()));
}
return c.setItems(i);
}).cache();
Mono<OrderEntity> orderEntity = Mono.zip(mdl, cartEntity, (m, c) -> toEntity(m, c)).cache();
return orderEntity.flatMap(oe -> dbClient.sql("""
INSERT INTO ecomm.orders (address_id, card_id, customer_id, order_date, total, status)
VALUES($1, $2, $3, $4, $5, $6)
""")
.bind("$1", Parameter.fromOrEmpty(oe.getAddressId(), UUID.class))
.bind("$2", Parameter.fromOrEmpty(oe.getCardId(), UUID.class))
.bind("$3", Parameter.fromOrEmpty(oe.getCustomerId(), UUID.class))
.bind("$4",
OffsetDateTime.ofInstant(oe.getOrderDate().toInstant(), ZoneId.of("Z")).truncatedTo(
ChronoUnit.MICROS))
.bind("$5", oe.getTotal())
.bind("$6", StatusEnum.CREATED.getValue()).map(new OrderMapper()::apply)
.one())
.then(orderEntity.flatMap(x ->
template.selectOne(
query(where("customer_id").is(x.getCustomerId()).and("order_date")
.greaterThanOrEquals(
OffsetDateTime.ofInstant(x.getOrderDate().toInstant(), ZoneId.of("Z"))
.truncatedTo(
ChronoUnit.MICROS))),
OrderEntity.class).map(t -> x.setId(t.getId()).setStatus(t.getStatus()))
));
}
@Override
public Mono<OrderEntity> updateMapping(OrderEntity orderEntity) {
return oiRepo.saveAll(orderEntity.getItems().stream().map(i -> new OrderItemEntity()
.setOrderId(orderEntity.getId()).setItemId(i.getId())).collect(toList()))
.then(
itemRepo.deleteCartItemJoinById(
orderEntity.getItems().stream().map(i -> i.getId()).collect(toList()),
orderEntity.getCartId()).then(Mono.just(orderEntity)));
}
}
class OrderMapper implements BiFunction<Row, Object, OrderEntity> {
@Override
public OrderEntity apply(Row row, Object o) {
OrderEntity oe = new OrderEntity();
return oe.setId(row.get("id", UUID.class))
.setCustomerId(row.get("customer_id", UUID.class))
.setAddressId(row.get("address_id", UUID.class))
.setCardId(row.get("card_id", UUID.class))
.setOrderDate(Timestamp.from(
ZonedDateTime.of((LocalDateTime) row.get("order_date"), ZoneId.of("Z")).toInstant()))
.setTotal(row.get("total", BigDecimal.class))
.setPaymentId(row.get("payment_id", UUID.class))
.setShipmentId(row.get("shipment_id", UUID.class))
.setStatus(StatusEnum.fromValue(row.get("status", String.class)));
}
}
public interface OrderService {
Mono<OrderEntity> addOrder(@Valid Mono<NewOrder> newOrder);
Mono<OrderEntity> updateMapping(@Valid OrderEntity orderEntity);
Flux<OrderEntity> getOrdersByCustomerId(@NotNull @Valid String customerId);
Mono<OrderEntity> getByOrderId(String id);
}
@Service
public class OrderServiceImpl implements OrderService {
private OrderRepository repository;
private UserRepository userRepo;
private AddressRepository addRepo;
private CardRepository cardRepo;
private ItemRepository itemRepo;
private ShipmentRepository shipRepo;
private BiFunction<OrderEntity, List<ItemEntity>, OrderEntity> biOrderItems = (o, fi) -> o
.setItems(fi);
@Override
public Mono<OrderEntity> addOrder(@Valid Mono<NewOrder> newOrder) {
return repository.insert(newOrder);
}
@Override
public Mono<OrderEntity> updateMapping(@Valid OrderEntity orderEntity) {
return repository.updateMapping(orderEntity);
}
@Override
public Flux<OrderEntity> getOrdersByCustomerId(String customerId) {
return repository.findByCustomerId(UUID.fromString(customerId)).flatMap(order ->
Mono.just(order)
.zipWith(userRepo.findById(order.getCustomerId()))
.map(t -> t.getT1().setUserEntity(t.getT2()))
.zipWith(addRepo.findById(order.getAddressId()))
.map(t -> t.getT1().setAddressEntity(t.getT2()))
.zipWith(cardRepo.findById(order.getCardId() != null ? order.getCardId()
: UUID.fromString("0a59ba9f-629e-4445-8129-b9bce1985d6a"))
.defaultIfEmpty(new CardEntity()))
.map(t -> t.getT1().setCardEntity(t.getT2()))
.zipWith(itemRepo.findByCustomerId(order.getCustomerId()).collectList(),
biOrderItems)
);
}
@Override
public Mono<OrderEntity> getByOrderId(String id) {
return repository.findById(UUID.fromString(id)).flatMap(order ->
Mono.just(order)
.zipWith(userRepo.findById(order.getCustomerId()))
.map(t -> t.getT1().setUserEntity(t.getT2()))
.zipWith(addRepo.findById(order.getAddressId()))
.map(t -> t.getT1().setAddressEntity(t.getT2()))
.zipWith(cardRepo.findById(order.getCardId()))
.map(t -> t.getT1().setCardEntity(t.getT2()))
.zipWith(itemRepo.findByCustomerId(order.getCustomerId()).collectList(),
biOrderItems)
);
}
}
@RestController
public class OrderController implements OrderApi {
private final OrderRepresentationModelAssembler assembler;
private OrderService service;
@Override
public Mono<ResponseEntity<Order>> addOrder(@Valid Mono<NewOrder> newOrder,
ServerWebExchange exchange) {
return service.addOrder(newOrder.cache())
.zipWhen(x -> service.updateMapping(x))
.map(t -> status(HttpStatus.CREATED).body(assembler.entityToModel(t.getT2(), exchange)))
.defaultIfEmpty(notFound().build());
}
@Override
public Mono<ResponseEntity<Flux<Order>>> getOrdersByCustomerId(@NotNull @Valid String customerId,
ServerWebExchange exchange) {
return Mono
.just(ok(assembler.toListModel(service.getOrdersByCustomerId(customerId), exchange)));
}
@Override
public Mono<ResponseEntity<Order>> getByOrderId(String id, ServerWebExchange exchange) {
return service.getByOrderId(id).map(o -> assembler.entityToModel(o, exchange))
.map(ResponseEntity::ok)
.defaultIfEmpty(notFound().build());
}
}
애플리케이션에 H2 콘솔 추가
@Component
public class H2ConsoleComponent {
private final static Logger log = LoggerFactory.getLogger(H2ConsoleComponent.class);
private Server webServer;
@Value("${modern.api.h2.console.port:8081}")
Integer h2ConsolePort;
@EventListener(ContextRefreshedEvent.class)
public void start() throws java.sql.SQLException {
log.info("starting h2 console at port "+h2ConsolePort);
this.webServer = org.h2.tools.Server.createWebServer("-webPort", h2ConsolePort.toString(), "-tcpAllowOthers").start();
}
@EventListener(ContextClosedEvent.class)
public void stop() {
log.info("stopping h2 console at port "+h2ConsolePort);
this.webServer.stop();
}
}
spring.flyway.url=jdbc:h2:file:./data/ecomm;AUTO_SERVER=TRUE;DB_CLOSE_DELAY=-1;IGNORECASE=TRUE;DATABASE_TO_UPPER=FALSE
spring.flyway.schemas=ecomm
spring.flyway.user=
spring.flyway.password=
spring.r2dbc.url=r2dbc:h2:file://././data/ecomm?options=AUTO_SERVER=TRUE;DB_CLOSE_DELAY=-1;IGNORECASE=TRUE;DATABASE_TO_UPPER=FALSE;;TRUNCATE_LARGE_LENGTH=TRUE;DB_CLOSE_ON_EXIT=FALSE
spring.r2dbc.driver=io.r2dbc:r2dbc-h2
spring.r2dbc.name=
spring.r2dbc.password=
소라브 샤르마, 『스프링 6와 스프링 부트 3로 배우는 모던 API 개발』 4장 (0) | 2025.01.21 |
---|---|
소라브 샤르마, 『스프링 6와 스프링 부트 3로 배우는 모던 API 개발』 3장 (0) | 2025.01.13 |
소라브 샤르마, 『스프링 6와 스프링 부트 3로 배우는 모던 API 개발』 2장 (3) | 2025.01.09 |
소라브 샤르마, 『스프링 6와 스프링 부트 3로 배우는 모던 API 개발』 1장 (0) | 2024.12.24 |
[필독! 개발자 온보딩 가이드] 시맨틱 버저닝 (0) | 2024.02.01 |