Java反應式事件溯源之第 4 部分:控制器

banq 發表於 2022-01-23
Java

這裡為 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、基礎設施等。所有的規則都可以用這張圖來表示:

Java反應式事件溯源之第 4 部分:控制器

檢視part_4標籤並使用該應用程式。主要收穫是,使用 Akka Persistence 之類的工具,我們可以非常快速地對 Event Sourced 應用程式進行原型設計。我們可以輕鬆地將其新增到現有的阻塞或非阻塞堆疊中。我們可以長時間不使用持久的事件儲存,但我有一種感覺,您希望看到一些可用於生產的東西。