JacksonでJSONを別の階層のプロパティにマップする

具体的にはJacksonでMapにデシリアライズできるJSONを、こんなクラスにマップしたいときの備忘録。

{
  "key1": "value1",
  "key2": "value2",
  ...
}
public class Response {
  private Map<String, String> map;
}

Mapに@JsonUnwrappedは使えないので、@JsonAnySetterを使う。シリアライズも必要な場合は@JsonAnyGetterも使う。

import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnore;

import lombok.Value;

@Value
public class Response {
    @JsonIgnore
    Map<String, String> map = new HashMap<>();

    @JsonAnySetter
    public void set(String key, String value) {
        map.put(key, value);
    }

    @JsonAnyGetter
    public Map<String, String> get() {
        return map;
    }
}
public class Main {
    public static void main(String[] args) throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        String json = "{\"key1\":\"value1\",\"key2\":\"value2\"}";
        Response response = objectMapper.readValue(json, Response.class);
        System.out.println(response);
        System.out.println(objectMapper.writeValueAsString(response));
    }
}

実行結果

Response(map={key1=value1, key2=value2})
{"key1":"value1","key2":"value2"}

参考

Javaアプリのメモリリークを調べる

JavaアプリのメモリリークをPrometheusとJDK Flight Recorderを使って調べるときのメモ。

準備

ソースコード

メモリリークがあるSpring Bootのデモアプリ。

build.gradle

plugins {
    id 'org.springframework.boot' version '2.6.7'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'io.micrometer:micrometer-registry-prometheus'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

application.properties

Prometheusでメトリックスを見るために、Spring Boot ActuatorのPrometheusエンドポイントを有効にする。

management.endpoints.web.exposure.include=prometheus

DemoApplication.java

メモリリークを発生させるために、1MBのオブジェクトを1秒毎に生成して破棄されないようにListに追加する。

@EnableScheduling
@SpringBootApplication
public class DemoApplication {
    private final List<Demo> list = new ArrayList<>();

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Scheduled(fixedRate = 1000)
    public void run() {
        list.add(new Demo());
    }

    private static class Demo {
        private final byte[] buff = new byte[1_000_000];
    }
}

Prometheus

prometheus.yml

今回はDocker Desktop for MacでPrometheusを使うので、コンテナからホストにアクセスするためにhost.docker.internalを使う。

scrape_configs:
  - job_name: spring-boot
    scrape_interval: 5s
    metrics_path: /actuator/prometheus
    static_configs:
      - targets: ['host.docker.internal:8080']

Demo

prometheus.ymlをDockerでファイル共有できるディレクトリに置いてから、Prometheusを起動する。

% cp prometheus.yml /tmp

% docker run -p 9090:9090 -v /tmp/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus

今回はTemurin 17.0.3を使う。

% sdk use java 17.0.3-tem   

検証用にヒープサイズを256MBにして、デモアプリを起動する。

% java -Xms256m -Xmx256m -jar build/libs/demo-0.0.1-SNAPSHOT.jar

Prometheus

localhost:9090のPrometheusにアクセスしてヒープの使用量を見ると、old領域が徐々に増えているのが確認できる。
(最終的には256MBに到達してOOMが発生する)

JDK Flight Recorder

JFRファイルに記録する。

# 起動時から記録する場合
% java -Xms256m -Xmx256m -XX:StartFlightRecording -jar build/libs/demo-0.0.1-SNAPSHOT.jar  

# 起動後に記録する場合
% jcmd <pid> JFR.start

# ファイルに保存する
% jcmd <pid> JFR.dump filename=recording.jfr path-to-gc-roots=true

JDK Mission ControlでJFRファイルを開くと、メモリリークしているオブジェクトが確認できる。

デバッグ用にstack traceも記録したい場合は、settings=profileを指定する。

% java -Xms256m -Xmx256m -XX:StartFlightRecording:settings=profile -jar build/libs/demo-0.0.1-SNAPSHOT.jar  

or

% jcmd <pid> JFR.start settings=profile

参考

Wiresharkで任意のHTTP Headerでフィルターする

Wiresharkで任意のHTTP Headerでフィルターしたいときの備忘録。

HTTPのフィルターが用意されている場合

X-Forwarded-ForヘッダーのようにHTTPのフィルターが用意されている場合は、リファレンスに記載されているFIELD NAMEを指定すればフィルターできる。

HTTPのフィルターが用意されていない場合

HTTPのフィルターが用意されていない場合は、Custom HTTP Header Fieldsに登録するか、http containsを使う。

Custom HTTP Header Fieldsを使う場合

Preferences > Protocols > HTTP > Custom HTTP headers fields から、Custom HTTP Headerを登録する。

Wiresharkを再起動後、http.header.[Header name] でフィルターできる。

http containsを使う場合

Custom HTTP Header Fieldを登録したくない場合は、http contains "[Header name]: [Header value]"でもフィルターできる。

参考

Javaのasync-profilerを試す

Javaのasync-profilerを試したときのメモ。
IntelliJ IDEA Ultimateでも試せるが、今回はツールを直接実行する)

