在建置SpringMVC時,常常使用annotation的方式來定義功能。不過annotation在開發時很便捷,可是對於不熟悉Spring的開發者,或者是想要針對annotation進行customize時就會很不方便,因為我們無法在貼annotation的地方直接寫客製邏輯,這些邏輯得寫在另一個地方,造成維護與debug的不易。
所以有另一種開發模式,叫做functional,有點Java lambda的味道,可以直接把我們想要的功能寫出來,而不是透過annotation來做代表。
在WebFlux的Spring中,常用以下四種類別:
定義要處理哪種request
定義接近來的request要做哪些事情
HTTP request, 包含header與body的資訊
HTTP response, 包含header與body的資訊
以下來看個GET的範例:
@Configuration
public class RouterFunctionConfig {
@Bean
public RouterFunction<?> helloRouterFunction() {
return route(GET("/hello"),
request -> ok().body(just("Hello World!"), String.class));
}
}
若要處理多個request endpoint,可以如下接著:
@Bean
public RouterFunction<?> helloRouterFunction() {
return route(GET("/hello"),
request -> ok().body(just("Hello World!"), String.class))
.andRoute(GET("/bye"),
request -> ok().body(just("Bye bye!"), String.class));
}
對比先前建置的OrderController:
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
import static org.springframework.web.reactive.function.server.RequestPredicates.queryParam;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import java.net.URI;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
@Configuration
public class RouterFunctionConfig {
@Autowired
private OrderRepository orderRepo;
@Bean
public RouterFunction<?> routerFunction() {
return route(GET("/api/orders").
and(queryParam("recent", t->t != null )),
this::recents)
.andRoute(POST("/api/orders"), this::postOrder);
}
public Mono<ServerResponse> recents(ServerRequest request) {
return ServerResponse.ok()
.body(orderRepo.findAll().take(12), Order.class);
}
public Mono<ServerResponse> postOrder(ServerRequest request) {
return request.bodyToMono(Order.class)
.flatMap(order -> orderRepo.save(order))
.flatMap(savedOrder -> {
return ServerResponse
.created(URI.create(
"http://localhost:8081/api/orders/" +
savedOrder.getId()))
.body(savedOrder, Order.class);
});
}
}
再來介紹如何測試WebFlux的Controller。
直接上範例:
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public class OrderControllerTest {
@Test
public void shouldReturnRecentOrders() {
Order[] orders = {
testOrder(1L), testOrder(2L),
testOrder(3L), testOrder(4L),
testOrder(5L), testOrder(6L),
testOrder(7L), testOrder(8L),
testOrder(9L), testOrder(10L),
testOrder(11L), testOrder(12L),
testOrder(13L), testOrder(14L),
testOrder(15L), testOrder(16L)
};
Flux<Order> orderFlux = Flux.just(orders);
OrderRepository orderRepo = Mockito.mock(OrderRepository.class);
when(orderRepo.findAll()).thenReturn(orderFlux);
WebTestClient testClient = WebTestClient.bindToController(
new OrderController(orderRepo)
).build();
testClient.get().uri("/api/orders?recent")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$").isArray()
.jsonPath("$").isNotEmpty()
.jsonPath("$[0].id").isEqualTo(orders[0].getId().toString())
.jsonPath("$[0].name").isEqualTo("Order 1")
.jsonPath("$[1].id").isEqualTo(orders[1].getId().toString())
.jsonPath("$[1].name").isEqualTo("Order 2")
.jsonPath("$[11].id").isEqualTo(orders[11].getId().toString())
.jsonPath("$[11].name").isEqualTo("Order 12")
.jsonPath("$[12]").doesNotExist();
}
...
}
jsonPath太醜了,可以用json來比對整串String:
ClassPathResource recentsResource =
new ClassPathResource("/orders/recent-orders.json");
String recentsJson = StreamUtils.copyToString(
recentsResource.getInputStream(), Charset.defaultCharset());
testClient.get().uri("/api/orders?recent")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectBody().json(recentsJson);
或者expectBodyList:
testClient.get().uri("/api/orders?recent")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectBodyList(Order.class)
.contains(Arrays.copyOf(orders, 12));
以下為測試post的範例:
@SuppressWarnings("unchecked")
@Test
public void shouldSaveAOrder() {
OrderRepository orderRepo = Mockito.mock(OrderRepository.class);
WebTestClient testClient = WebTestClient.bindToController(
new OrderController(orderRepo)
).build();
Mono<Order> unsavedOrderMono = Mono.just(testOrder(1L));
Order savedOrder = testOrder(1L);
Flux<Order> savedOrderMono = Flux.just(savedOrder);
when(orderRepo.saveAll(any(Mono.class))).thenReturn(savedOrderMono);
testClient.post()
.uri("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.body(unsavedOrderMono, Order.class)
.exchange()
.expectStatus().isCreated()
.expectBody(Order.class)
.isEqualTo(savedOrder);
}
前面這些測試都是透過mock的實作來實現,不會啟動server,不過其實也可以真的起一個server來做測試。
import java.io.IOException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.WebTestClient;
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)
public class OrderControllerWebTest {
@Autowired
private WebTestClient testClient;
}
@Test
public void shouldReturnRecentOrders() throws IOException {
testClient.get().uri("/api/orders?recent")
.accept(MediaType.APPLICATION_JSON).exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$").isArray()
.jsonPath("$.length()").isEqualTo(3)
.jsonPath("$[?(@.name == 'Conference')]").exists()
.jsonPath("$[?(@.name == 'Michael')]").exists()
.jsonPath("$[?(@.name == 'One O one')]").exists();
}