Initial commit.

This commit is contained in:
Florian THIERRY
2025-09-18 16:57:59 +02:00
commit 076a224bcc
45 changed files with 1123 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
plugins {
id("java")
id("org.springframework.boot")
id("io.spring.dependency-management")
}
group = "com.zeenea.experiments.virtualthreads.reactorapp"
version = "0.0.1-SNAPSHOT"
dependencies {
implementation("org.springframework.boot:spring-boot-starter-webflux")
developmentOnly("org.springframework.boot:spring-boot-devtools")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("org.postgresql:postgresql")
compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.projectreactor:reactor-test")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.withType<Test> {
useJUnitPlatform()
}

View File

@@ -0,0 +1,10 @@
package com.zeenea.experiments.virtualthreads.reactorapp.configuration;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@Configuration
@EnableJpaRepositories("com.zeenea.experiments.virtualthreads.reactorapp.infrastructure")
@EntityScan("com.zeenea.experiments.virtualthreads.reactorapp.infrastructure")
public class JpaConfiguration {}

View File

@@ -0,0 +1,26 @@
package com.zeenea.experiments.virtualthreads.reactorapp.domain.catalog;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.catalog.model.Catalog;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.catalog.port.CatalogPort;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.UUID;
@Service
public class CatalogService {
private final CatalogPort catalogPort;
public CatalogService(CatalogPort catalogPort) {
this.catalogPort = catalogPort;
}
public Mono<Catalog> getById(UUID catalogId) {
return catalogPort.getById(catalogId);
}
public Flux<Catalog> getAll() {
return catalogPort.getAll();
}
}

View File

@@ -0,0 +1,5 @@
package com.zeenea.experiments.virtualthreads.reactorapp.domain.catalog.model;
import java.util.UUID;
public record Catalog(UUID id, String name) {}

View File

@@ -0,0 +1,12 @@
package com.zeenea.experiments.virtualthreads.reactorapp.domain.catalog.port;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.catalog.model.Catalog;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.UUID;
public interface CatalogPort {
Mono<Catalog> getById(UUID catalogId);
Flux<Catalog> getAll();
}

View File

@@ -0,0 +1,19 @@
package com.zeenea.experiments.virtualthreads.reactorapp.domain.item;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.item.model.Item;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.item.port.ItemPort;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
@Service
public class ItemService {
private final ItemPort itemPort;
public ItemService(ItemPort itemPort) {
this.itemPort = itemPort;
}
public Flux<Item> getAllItems() {
return itemPort.getAll();
}
}

View File

@@ -0,0 +1,14 @@
package com.zeenea.experiments.virtualthreads.reactorapp.domain.item.model;
import java.time.ZonedDateTime;
import java.util.UUID;
public record Item(
UUID id,
String name,
ZonedDateTime sharedDate
) {
public boolean isShared() {
return sharedDate != null;
}
}

View File

@@ -0,0 +1,8 @@
package com.zeenea.experiments.virtualthreads.reactorapp.domain.item.port;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.item.model.Item;
import reactor.core.publisher.Flux;
public interface ItemPort {
Flux<Item> getAll();
}

View File

@@ -0,0 +1,36 @@
package com.zeenea.experiments.virtualthreads.reactorapp.domain.marketplace;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.catalog.CatalogService;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.catalog.model.Catalog;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.item.ItemService;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.item.model.Item;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.marketplace.model.Marketplace;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.util.List;
@Service
public class MarketplaceService {
private final CatalogService catalogService;
private final ItemService itemService;
public MarketplaceService(CatalogService catalogService, ItemService itemService) {
this.catalogService = catalogService;
this.itemService = itemService;
}
public Mono<Marketplace> getMarketplace() {
Mono<List<Catalog>> catalogListMono = catalogService.getAll().collectList();
Mono<List<Item>> sharedItemListMono = itemService.getAllItems()
.filter(Item::isShared)
.collectList();
return Mono.zip(catalogListMono, sharedItemListMono)
.map(tuple -> {
List<Catalog> catalogs = tuple.getT1();
List<Item> items = tuple.getT2();
return new Marketplace(catalogs, items);
});
}
}