準備

async-profiler

ここからasync-profilerをダウンロードする。
実際には、ダウンロードしたファイル含まれているprofiler.shを使う。

検証用のソースコード

Application.java

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;

@SpringBootApplication
@EnableScheduling
public class Application {

    @Scheduled(fixedRate = 5000)
    public void loop1() {
        for (int i = 0; i < 1_000_000_000; i++) {}
    }

    @Scheduled(fixedRate = 5000)
    public void loop2() {
        for (int i = 0; i < 500_000_000; i++) {}
    }

    @Scheduled(fixedRate = 5000)
    public void loop3() {
        for (int i = 0; i < 250_000_000; i++) {}
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

build.gradle

plugins {
    id 'org.springframework.boot' version '2.6.4'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
}

application.properties

spring.task.scheduling.pool.size=3

Demo

  1. 検証用のアプリを実行する。
  2. duration(今回は30秒)とアプリのPIDを指定してprofiler.shを実行する。
% jcmd
2202 org.gradle.launcher.daemon.bootstrap.GradleDaemon 7.4
15962 jdk.jcmd/sun.tools.jcmd.JCmd
15742 com.example.Application
% ./profiler.sh -d 30 15742 
Profiling for 30 seconds
Done
--- Execution profile ---
Total samples       : 488

--- 2690000000 ns (55.12%), 269 samples
  [ 0] com.example.Application.loop1
  [ 1] jdk.internal.reflect.NativeMethodAccessorImpl.invoke0
  [ 2] jdk.internal.reflect.NativeMethodAccessorImpl.invoke
  [ 3] jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke
  [ 4] java.lang.reflect.Method.invoke
  [ 5] org.springframework.scheduling.support.ScheduledMethodRunnable.run
  [ 6] org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run
  [ 7] java.util.concurrent.Executors$RunnableAdapter.call
  [ 8] java.util.concurrent.FutureTask.runAndReset
  [ 9] java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run
  [10] java.util.concurrent.ThreadPoolExecutor.runWorker
  [11] java.util.concurrent.ThreadPoolExecutor$Worker.run
  [12] java.lang.Thread.run

--- 700000000 ns (14.34%), 70 samples
  [ 0] com.example.Application.loop2
  [ 1] jdk.internal.reflect.NativeMethodAccessorImpl.invoke0
  [ 2] jdk.internal.reflect.NativeMethodAccessorImpl.invoke
  [ 3] jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke
  [ 4] java.lang.reflect.Method.invoke
  [ 5] org.springframework.scheduling.support.ScheduledMethodRunnable.run
  [ 6] org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run
  [ 7] java.util.concurrent.Executors$RunnableAdapter.call
  [ 8] java.util.concurrent.FutureTask.runAndReset
  [ 9] java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run
  [10] java.util.concurrent.ThreadPoolExecutor.runWorker
  [11] java.util.concurrent.ThreadPoolExecutor$Worker.run
  [12] java.lang.Thread.run

--- 690000000 ns (14.14%), 69 samples
  [ 0] com.example.Application.loop2
  [ 1] jdk.internal.reflect.NativeMethodAccessorImpl.invoke0
  [ 2] jdk.internal.reflect.NativeMethodAccessorImpl.invoke
  [ 3] jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke
  [ 4] java.lang.reflect.Method.invoke
  [ 5] org.springframework.scheduling.support.ScheduledMethodRunnable.run
  [ 6] org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run
  [ 7] java.util.concurrent.Executors$RunnableAdapter.call
  [ 8] java.util.concurrent.FutureTask.runAndReset
  [ 9] java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run
  [10] java.util.concurrent.ThreadPoolExecutor.runWorker
  [11] java.util.concurrent.ThreadPoolExecutor$Worker.run
  [12] java.lang.Thread.run

--- 370000000 ns (7.58%), 37 samples
  [ 0] com.example.Application.loop3
  [ 1] jdk.internal.reflect.NativeMethodAccessorImpl.invoke0
  [ 2] jdk.internal.reflect.NativeMethodAccessorImpl.invoke
  [ 3] jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke
  [ 4] java.lang.reflect.Method.invoke
  [ 5] org.springframework.scheduling.support.ScheduledMethodRunnable.run
  [ 6] org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run
  [ 7] java.util.concurrent.Executors$RunnableAdapter.call
  [ 8] java.util.concurrent.FutureTask.runAndReset
  [ 9] java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run
  [10] java.util.concurrent.ThreadPoolExecutor.runWorker
  [11] java.util.concurrent.ThreadPoolExecutor$Worker.run
  [12] java.lang.Thread.run

--- 370000000 ns (7.58%), 37 samples
  [ 0] com.example.Application.loop3
  [ 1] jdk.internal.reflect.NativeMethodAccessorImpl.invoke0
  [ 2] jdk.internal.reflect.NativeMethodAccessorImpl.invoke
  [ 3] jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke
  [ 4] java.lang.reflect.Method.invoke
  [ 5] org.springframework.scheduling.support.ScheduledMethodRunnable.run
  [ 6] org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run
  [ 7] java.util.concurrent.Executors$RunnableAdapter.call
  [ 8] java.util.concurrent.FutureTask.runAndReset
  [ 9] java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run
  [10] java.util.concurrent.ThreadPoolExecutor.runWorker
  [11] java.util.concurrent.ThreadPoolExecutor$Worker.run
  [12] java.lang.Thread.run

--- 20000000 ns (0.41%), 2 samples
  [ 0] com.example.Application.loop1
  [ 1] jdk.internal.reflect.NativeMethodAccessorImpl.invoke0
  [ 2] jdk.internal.reflect.NativeMethodAccessorImpl.invoke
  [ 3] jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke
  [ 4] java.lang.reflect.Method.invoke
  [ 5] org.springframework.scheduling.support.ScheduledMethodRunnable.run
  [ 6] org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run
  [ 7] java.util.concurrent.Executors$RunnableAdapter.call
  [ 8] java.util.concurrent.FutureTask.runAndReset
  [ 9] java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run
  [10] java.util.concurrent.ThreadPoolExecutor.runWorker
  [11] java.util.concurrent.ThreadPoolExecutor$Worker.run
  [12] java.lang.Thread.run

--- 10000000 ns (0.20%), 1 sample
  [ 0] com.example.Application.loop2
  [ 1] jdk.internal.reflect.NativeMethodAccessorImpl.invoke0
  [ 2] jdk.internal.reflect.NativeMethodAccessorImpl.invoke
  [ 3] jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke
  [ 4] java.lang.reflect.Method.invoke
  [ 5] org.springframework.scheduling.support.ScheduledMethodRunnable.run
  [ 6] org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run
  [ 7] java.util.concurrent.Executors$RunnableAdapter.call
  [ 8] java.util.concurrent.FutureTask.runAndReset
  [ 9] java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run
  [10] java.util.concurrent.ThreadPoolExecutor.runWorker
  [11] java.util.concurrent.ThreadPoolExecutor$Worker.run
  [12] java.lang.Thread.run

--- 10000000 ns (0.20%), 1 sample
  [ 0] __psynch_cvwait
  [ 1] os::PlatformEvent::park(long)
  [ 2] Monitor::IWait(Thread*, long)
  [ 3] Monitor::wait(bool, long, bool)
  [ 4] WatcherThread::sleep() const
  [ 5] WatcherThread::run()
  [ 6] Thread::call_run()
  [ 7] thread_native_entry(Thread*)
  [ 8] _pthread_start
  [ 9] thread_start

--- 10000000 ns (0.20%), 1 sample
  [ 0] close
  [ 1] attach_listener_thread_entry(JavaThread*, Thread*)
  [ 2] JavaThread::thread_main_inner()
  [ 3] JavaThread::run()
  [ 4] Thread::call_run()
  [ 5] thread_native_entry(Thread*)
  [ 6] _pthread_start
  [ 7] thread_start

--- 10000000 ns (0.20%), 1 sample
  [ 0] Monitor::unlock()
  [ 1] G1YoungRemSetSamplingThread::run_service()
  [ 2] ConcurrentGCThread::run()
  [ 3] Thread::call_run()
  [ 4] thread_native_entry(Thread*)
  [ 5] _pthread_start
  [ 6] thread_start

          ns  percent  samples  top
  ----------  -------  -------  ---
  2710000000   55.53%      271  com.example.Application.loop1
  1400000000   28.69%      140  com.example.Application.loop2
   740000000   15.16%       74  com.example.Application.loop3
    10000000    0.20%        1  Monitor::unlock()
    10000000    0.20%        1  __psynch_cvwait
    10000000    0.20%        1  close

Flame Graph

拡張子.htmlで保存すると、Flame Graphのフォーマットに自動変換される。

% ./profiler.sh -d 30 -f flamegraph.html 15742 
Profiling for 30 seconds
Done

flamegraph.html

参考

GradleでSpring Bootをビルドしたときのjarファイル

Spring Boot 2.5からGradleでビルドすると *.jar*-plain.jar の2つが生成されるようになったので、これらについてのメモ。

Demo

Spring InitializrからGradle Projectを選択してプロジェクトを作成する。(ArtifactなどのProject Metadataはデフォルトのまま)

% tree .
.
├── HELP.md
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── example
    │   │           └── demo
    │   │               └── DemoApplication.java
    │   └── resources
    │       └── application.properties
    └── test
        └── java
            └── com
                └── example
                    └── demo
                        └── DemoApplicationTests.java

14 directories, 10 files

build.gradle

plugins {
    id 'org.springframework.boot' version '2.6.3'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

このプロジェクトをビルドすると、2つのjarファイルができる。

  • demo-0.0.1-SNAPSHOT.jar
  • demo-0.0.1-SNAPSHOT-plain.jar

demo-0.0.1-SNAPSHOT.jar

bootJarタスクによって生成されるSpring Boot fat jarで、全てのモジュールのdependenciesを含んでいるため、java -jar demo-0.0.1-SNAPSHOT.jar で実行できる。

% jar tf build/libs/demo-0.0.1-SNAPSHOT.jar
META-INF/
META-INF/MANIFEST.MF
org/
org/springframework/
org/springframework/boot/
org/springframework/boot/loader/
org/springframework/boot/loader/ClassPathIndexFile.class
org/springframework/boot/loader/ExecutableArchiveLauncher.class
org/springframework/boot/loader/JarLauncher.class
org/springframework/boot/loader/LaunchedURLClassLoader$DefinePackageCallType.class
org/springframework/boot/loader/LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
org/springframework/boot/loader/LaunchedURLClassLoader.class
org/springframework/boot/loader/Launcher.class
org/springframework/boot/loader/MainMethodRunner.class
org/springframework/boot/loader/PropertiesLauncher$1.class
org/springframework/boot/loader/PropertiesLauncher$ArchiveEntryFilter.class
org/springframework/boot/loader/PropertiesLauncher$ClassPathArchives.class
org/springframework/boot/loader/PropertiesLauncher$PrefixMatchingArchiveFilter.class
org/springframework/boot/loader/PropertiesLauncher.class
org/springframework/boot/loader/WarLauncher.class
org/springframework/boot/loader/archive/
org/springframework/boot/loader/archive/Archive$Entry.class
org/springframework/boot/loader/archive/Archive$EntryFilter.class
org/springframework/boot/loader/archive/Archive.class
org/springframework/boot/loader/archive/ExplodedArchive$AbstractIterator.class
org/springframework/boot/loader/archive/ExplodedArchive$ArchiveIterator.class
org/springframework/boot/loader/archive/ExplodedArchive$EntryIterator.class
org/springframework/boot/loader/archive/ExplodedArchive$FileEntry.class
org/springframework/boot/loader/archive/ExplodedArchive$SimpleJarFileArchive.class
org/springframework/boot/loader/archive/ExplodedArchive.class
org/springframework/boot/loader/archive/JarFileArchive$AbstractIterator.class
org/springframework/boot/loader/archive/JarFileArchive$EntryIterator.class
org/springframework/boot/loader/archive/JarFileArchive$JarFileEntry.class
org/springframework/boot/loader/archive/JarFileArchive$NestedArchiveIterator.class
org/springframework/boot/loader/archive/JarFileArchive.class
org/springframework/boot/loader/data/
org/springframework/boot/loader/data/RandomAccessData.class
org/springframework/boot/loader/data/RandomAccessDataFile$1.class
org/springframework/boot/loader/data/RandomAccessDataFile$DataInputStream.class
org/springframework/boot/loader/data/RandomAccessDataFile$FileAccess.class
org/springframework/boot/loader/data/RandomAccessDataFile.class
org/springframework/boot/loader/jar/
org/springframework/boot/loader/jar/AbstractJarFile$JarFileType.class
org/springframework/boot/loader/jar/AbstractJarFile.class
org/springframework/boot/loader/jar/AsciiBytes.class
org/springframework/boot/loader/jar/Bytes.class
org/springframework/boot/loader/jar/CentralDirectoryEndRecord$1.class
org/springframework/boot/loader/jar/CentralDirectoryEndRecord$Zip64End.class
org/springframework/boot/loader/jar/CentralDirectoryEndRecord$Zip64Locator.class
org/springframework/boot/loader/jar/CentralDirectoryEndRecord.class
org/springframework/boot/loader/jar/CentralDirectoryFileHeader.class
org/springframework/boot/loader/jar/CentralDirectoryParser.class
org/springframework/boot/loader/jar/CentralDirectoryVisitor.class
org/springframework/boot/loader/jar/FileHeader.class
org/springframework/boot/loader/jar/Handler.class
org/springframework/boot/loader/jar/JarEntry.class
org/springframework/boot/loader/jar/JarEntryCertification.class
org/springframework/boot/loader/jar/JarEntryFilter.class
org/springframework/boot/loader/jar/JarFile$1.class
org/springframework/boot/loader/jar/JarFile$JarEntryEnumeration.class
org/springframework/boot/loader/jar/JarFile.class
org/springframework/boot/loader/jar/JarFileEntries$1.class
org/springframework/boot/loader/jar/JarFileEntries$EntryIterator.class
org/springframework/boot/loader/jar/JarFileEntries$Offsets.class
org/springframework/boot/loader/jar/JarFileEntries$Zip64Offsets.class
org/springframework/boot/loader/jar/JarFileEntries$ZipOffsets.class
org/springframework/boot/loader/jar/JarFileEntries.class
org/springframework/boot/loader/jar/JarFileWrapper.class
org/springframework/boot/loader/jar/JarURLConnection$1.class
org/springframework/boot/loader/jar/JarURLConnection$JarEntryName.class
org/springframework/boot/loader/jar/JarURLConnection.class
org/springframework/boot/loader/jar/StringSequence.class
org/springframework/boot/loader/jar/ZipInflaterInputStream.class
org/springframework/boot/loader/jarmode/
org/springframework/boot/loader/jarmode/JarMode.class
org/springframework/boot/loader/jarmode/JarModeLauncher.class
org/springframework/boot/loader/jarmode/TestJarMode.class
org/springframework/boot/loader/util/
org/springframework/boot/loader/util/SystemPropertyUtils.class
BOOT-INF/
BOOT-INF/classes/
BOOT-INF/classes/com/
BOOT-INF/classes/com/example/
BOOT-INF/classes/com/example/demo/
BOOT-INF/classes/com/example/demo/DemoApplication.class
BOOT-INF/classes/application.properties
BOOT-INF/lib/
BOOT-INF/lib/spring-boot-autoconfigure-2.6.3.jar
BOOT-INF/lib/spring-boot-2.6.3.jar
BOOT-INF/lib/jakarta.annotation-api-1.3.5.jar
BOOT-INF/lib/spring-context-5.3.15.jar
BOOT-INF/lib/spring-aop-5.3.15.jar
BOOT-INF/lib/spring-beans-5.3.15.jar
BOOT-INF/lib/spring-expression-5.3.15.jar
BOOT-INF/lib/spring-core-5.3.15.jar
BOOT-INF/lib/snakeyaml-1.29.jar
BOOT-INF/lib/logback-classic-1.2.10.jar
BOOT-INF/lib/log4j-to-slf4j-2.17.1.jar
BOOT-INF/lib/jul-to-slf4j-1.7.33.jar
BOOT-INF/lib/spring-jcl-5.3.15.jar
BOOT-INF/lib/logback-core-1.2.10.jar
BOOT-INF/lib/slf4j-api-1.7.33.jar
BOOT-INF/lib/log4j-api-2.17.1.jar
BOOT-INF/lib/spring-boot-jarmode-layertools-2.6.3.jar
BOOT-INF/classpath.idx
BOOT-INF/layers.idx

demo-0.0.1-SNAPSHOT-plain.jar

jarタスクによって生成されるplain jarファイルで、このモジュールのclassとresourceのみを含んでいる。

% jar tf build/libs/demo-0.0.1-SNAPSHOT-plain.jar
META-INF/
META-INF/MANIFEST.MF
com/
com/example/
com/example/demo/
com/example/demo/DemoApplication.class
application.properties

plain jarを生成したくない場合は、build.gradleにこの設定を追加する。

jar {
    enabled = false
}

参考

JavaのSortedSet

JavaのSortedSetについての備忘録。
HashSetなどの通常のSetは要素比較にオブジェクトのequalsを使うが、TreeSetなどのSortedSetは要素比較にcompareTo(またはcompare)を使う。そのため、SortedSetにComparatorを設定している場合は、要素比較の結果がHashSetなどとは異なることがある。

OpenJDKのソースコード

Setのequalsはcontainsを呼ぶ。

HashSet

HashSetのcontainsは、内部で保持しているHashMapのcontainsKeyを呼び、==またはkey.equalsで比較する。

TreeSet

TreeSetのcontainsは、内部で保持しているTreeMapのcontainsKeyを呼び、compareTo(またはcompare)で比較する。
そのため、String.CASE_INSENSITIVE_ORDERなどを使っていると、オブジェクトのequalsと結果が変わってくる。

Demo

% java -version
openjdk version "17.0.2" 2022-01-18
OpenJDK Runtime Environment Temurin-17.0.2+8 (build 17.0.2+8)
OpenJDK 64-Bit Server VM Temurin-17.0.2+8 (build 17.0.2+8, mixed mode, sharing)

String.CASE_INSENSITIVE_ORDERを使ったTreeSetを作成して、初期データ(a, B, c)を追加する

jshell> Set<String> tree = new TreeSet<>(String.CASE_INSENSITIVE_ORDER)
tree ==> []

jshell> tree.add("a")
$2 ==> true

jshell> tree.add("c")
$3 ==> true

jshell> tree.add("B")
$4 ==> true

jshell> tree
tree ==> [a, B, c]

case insensitiveなので、大文字・小文字は区別されない

jshell> tree.contains("a")
$6 ==> true

jshell> tree.contains("A")
$7 ==> true

jshell> String.CASE_INSENSITIVE_ORDER.compare("a", "A")
$8 ==> 0

このTreeSetから作ったHashSetは、大文字・小文字が区別される

jshell> Set<String> hash = new HashSet<>(tree)
hash ==> [a, B, c]

jshell> hash.contains("a")
$12 ==> true

jshell>  hash.contains("A")
$13 ==> false

2つのSetは同じデータを持っているので、equalsはどちらもtrueになる

jshell> tree.equals(hash)
$14 ==> true

jshell> hash.equals(tree)
$15 ==> true

この状態から片方のSetの要素を大文字に変更する
(HashSetから小文字のcを削除して、大文字のCを追加する)

jshell> hash.remove("c")
$16 ==> true

jshell> hash.add("C")
$17 ==> true

jshell> hash
hash ==> [a, B, C]

jshell> tree
tree ==> [a, B, c]

大文字・小文字の違いがあるためHashSetのequalsはfalse になるが、TreeSetはcase insensitiveのためtrueになる

jshell>  hash.equals(tree)
$21 ==> false

jshell>  tree.equals(hash)
$22 ==> true

参考

JacksonとLombokの組み合わせ

JacksonとLombokの組み合わせについての備忘録。

ソースコード

plugins {
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = JavaVersion.VERSION_11

repositories {
    mavenCentral()
}

dependencies {
    annotationProcessor 'org.projectlombok:lombok:1.18.22'
    compileOnly 'org.projectlombok:lombok:1.18.22'
    implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.1'
}

ObjectMapperでJSONの文字列をUserクラスにデシリアライズする。

public class Main {
    public static void main(String[] args) throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        String json = "{\"id\":1,\"name\":\"name1\"}";
        User user = objectMapper.readValue(json, User.class);
        System.out.println(user);
    }
}

パターン

@Data

引数なしのデフォルトコンストラクタが生成されるので、デシリアライズできる。

import lombok.Data;

@Data
public class User {
    private long id;
    private String name;
}

@Data + @AllArgsConstructor + @NoArgsConstructor

@AllArgsConstructorを追加した場合はデフォルトコンストラクタが生成されないので、@NoArgsConstructorも追加する。

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private long id;
    private String name;
}

@Data + @AllArgsConstructor + @ConstructorProperties (lombok.config)

ConstructorPropertiesを使う場合は、lombok.configに設定を追加する。

lombok.anyConstructor.addConstructorProperties=true
import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class User {
    private long id;
    private String name;
}

@Value + @ConstructorProperties (lombok.config)

lombok.anyConstructor.addConstructorProperties=true
import lombok.Value;

@Value
public class User {
    long id;
    String name;
}

@Value + @ConstructorProperties

lombok.configを使わずに、明示的にConstructorPropertiesを設定したい場合。

import java.beans.ConstructorProperties;
import lombok.Value;

@Value
public class User {
    long id;
    String name;

    @ConstructorProperties({ "id", "name" })
    public User(long id, String name) {
        this.id = id;
        this.name = name;
    }
}

@Value + @JsonCreator

コンストラクタまたはファクトリーメソッドに@JsonCreatorを設定する。

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Value;

@Value
public class User {
    long id;
    String name;

    @JsonCreator
    public User(@JsonProperty("id") long id, @JsonProperty("name") String name) {
        this.id = id;
        this.name = name;
    }
}

@Value + @Builder + @JsonDeserialize + @JsonPOJOBuilder

@JsonPOJOBuilderのsetter名はデフォルトではwithから始まるので、Lombokのbuilderに合わせてprefixなしにする。

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import lombok.Builder;
import lombok.Value;

@Value
@Builder
@JsonDeserialize(builder = User.UserBuilder.class)
public class User {
    long id;
    String name;

    @JsonPOJOBuilder(withPrefix = "")
    public static class UserBuilder {
    }
}

@Value + @Builder + @Jacksonized

@Jacksonizedを使うと、上記と同じ設定をLombokが自動生成する。

import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;

@Value
@Builder
@Jacksonized
public class User {
    long id;
    String name;
}

参考