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();
    }
}

参考

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

Javaコードをコンパイルする

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 

Javaコードをコンパイルする

% 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!"));
    }
}

参考

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.enabledtrueにするだけで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