Java反應式事件溯源之第 4 部分:控制器
這裡為 HTTP API 層選擇了 Spring 框架,只是因為它非常流行。這可以是您想要的任何東西,只要記住我們正在構建一個反應式解決方案,因此使用具有非阻塞 API 的東西也是合理的,例如 Micronaut、Quarkus 等。
有ShowController2 個端點。第一個是ShowResponse通過id獲取:
@RestController @RequestMapping(value = "/shows") public class ShowController { private final ShowService showService; public ShowController(ShowService showService) { this.showService = showService; } @GetMapping(value = "{showId}", produces = "application/json") public Mono<ShowResponse> findById(@PathVariable UUID showId) { CompletionStage<ShowResponse> showResponse = showService.findShowBy(ShowId.of(showId)).thenApply(ShowResponse::from); return Mono.fromCompletionStage(showResponse); } |
由於我們使用的是 Spring WebFlux,因此我們需要轉換CompletionStage為Mono以保持反應性。在標準(阻塞)控制器中,我們需要阻塞並等待ShowService響應。
第二個端點更有趣,因為它用於座位預訂和取消。
@PatchMapping(value = "{showId}/seats/{seatNum}", consumes = "application/json") public Mono<ResponseEntity<String>> reserve(@PathVariable("showId") UUID showIdValue, @PathVariable("seatNum") int seatNumValue, @RequestBody SeatActionRequest request) { ShowId showId = ShowId.of(showIdValue); SeatNumber seatNumber = SeatNumber.of(seatNumValue); CompletionStage<ShowEntityResponse> actionResult = switch (request.action()) { case RESERVE -> showService.reserveSeat(showId, seatNumber); case CANCEL_RESERVATION -> showService.cancelReservation(showId, seatNumber); }; return Mono.fromCompletionStage(actionResult.thenApply(response -> switch (response) { case CommandProcessed ignored -> accepted().body(request.action() + " successful"); case CommandRejected rejected -> badRequest().body(request.action() + " failed with: " + rejected.error().name()); })); } |
讓我們跳過關於它是否是 RESTful 的討論。在這種情況下,我們需要將來自服務的響應轉換為適當的 HTTP 狀態程式碼。
我們還需要一些基本的 Spring Beans 配置:
@Configuration class BaseConfiguration { @Bean public Config config() { return PersistenceTestKitPlugin.config().withFallback(ConfigFactory.load()); } @Bean(destroyMethod = "terminate") public ActorSystem<Void> actorSystem(Config config) { return ActorSystem.create(VoidBehavior.create(), "es-workshop", config); } @Bean public ClusterSharding clusterSharding(ActorSystem<?> actorSystem) { return ClusterSharding.get(actorSystem); } @Bean Clock clock() { return new Clock.UtcClock(); } } @Configuration class ReservationConfiguration { @Bean public ShowService showService(ClusterSharding sharding, Clock clock) { return new ShowService(sharding, clock); } } |
這ActorSystem是一個相當沉重的結構。它應該只建立一次,是Bean. 建立型別化ActorSystem需要傳遞一些guardianBehavior. 此時,我們不需要這個功能,所以我們可以傳遞一個VoidBehavior:
public class VoidBehavior { public static Behavior<Void> create() { return Behaviors.receive(Void.class).build(); } } |
guardianBehavior在手動建立actor的情況下更有用。在我們的案例中,我們正在使用分片來實現。
配置bean正在使用記憶體中的事件儲存。這就是為什麼akka-persistence-testkit_*依賴的範圍必須是編譯。這只是用於原型設計,在下一部分中,當我們引入一個可用於生產的事件儲存時,它將被切換回測試。
控制器的測試:
使用WebTestClient:
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @DirtiesContext(classMode = ClassMode.AFTER_CLASS) class ShowControllerItTest { @Autowired private WebTestClient webClient; @Test public void shouldGetShowById() { //given String showId = randomShowId().id().toString(); //when //then webClient.get().uri("/shows/{showId}", showId) .exchange() .expectStatus().isOk() .expectBody(ShowResponse.class).value(shouldHaveId(showId)); } |
值得注意的一點是,我們在每次測試後都關閉Spring Context,以避免Actor System的衝突:
2021-10-14 10:51:48,057 ERROR akka.io.TcpListener - Bind failed for TCP channel on endpoint [/127.0.0.1:2551] java.net.BindException: [/127.0.0.1:2551] Address already in use at java.base/sun.nio.ch.Net.bind0(Native Method) at java.base/sun.nio.ch.Net.bind(Net.java:555) at java.base/sun.nio.ch.ServerSocketChannelImpl.netBind(ServerSocketChannelImpl.java:337) at java.base/sun.nio.ch.ServerSocketChannelImpl.bind(ServerSocketChannelImpl.java:294) at java.base/sun.nio.ch.ServerSocketAdaptor.bind(ServerSocketAdaptor.java:89) |
使用@DirtiesContext(classMode = ClassMode.AFTER_CLASS)是不夠的,我們還需要配置ActorSystem Bean的銷燬方法@Bean(destroyMethod = "terminate")。
另一種方法是在所有測試中重用 Spring Context 和 Actor System,但是我們不能像在ShowServiceTest中那樣手動建立 Actor System 。
執行應用程式
我們的應用程式已經準備好通過CinemaApplication類或從命令列./mvnw spring-boot:run -Dspring-boot.run.jvmArguments="--enable-preview" 啟動(確保你使用的是Java 17)。
你可以用development/show.http檔案(需要IntelliJ IDEA Ultimate)或development/show.sh的curl執行一些請求。
總結
當涉及到打包和明確的責任分工時,我是一個有點控制狂的人。這就是為什麼我為此新增了一個帶有ArchUnit斷言的測試。
PackageStructureValidationTest將檢查模組之間(基礎不應依賴保留)和單個模組內部是否有違反規則的情況。
領域層不應該依賴於應用、api、基礎設施(用於未來的變化)和akka。
應用層不應該依賴api、基礎設施等。所有的規則都可以用這張圖來表示:
檢視part_4標籤並使用該應用程式。主要收穫是,使用 Akka Persistence 之類的工具,我們可以非常快速地對 Event Sourced 應用程式進行原型設計。我們可以輕鬆地將其新增到現有的阻塞或非阻塞堆疊中。我們可以長時間不使用持久的事件儲存,但我有一種感覺,您希望看到一些可用於生產的東西。
相關文章
- Java反應式事件溯源之第 2 部分:Actor 模型Java事件模型
- Java反應式事件溯源之第5部分:事件儲存Java事件
- Java反應式事件溯源之第3部分:服務Java事件
- Java反應式事件溯源:領域Java事件
- 事件消費者之 Saga - 事件溯源事件
- 事件消費者之 Reactor - 事件溯源事件React
- 事件消費者之 Projector - 事件溯源事件Project
- 《反應式應用開發》之“什麼是反應式應用”
- .NET分散式Orleans - 6 - 事件溯源分散式事件
- 事件流與事件溯源事件
- 事件協作和事件溯源事件
- PHP 事件溯源PHP事件
- 事件溯源超越關聯式資料庫 - confluent事件資料庫
- Linux伺服器應急事件溯源報告Linux伺服器事件
- 剖玄析微聚合 - 事件溯源事件
- 函式化事件溯源的決策者模式 - thinkbeforecoding函式事件模式
- 用資料結構解釋事件溯源 – {4Comprehension}資料結構事件
- 測開之函式進階· 第4篇《匿名函式》函式
- 事件溯源全指南 - Arkwrite事件
- Java反應式框架Reactor中的Mono和FluxJava框架ReactMonoUX
- 用Java構建反應式REST API - Kalpa SenanayakeJavaRESTAPINaN
- 事件溯源將顛覆關聯式資料庫! - Remy事件資料庫REM
- 使用EventStoreDB實現事件溯源的Java開源專案事件Java
- 第4章函式函式
- 如何在Java後端中實現事件驅動架構:從事件匯流排到事件溯源Java後端事件架構
- 使用Kafka實現事件溯源Kafka事件
- Rust中的事件溯源 - ariseyhunRust事件
- 使用Resilience4j實施反應式斷路器 - WenqiENQ
- Chronicle事件溯源的最佳實踐事件
- Python的事件溯源開源庫Python事件
- Spring Boot和EventStoreDB事件溯源案例Spring Boot事件
- Occcurrent:JVM事件溯源工具庫包JVM事件
- [零基礎學JAVA]Java SE應用部分-35.JAVA類集之四Java
- JavaScript的BOM程式設計,事件-第4章JavaScript程式設計事件
- 無伺服器與事件溯源結合的演示案例:將事件溯源作為Azure函式的資料持久化機制的庫伺服器事件函式持久化
- JavaScript函式的反應性JavaScript函式
- 使用Redis/RabbitMQ/EventStore實現事件溯源CQRS微服務應用 - Aram KoukiaRedisMQ事件微服務
- HomeAway分享雲端事件溯源經驗事件