spring-webからspring-webfluxへの移行

これまで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());
                });
    }
}

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です