mike-neckのブログ

Java or Groovy or Swift or Golang

Future を Mono に変換する

CompletableFuture<T> から Mono<T> に関しては、 Mono#fromFuture(CompletableFuture) というメソッドがあるのだが、 Future<T> からは Mono<T> への変換メソッドはないし、もっと言えば Future<T> から CompletableFuture<T> への変換メソッドもない。

最初は次のような変換コードを書いていたが、あまり良さそうに見えない。

final Future<T> future = ...;
final CompletableFuture<T> completableFuture = CompletableFuture.supplyAsync(() -> {
    try {
        return future.get();
    } catch(final InterruptedException | ExecutionException e) {
        throw new RuntimeException(e);
    }
});
final Mono<T> mono = Mono.fromFuture(completableFuture);

いろいろ調べてたら、 MonoProcessor<T> というものがあって、これは Subscriber<T> を実装しているので、 onNext(T) というメソッドから値を渡せるし、 onError(Throwable) というメソッドから例外を伝播させられるようだ。

これを使うと次のようになる。

final Future<T> future = ...;
final MonoProcessor<T> processor = MonoProcessor.create();
try {
  processor.onNext(future.get());
} catch(final InterruptedException | ExecutionException e) {
  processor.onError(e);
}
final Mono<T> mono = processor;

非常にシンプルにできた。


2018/05/06 0:12 追記

@making さんから、上のコードがブロックしていると指摘を受けた上で、次のようにするとよいとアドバイスをうけた。

final Future<T> future = ...;
final Mono<T> mono = Mono.fromCallable(future::get)
    .subscribeOn(Schedulers.fromExecutore(executor));

Spring WebFlux Security のカスタム認証

Spring WebFlux Security でカスタム認証を実装します。ベースとなるユーザー認証部分は以前に実装したとおりです。

mike-neck.hatenadiary.com

今回は権限まわりの実装します。〜〜のパスには〜〜の権限が必要みたいなやつです。


特定のリソースへの特定のメソッドでのアクセス可否を設定するには SecurityWebFIlterChain を Bean 登録します。

  @Bean
  SecurityWebFilterChain springSecurityFilterChain(final ServerHttpSecurity http) {
    return http.httpBasic() // (1)
        .and()
        .authorizeExchange() // (2)
        .pathMatchers(HttpMethod.GET, "/talks/**") // (3)
        .hasAuthority("USER") // (4)
        .pathMatchers(HttpMethod.GET, "/test/**")
        .hasAuthority("ADMIN")
        .and()
        .build();
  }
  1. 今回はベーシック認証なので httpBasic を呼び出します
  2. アクセス可能なパス、メソッド、Authority(Role) の組み合わせ設定をします
  3. GET で /talk 以下のアクセスを許可します
  4. 上記のパスへのGETアクセスを authority=USER をもつユーザーに限定します

例えば USER 権限のみをもつユーザー foo を用意して、 /talks にアクセスしてみます

$ curl -v http://localhost:8080/talks --basic -u "foo:foo-pass"
> GET /talks HTTP/1.1
> Host: localhost:8080
> Authorization: Basic Zm9vOmZvby1wYXNz
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< transfer-encoding: chunked
< Content-Type: application/json
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Content-Type-Options: nosniff
< X-Frame-Options: DENY
< X-XSS-Protection: 1 ; mode=block
< 
[{"id":23014831827719168,"name":"beta users","description":"beta users","created":[2018,3,4,10,30,18,464000000]}]

talks にこのユーザーはアクセスできるので、jsonが返ってきます。今後は ADMIN 権限が必要な /test にアクセスしてみます。

$ curl -v http://localhost:8080/test --basic -u "foo:foo-pass"
> GET /test HTTP/1.1
> Host: localhost:8080
> Authorization: Basic Zm9vOmZvby1wYXNz
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 403 Forbidden
< transfer-encoding: chunked
< Content-Type: text/plain
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Content-Type-Options: nosniff
< X-Frame-Options: DENY
< X-XSS-Protection: 1 ; mode=block
< 
Access Denied