View File

@@ -0,0 +1,11 @@
package com.zeenea.experiments.virtualthreads.reactorapp.domain.marketplace.model;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.catalog.model.Catalog;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.item.model.Item;
import java.util.List;
public record Marketplace(
List<Catalog> catalogs,
List<Item> sharedItems
) {}

View File

@@ -0,0 +1,27 @@
package com.zeenea.experiments.virtualthreads.reactorapp.exposition.catalog;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.catalog.CatalogService;
import com.zeenea.experiments.virtualthreads.reactorapp.exposition.catalog.model.CatalogDto;
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 reactor.core.publisher.Mono;
import java.util.UUID;
@RestController
@RequestMapping("/api/catalogs")
public class CatalogController {
private final CatalogService catalogService;
public CatalogController(CatalogService catalogService) {
this.catalogService = catalogService;
}
@GetMapping("/{catalogId}")
public Mono<CatalogDto> getById(@PathVariable("catalogId") UUID catalogId) {
return catalogService.getById(catalogId)
.map(CatalogDto::new);
}
}

View File

@@ -0,0 +1,14 @@
package com.zeenea.experiments.virtualthreads.reactorapp.exposition.catalog.model;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.catalog.model.Catalog;
import java.util.UUID;
public record CatalogDto(
UUID id,
String name
) {
public CatalogDto(Catalog catalog) {
this(catalog.id(), catalog.name());
}
}

View File

@@ -0,0 +1,24 @@
package com.zeenea.experiments.virtualthreads.reactorapp.exposition.marketplace;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.marketplace.MarketplaceService;
import com.zeenea.experiments.virtualthreads.reactorapp.exposition.marketplace.model.MarketplaceDto;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api/marketplace")
public class MarketplaceController {
private final MarketplaceService marketplaceService;
public MarketplaceController(MarketplaceService marketplaceService) {
this.marketplaceService = marketplaceService;
}
@GetMapping
public Mono<MarketplaceDto> getMarketplace() {
return marketplaceService.getMarketplace()
.map(MarketplaceDto::new);
}
}

View File

@@ -0,0 +1,15 @@
package com.zeenea.experiments.virtualthreads.reactorapp.exposition.marketplace.model;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.item.model.Item;
import java.util.UUID;
public record ItemDto(
UUID id,
String name,
Boolean isShared
) {
public ItemDto(Item item) {
this(item.id(), item.name(), item.isShared());
}
}

View File

@@ -0,0 +1,15 @@
package com.zeenea.experiments.virtualthreads.reactorapp.exposition.marketplace.model;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.marketplace.model.Marketplace;
import com.zeenea.experiments.virtualthreads.reactorapp.exposition.catalog.model.CatalogDto;
import java.util.List;
public record MarketplaceDto(List<CatalogDto> catalogs, List<ItemDto> sharedItems) {
public MarketplaceDto(Marketplace marketplace) {
this(
marketplace.catalogs().stream().map(CatalogDto::new).toList(),
marketplace.sharedItems().stream().map(ItemDto::new).toList()
);
}
}

View File

@@ -0,0 +1,37 @@
package com.zeenea.experiments.virtualthreads.reactorapp.infrastructure.catalog;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.catalog.model.Catalog;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.catalog.port.CatalogPort;
import com.zeenea.experiments.virtualthreads.reactorapp.infrastructure.catalog.model.CatalogJpaEntity;
import com.zeenea.experiments.virtualthreads.reactorapp.infrastructure.catalog.repository.CatalogJpaRepository;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.util.UUID;
@Component
public class CatalogJpaAdapter implements CatalogPort {
private final CatalogJpaRepository catalogJpaRepository;
public CatalogJpaAdapter(CatalogJpaRepository catalogJpaRepository) {
this.catalogJpaRepository = catalogJpaRepository;
}
@Override
public Mono<Catalog> getById(UUID catalogId) {
return Mono.fromCallable(() -> catalogJpaRepository.findById(catalogId))
.subscribeOn(Schedulers.boundedElastic())
.flatMap(result -> result.map(Mono::just).orElseGet(Mono::empty))
.map(CatalogJpaEntity::toDomain);
}
@Override
public Flux<Catalog> getAll() {
return Mono.fromCallable(catalogJpaRepository::findAll)
.subscribeOn(Schedulers.boundedElastic())
.flatMapMany(Flux::fromIterable)
.map(CatalogJpaEntity::toDomain);
}
}

