Spring BootのWebClientのテストにSpring Cloud ContractのWireMockを使う
Spring BootのWebClientのテストにSpring Cloud ContractのWireMockを使ってみたときのメモ。
(ちなみに、RestTemplateのテストで使えるMockRestServiceServerはWebClientをサポートしていなかった)
ソースコード
build.gradle
plugins { id 'org.springframework.boot' version '2.4.5' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' } group = 'com.example' version = '0.0.1-SNAPSHOT' sourceCompatibility = JavaVersion.VERSION_11 repositories { mavenCentral() } dependencyManagement { imports { mavenBom 'org.springframework.cloud:spring-cloud-dependencies:2020.0.2' } } dependencies { annotationProcessor 'org.projectlombok:lombok' compileOnly 'org.projectlombok:lombok' implementation 'org.springframework.boot:spring-boot-starter-webflux' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner' testImplementation 'io.projectreactor:reactor-test' } test { useJUnitPlatform() }
テスト対象コード
GitHubApiClient.java
@Component public class GitHubApiClient { private final WebClient webClient; public GitHubApiClient(WebClient.Builder builder, @Value("${base-url:https://api.github.com}") String baseUrl) { webClient = builder.baseUrl(baseUrl).build(); } public Mono<User> getUser(String username) { return webClient.get() .uri("/users/{username}", username) .retrieve() .bodyToMono(User.class); } }
User.java
@JsonIgnoreProperties(ignoreUnknown = true) @JsonNaming(SnakeCaseStrategy.class) @Data public class User { private long id; private String login; ... }
テストコード
GitHubApiClientTest.java
package com.example; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @SpringBootTest(properties = "base-url=http://localhost:${wiremock.server.port}") @AutoConfigureWireMock(port = 0) class GitHubApiClientTest { @Autowired private GitHubApiClient client; @Test void getUser() { ResponseDefinitionBuilder response = aResponse().withBody("{\"login\":\"hirakida\",\"id\":100}") .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .withStatus(HttpStatus.OK.value()); stubFor(get(urlEqualTo("/users/hirakida")).willReturn(response)); Mono<User> result = client.getUser("hirakida"); StepVerifier.create(result) .expectNextMatches(user -> user != null && "hirakida".equals(user.getLogin()) && user.getId() == 100) .verifyComplete(); } }
参考
- https://docs.spring.io/spring-framework/docs/5.3.x/reference/html/web-reactive.html#webflux-client-testing
- https://github.com/spring-projects/spring-framework/issues/19852
- https://github.com/spring-projects/spring-boot/issues/8404
- https://youtu.be/wAYt4Z4SF7g?t=2310
- https://stackoverflow.com/questions/48707625/set-property-with-wiremock-random-port-in-spring-boot-test
GradleでJNIを試す
GradleでJNIを試したときのメモ。
ソースコード
% tree . . ├── build.gradle ├── gradle │ └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── hello │ └── c │ ├── com_example_HelloJNI.h │ └── hello.c └── main └── java └── com └── example ├── HelloJNI.java └── Main.java
build.gradle
MacOSの設定を追加。Windowsなどの設定も入れる場合はこちらを参照。
import org.gradle.internal.jvm.Jvm plugins { id 'java' id 'application' id 'c' } group 'com.example' version '1.0-SNAPSHOT' sourceCompatibility = JavaVersion.VERSION_11 repositories { mavenCentral() } model { components { hello(NativeLibrarySpec) { binaries.all { cCompiler.args '-I', "${Jvm.current().javaHome}/include" cCompiler.args '-I', "${Jvm.current().javaHome}/include/darwin" } } } } build.dependsOn 'helloSharedLibrary' mainClassName = 'com.example.Main' run { systemProperty 'java.library.path', file("${buildDir}/libs/hello/shared").absolutePath }
src/hello/c
com_example_HelloJNI.h
HelloJNI.javaを作成後、ヘッダーファイルを自動生成する。
% javac -h -jni HelloJNI.java
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_example_HelloJNI */ #ifndef _Included_com_example_HelloJNI #define _Included_com_example_HelloJNI #ifdef __cplusplus extern "C" { #endif /* * Class: com_example_HelloJNI * Method: hello * Signature: ()V */ JNIEXPORT void JNICALL Java_com_example_HelloJNI_hello (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif
hello.c
#include "com_example_HelloJNI.h" JNIEXPORT void JNICALL Java_com_example_HelloJNI_hello(JNIEnv *env, jobject obj) { printf("Hello!\n"); }
src/main/java
HelloJNI.java
package com.example; public class HelloJNI { static { System.loadLibrary("hello"); } public native void hello(); }
Main.java
package com.example; public final class Main { public static void main(String[] args) { HelloJNI jni = new HelloJNI(); jni.hello(); } }
動作確認
% ./gradlew run
> Task :run
Hello!
参考
JITWatchを試す
OpenJDKでJITWatchを試したときのメモ。
準備
JDK
AdoptOpenJDK 11 HosSpotを用意する。今回はSDKMANでインストールする。
% sdk install java 11.0.9.hs-adpt % sdk use java 11.0.9.hs-adpt
hsdis (HotSpot disassembler)
AdoptOpenJDKのソースコードをダウンロードして、hsdisのディレクトリに移動する。
% git clone git@github.com:AdoptOpenJDK/openjdk-jdk11u.git % git checkout jdk-11.0.9+11_adopt % cd openjdk-jdk11u/src/utils/hsdis
binutilsをダウンロードする。
(AdoptOpenJDK 11.0.9の場合、binutils-2.29 以降を使うとビルドできなかったので、このバージョンを使う)
% wget http://ftp.gnu.org/gnu/binutils/binutils-2.28.1.tar.gz % tar xvf binutils-2.28.1.tar.gz
hsdisをビルドする。
% make BINUTILS=binutils-2.28.1 ARCH=amd64
ビルドしたhsdisのライブラリを、SDKMANでインストールしたJDKのディレクトリにコピーする。
% cp build/macosx-amd64/hsdis-amd64.dylib ~/.sdkman/candidates/java/11.0.9.hs-adpt/lib/server/
JITWatch
JITWatchをダウンロードしてビルドする。
% git clone git@github.com:AdoptOpenJDK/jitwatch.git % cd jitwatch % ./gradlew clean build
HotSpotログファイル
今回はJITWatchのデモアプリを使う 。
ダウンロードしたJITWatchの中にある以下のシェルを実行すると、カレントディレクトリにhotspot_pidxxxxx.log
というログファイルが出力される。
% ./makeDemoLogFile.sh
Demo
JITWatchの設定
JITWatchを起動する。
% ./gradlew run
Config
ボタンをクリックしてJITWatch Configurationを開く。
Source locations
Add JDK src
ボタンをクリックしてJDKのsrc.zipを追加する。
$HOME/.sdkman/candidates/java/11.0.9.hs-adpt/lib/src.zip
Add Folder
ボタンをクリックして解析するsourceを追加する。
※デモアプリの場合 [working directory]/jitwatch/core/src/main/java
Class locations
Add Folder
ボタンをクリックして解析するclassを追加する 。
※デモアプリの場合 [working directory]/jitwatch/core/build/classes/java/main
Source locationsとClass locationsを設定後、Save
ボタンをクリックする。
JITWatchで解析
Open Log
ボタンからHotSpotログファイルを選択して、Start
ボタンをクリックする 。
解析が終わると、Compilations TimelineやTriViewなどが見れる。
詳細はこちら
https://www.chrisnewland.com/images/jitwatch/HotSpot_Profiling_Using_JITWatch.pdf
参考
https://github.com/AdoptOpenJDK/jitwatch
https://www.morling.dev/blog/building-hsdis-for-openjdk-15/
https://www.sakatakoichi.com/entry/2014/12/04/202747
https://www.sakatakoichi.com/entry/2016/06/01/180742
https://www.oracle.com/webfolder/technetwork/jp/javamagazine/Java-MA15-Architect-newland.pdf
GradleでJava 14のプレビュー機能を試す
GradleでJava 14のプレビュー機能を試したときのメモ。
Gradleは6.3以降を使用する。
Gradle 6.3 Release Notes
IntelliJ IDEAでJava 14を使う場合は、2020.1以降を使用する。
Java 14 and IntelliJ IDEA | The IntelliJ IDEA Blog
ソースコード
build.gradle
plugins { id 'java' id 'application' } group 'com.example' version '1.0-SNAPSHOT' sourceCompatibility = JavaVersion.VERSION_14 repositories { mavenCentral() } tasks.withType(JavaCompile) { options.compilerArgs += ['--enable-preview'] } mainClassName = 'com.example.Main' run { jvmArgs = ['--enable-preview'] } test { jvmArgs = ['--enable-preview'] }
Main.java
今回はRecordsを試す
JEP 359: Records (Preview)
package com.example; public class Main { public static void main(String... args) { Point point = new Point(1, 2); System.out.println(point); Range range = new Range(1, 2); System.out.println(range); } record Point(int x, int y) {} record Range(int lo, int hi) { public Range { if (lo > hi) { throw new IllegalArgumentException("(%d,%d)".formatted(lo, hi)); } } } }
実行結果
% ./gradlew run > Task :run Point[x=1, y=2] Range[lo=1, hi=2] BUILD SUCCESSFUL in 1s
GraalVMのNative Imageを試す
GraalVMのNative Imageを試したときのメモ。
ソースコード
public class HelloWorld { static { System.out.println("Static initialization"); } public static void main(String[] args) { System.out.println("Hello world!"); } }
Demo
DockerのGraalVMを使う場合
カレントディレクトリにHelloWorld.javaを置いて、コンテナを起動する
% docker run -v `pwd`:/tmp -it oracle/graalvm-ce bash
native-imageコマンドをインストールする
bash-4.2# gu install native-image
bash-4.2# cd tmp bash-4.2# mkdir out bash-4.2# javac -d out/ HelloWorld.java
Native Imageを生成して実行する
bash-4.2# native-image -cp out/ HelloWorld Build on Server(pid: 113, port: 42993) [helloworld:113] classlist: 208.02 ms [helloworld:113] (cap): 812.16 ms [helloworld:113] setup: 1,094.60 ms [helloworld:113] (typeflow): 2,439.05 ms [helloworld:113] (objects): 2,606.36 ms [helloworld:113] (features): 118.09 ms [helloworld:113] analysis: 5,267.27 ms [helloworld:113] (clinit): 61.06 ms [helloworld:113] universe: 190.47 ms [helloworld:113] (parse): 232.66 ms [helloworld:113] (inline): 653.02 ms [helloworld:113] (compile): 1,515.33 ms [helloworld:113] compile: 2,571.28 ms [helloworld:113] image: 216.05 ms [helloworld:113] write: 338.25 ms [helloworld:113] [total]: 10,010.49 ms
bash-4.2# time ./helloworld Static initialization Hello world! real 0m0.015s user 0m0.001s sys 0m0.003s
--initialize-at-build-time を指定した場合
bash-4.2# native-image --initialize-at-build-time=HelloWorld -cp out/ HelloWorld Build on Server(pid: 113, port: 42993) [helloworld:113] classlist: 161.72 ms Static initialization [helloworld:113] (cap): 784.22 ms [helloworld:113] setup: 1,035.82 ms [helloworld:113] (typeflow): 2,543.74 ms [helloworld:113] (objects): 2,808.22 ms [helloworld:113] (features): 108.06 ms [helloworld:113] analysis: 5,550.17 ms [helloworld:113] (clinit): 61.06 ms [helloworld:113] universe: 226.54 ms [helloworld:113] (parse): 238.89 ms [helloworld:113] (inline): 757.67 ms [helloworld:113] (compile): 1,525.27 ms [helloworld:113] compile: 2,684.69 ms [helloworld:113] image: 213.52 ms [helloworld:113] write: 328.43 ms [helloworld:113] [total]: 10,317.39 ms
bash-4.2# time ./helloworld Hello world! real 0m0.016s user 0m0.000s sys 0m0.005s
Macで実行する場合
SDKMANでGraalVMをインストールする
% sdk install java 20.1.0.r11-grl % sdk use java 20.1.0.r11-grl
native-imageコマンドをインストールする
% gu install native-image
% javac HelloWorld.java
Native Imageを生成して実行する
% native-image HelloWorld
% ./helloworld Static initialization Hello world!
Spring BootでRxJava2を使う
Spring Boot 2はRxJava2をサポートしているので、Spring MVCとSpring WebFluxどちらでも、Controllerの戻り値にFlowableなどを指定できる。
ソースコード
dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'io.reactivex.rxjava2:rxjava' }
@RestController public class HelloController { @GetMapping("/hello1") public Single<String> hello1() { return Single.just("Hello!"); } @GetMapping("/hello2") public Flowable<String> hello2() { return Flowable.fromIterable(List.of("Hello!", "Hello!")); } @GetMapping(value = "/hello3", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flowable<String> hello3() { return Flowable.fromIterable(List.of("Hello!", "Hello!")); } }
参考
- https://docs.spring.io/spring-framework/docs/5.2.1.RELEASE/spring-framework-reference/web.html#mvc-ann-return-types
- https://docs.spring.io/spring-framework/docs/5.2.1.RELEASE/spring-framework-reference/web.html#mvc-ann-async-reactive-types
- https://docs.spring.io/spring-framework/docs/5.2.1.RELEASE/spring-framework-reference/web-reactive.html#webflux-ann-return-types
Spring BootとArmeriaでHTTP/2のh2cを試す
Spring BootはHTTP/2のh2c (HTTP/2 over TCP)をサポートしていないが、Armeriaを組み合わせると使用できる。
追記
- Spring Boot 2.3.5からh2cの設定方法がドキュメントに記載されたので、Armeriaを使わなくても試せるようになった。
https://docs.spring.io/spring-boot/docs/2.4.x/reference/html/howto.html#howto-configure-http2-h2c
- Spring Boot 2.5からはserver.http2.enabled
をtrue
にするだけでh2cを使用できるようになった。
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.5-Release-Notes
準備
ソースコード
Spring Initializrでプロジェクトを作成する。
https://start.spring.io/
application.propeties
http2を有効にする。
server.http2.enabled=true
build.gradle
Armeriaのdependenciesを追加する。
dependencies { implementation platform('com.linecorp.armeria:armeria-bom:1.0.0') implementation 'com.linecorp.armeria:armeria-spring-boot2-webflux-starter' }
HelloController.java
Controllerを追加する。
@RestController public class HelloController { @GetMapping("/hello") public Mono<String> hello() { return Mono.just("Hello!"); } }
動作確認
ブラウザはh2cをサポートしていないため、curlコマンドで確認する。
% curl -v --http2 http://localhost:8080/hello * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 8080 (#0) > GET /hello HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.54.0 > Accept: */* > Connection: Upgrade, HTTP2-Settings > Upgrade: h2c > HTTP2-Settings: AAMAAABkAARAAAAAAAIAAAAA > < HTTP/1.1 101 Switching Protocols < connection: upgrade < upgrade: h2c * Received 101 * Using HTTP2, server supports multi-use * Connection state changed (HTTP/2 confirmed) * Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=84 * Connection state changed (MAX_CONCURRENT_STREAMS updated)! < HTTP/2 200 < content-type: text/plain;charset=UTF-8 < content-length: 6 < * Connection #0 to host localhost left intact Hello!
h2cなので、Wiresharkでも確認できる。
参考
https://docs.spring.io/spring-boot/docs/2.2.1.RELEASE/reference/html/howto.html#howto-configure-http2
https://armeria.dev/docs/advanced-spring-webflux-integration
https://github.com/spring-projects/spring-boot/issues/21997