これまでspring-webを使用してアプリケーションを作ってきたのですが、
リアクティブへの関心が自分の中で高まってきたのでspring-webfluxも触ってみたいなと思いました。
spring-webでやっていたことってspring-webfluxだとどうなるんだろう?
こんな疑問の解決に役に立てばとまとめてみます!
使用するライブラリ
■spring-web
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
}
■spring-webflux
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
}
Controller
■spring-web
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SpringWebController {
@GetMapping("/helloworld")
public String hello() {
TimeUnit.SECONDS.sleep(10);
return "hello world";
}
}
spring-webflux
import org.reactivestreams.Publisher;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import java.time.Duration;
@RestController
public class SpringWebfluxController {
@GetMapping("/helloworld")
public Publisher<String> hello() {
return Mono.just("hello world")
.delayElement(Duration.ofSeconds(10));
}
}
webと同じ実装でも動作するが、webfluxではMonoで返却することでreactiveの恩恵を受けられる。
webでは10秒間のスリープの間スレッドが占有され続けるが、
webfluxではブロックされないため、少ないスレッドで多くのリクエストを捌ける!
検証はしてないですが、こちらのサイトが参考になりました。
APIコールするアプリケーション
get
注意
web/webfluxによらず、uriを/getServer?name=rhirabayというように記載すると
actuatorを入れた時に正しくメトリクスが取れなかったり、
メモリが増え続ける可能性があるため注意
■spring-web
@RestController
public class SpringWebController {
@GetMapping("/get")
public String get() {
RestTemplate restTemplate = new RestTemplateBuilder()
.rootUri("http://localhost:8082")
.build();
return restTemplate.getForObject("/getServer?name={name}", String.class, "rhirabay");
}
@GetMapping("/getServer")
public String getServer(@RequestParam String name) {
return "Hello world, " + name + "!";
}
}
■spring-webflux
@RestController
public class SpringWebfluxController {
@GetMapping("/get")
public Publisher<String> get() {
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:8081")
.build();
return webClient.get()
.uri("/getServer?name={name}", "rhirabay")
.retrieve()
.bodyToMono(String.class);
}
@GetMapping("/getServer")
public Publisher<String> getServer(@RequestParam String name) {
return Mono.just("Hello world, " + name + "!");
}
}
post
■spring-web
@RestController
public class SpringWebController {
@GetMapping("/post")
public Message post() {
RestTemplate restTemplate = new RestTemplateBuilder()
.rootUri("http://localhost:8082")
.build();
return restTemplate.postForObject("/postServer", Message.of("rhirabay"), Message.class);
}
@PostMapping("/postServer")
public Message postServer(@RequestBody Message message) {
return Message.of("Hello world, " + message.getValue() + "!");
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
class Message {
private String value;
}
■spring-webflux
@RestController
public class SpringWebfluxController {
@GetMapping("/post")
public Publisher<Message> post() {
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:8081")
.build();
return webClient.post()
.uri("/postServer")
.body(Mono.just(Message.of("rhirabay")), Message.class)
.retrieve()
.bodyToMono(Message.class);
}
@PostMapping("/postServer")
public Publisher<Message> postServer(@RequestBody Mono<Message> monoMessage) {
return monoMessage.map(message -> Message.of("Hello world, " + message.getValue() + "!"));
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
class Message {
private String value;
}
API実行時にヘッダーを設定する
■spring-web
@RestController
public class SpringWebController {
@GetMapping("/header")
public Headers header() {
RestTemplate restTemplate = new RestTemplateBuilder()
.rootUri("http://localhost:8082")
.defaultHeader("defaultKey", "defaultValue") // 固定ヘッダー
.additionalInterceptors(new HeaderInterceptor()) // 可変ヘッダー①
.build();
HttpHeaders header = new HttpHeaders(); // 可変ヘッダー②
header.add("key", "value");
return restTemplate.exchange("/headerServer", HttpMethod.GET, new HttpEntity<>(header), Headers.class)
.getBody();
}
@GetMapping("/headerServer")
public Headers headerServer(
@RequestHeader String defaultKey,
@RequestHeader String commonKey,
@RequestHeader String key
) {
return Headers.of(defaultKey, commonKey, key);
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
class Headers {
private String defaultKey;
private String commonKey;
private String key;
}
class HeaderInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
request.getHeaders().add("commonKey", "commonValue");
return execution.execute(request, body);
}
}
■spring-webflux
@RestController
public class SpringWebfluxController {
@GetMapping("/header")
public Publisher<Headers> header() {
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:8081")
.defaultHeader("defaultKey", "defaultValue") // 固定ヘッダー
.filter(new HeaderFilter()) // 可変ヘッダー①
.build();
return webClient.get()
.uri("/headerServer")
.header("key", "value") // 可変ヘッダー②
.retrieve()
.bodyToMono(Headers.class);
}
@GetMapping("/headerServer")
public Publisher<Headers> headerServer(
@RequestHeader String defaultKey,
@RequestHeader String commonKey,
@RequestHeader String key
) {
return Mono.just(Headers.of(defaultKey, commonKey, key));
}
}
class HeaderFilter implements ExchangeFilterFunction {
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
ClientRequest wrappedRequest = ClientRequest.from(request)
.header("commonKey", "commonValue")
.build();
return next.exchange(wrappedRequest);
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
class Headers {
private String defaultKey;
private String commonKey;
private String key;
}
Clientのロギング
ログ内容はこんな感じ
request start. uri=[http://localhost:8081/getServer?name=rhirabay], method=[GET]
request end in 88ms. uri=[http://localhost:8081/getServer?name=rhirabay], status=[200]
■spring-web
@RestController
public class SpringWebController {
@GetMapping("/clientLogging")
public String clientLogging() {
RestTemplate restTemplate = new RestTemplateBuilder()
.rootUri("http://localhost:8082")
.additionalInterceptors(new ClientLoggingInterceptor())
.build();
return restTemplate.getForObject("/getServer?name={name}", String.class, "rhirabay");
}
}
@Slf4j
class ClientLoggingInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// request logging
log.info("request start. uri=[{}], method=[{}]", request.getURI(), request.getMethod());
var start = System.currentTimeMillis();
var response = execution.execute(request, body);
var end = System.currentTimeMillis();
// response logging
log.info("request end in {}ms. uri=[{}], status=[{}]", end - start, request.getURI(), response.getRawStatusCode());
return response;
}
}
■spring-webflux
@RestController
public class SpringWebfluxController {
@GetMapping("/clientLogging")
public Publisher<String> clientLogging() {
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:8081")
.filter(new ClientLoggingFilter())
.build();
return webClient.get()
.uri("/getServer?name={name}", "rhirabay")
.retrieve()
.bodyToMono(String.class);
}
}
@Slf4j
class ClientLoggingFilter implements ExchangeFilterFunction {
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
// request logging
log.info("request start. uri=[{}], method=[{}]", request.url(), request.method());
var start = System.currentTimeMillis();
return next.exchange(request)
.doOnSuccess(clientResponse -> {
var end = System.currentTimeMillis();
// response logging
log.info("request end in {}ms. uri=[{}], status=[{}]", end - start, request.url(), clientResponse.rawStatusCode());
});
}
}