今度は403が返ってきました。

Spring WebFlux で Mono が empty の場合に404 を返す

Spring WebFlux フレームワークは戻り値の方は Mono<V> になるのだが、この Mono<V> が empty の場合、Spring 側でよしなにやってくれると思ったら、実はそうでもないらしい。

例えばこのようなハンドラーを作ってみる

  Mono<ServerResponse> test(final ServerRequest request) {
    final Optional<String> value = request.queryParam("value");
    return Mono.justOrEmpty(value)
        .map(Wrapper::new)
        .flatMap(v -> ServerResponse.ok().body(Mono.just(v), Wrapper.class));
  }

  @Bean
  RouterFunction<ServerResponse> testEndpointHandler() {
    return route(GET("/test"), this::test);
  }

  @Value
  public static class Wrapper {
    private final String value;
  }

このハンドラーはクエリーパラメーター value の値を json にラップして返してくれる。

これに対してクエリーパラメーター value に対して値を設定してリクエストを送ってみると次のようなレスポンスが返ってくる。

$ curl -v "http://localhost:8080/test?value=foo"
> GET /test?value=foo HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< transfer-encoding: chunked
< Content-Type: application/json;charset=UTF-8
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Content-Type-Options: nosniff
< X-Frame-Options: DENY
< X-XSS-Protection: 1 ; mode=block
< 
{"value":"foo"}

一方、クエリーパラメーター value がないリクエストの場合は次のようになる。

$ curl -v "http://localhost:8080/test"
> GET /test HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Content-Type-Options: nosniff
< X-Frame-Options: DENY
< X-XSS-Protection: 1 ; mode=block
< content-length: 0
< 

クエリーパラメーター value が存在しない場合は返す Mono<ServerResponse> は empty なので 404 が返ってきてほしいのだが、実際は200 が返ってきてしまう。

このような場合は Mono#switchIfEmpty(Mono<? extends V>) を用いる。

Mono (Reactor Core 3.1.5.RELEASE)


修正後のコード(一部)

  Mono<ServerResponse> test(final ServerRequest request) {
    final Optional<String> value = request.queryParam("value");
    return Mono.justOrEmpty(value)
        .map(Wrapper::new)
        .flatMap(v -> ServerResponse.ok().body(Mono.just(v), Wrapper.class))
        .switchIfEmpty(ServerResponse.notFound().build());
  }

修正後のアプリケーションに対してクエリーパラメーター value を設定せずにリクエストしてみる。

$ curl -v http://localhost:8080/test --basic -u foo:foo-pass
> GET /test HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 404 Not Found
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Content-Type-Options: nosniff
< X-Frame-Options: DENY
< X-XSS-Protection: 1 ; mode=block
< content-length: 0
< 

404が返ってきた。

Gradle4.6 からの JUnit5 実行方法

Gradle4.6 がリリースされ、 JUnit5 に対応しました。以下にGradle から JUnit5 のテストを実行するための build.gradle を示しますが、特殊なことをするわけではありません。


build.gradle

plugins {
  id 'java'
}

repositories {
    mavenCentral()
}

test {
    useJUnitPlatform {
        includeEngines 'junit-jupiter'
    }
}

dependencies {
    testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.1.0'
    testRuntime group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.1.0'
}

(java-library プラグインを使う場合)

build.gradle.kts

plugins {
  id("java-library")
}

repositories {
  mavenCentral()
}

tasks {
  "test"(Test::class) {
    useJUnitPlatform {
      includeEngines("junit-jupiter")
    }
  }
}

dependencies {
  testImplementation("org.junit.jupiter:junit-jupiter-api:5.1.0")
  testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.1.0")
}

ポイントは3つ

  • ややこしいプラグインの記述がなくなりました
  • テストのコンパイルjunit-jupiter-api を ランタイムに junit-jupiter-engine を追加します
  • task.test.useJUnitPlatform() を呼び出す

これであとはよしなにJUnit5を実行して、今まで通りのレポートを出力してくれます。

f:id:mike_neck:20180302000902p:plain

f:id:mike_neck:20180302000935p:plain