View File

@@ -0,0 +1,34 @@
package com.zeenea.experiments.virtualthreads.reactorapp.infrastructure.catalog.model;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.catalog.model.Catalog;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.UUID;
@Entity
@Table(name = "catalog")
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class CatalogJpaEntity {
@Id
private UUID id;
@Column(nullable = false)
private String name;
public CatalogJpaEntity(Catalog catalog) {
id = catalog.id();
name = catalog.name();
}
public Catalog toDomain() {
return new Catalog(id, name);
}
}

View File

@@ -0,0 +1,11 @@
package com.zeenea.experiments.virtualthreads.reactorapp.infrastructure.catalog.repository;
import com.zeenea.experiments.virtualthreads.reactorapp.infrastructure.catalog.model.CatalogJpaEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.UUID;
@Repository
public interface CatalogJpaRepository extends JpaRepository<CatalogJpaEntity, UUID> {
}

View File

@@ -0,0 +1,27 @@
package com.zeenea.experiments.virtualthreads.reactorapp.infrastructure.item;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.item.model.Item;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.item.port.ItemPort;
import com.zeenea.experiments.virtualthreads.reactorapp.infrastructure.item.model.ItemJpaEntity;
import com.zeenea.experiments.virtualthreads.reactorapp.infrastructure.item.repository.ItemJpaRepository;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
@Component
public class ItemJpaAdapter implements ItemPort {
private final ItemJpaRepository itemJpaRepository;
public ItemJpaAdapter(ItemJpaRepository itemJpaRepository) {
this.itemJpaRepository = itemJpaRepository;
}
@Override
public Flux<Item> getAll() {
return Mono.fromCallable(itemJpaRepository::findAll)
.subscribeOn(Schedulers.boundedElastic())
.flatMapMany(Flux::fromIterable)
.map(ItemJpaEntity::toDomain);
}
}

View File

@@ -0,0 +1,39 @@
package com.zeenea.experiments.virtualthreads.reactorapp.infrastructure.item.model;
import com.zeenea.experiments.virtualthreads.reactorapp.domain.item.model.Item;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.ZonedDateTime;
import java.util.UUID;
@Entity
@Table(name = "item")
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class ItemJpaEntity {
@Id
private UUID id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private ZonedDateTime sharedDate;
public ItemJpaEntity(Item item) {
id = item.id();
name = item.name();
sharedDate = item.sharedDate();
}
public Item toDomain() {
return new Item(id, name, sharedDate);
}
}

View File

@@ -0,0 +1,11 @@
package com.zeenea.experiments.virtualthreads.reactorapp.infrastructure.item.repository;
import com.zeenea.experiments.virtualthreads.reactorapp.infrastructure.item.model.ItemJpaEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.UUID;
@Repository
public interface ItemJpaRepository extends JpaRepository<ItemJpaEntity, UUID> {
}

View File

@@ -0,0 +1,11 @@
package com.zeenea.experiments.virtualthreads.reactorapp.launcher;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = "com.zeenea.experiments.virtualthreads.reactorapp")
public class ReactorApplication {
public static void main(String[] args) {
SpringApplication.run(ReactorApplication.class, args);
}
}

View File

@@ -0,0 +1,14 @@
spring:
application:
name: reactor-app
datasource:
driverClassName: org.postgresql.Driver
url: jdbc:postgresql://localhost:51000/virtual_threads_test_db
username: virtual_threads_test_user
password: password
threads:
virtual:
enabled: true
server:
port: 52001