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"}
参考
- https://stackoverflow.com/questions/18043587/why-im-not-able-to-unwrap-and-serialize-a-java-map-using-the-jackson-java-libra
- https://github.com/FasterXML/jackson-databind/issues/171
- http://fasterxml.github.io/jackson-annotations/javadoc/2.13/com/fasterxml/jackson/annotation/JsonUnwrapped.html
- http://fasterxml.github.io/jackson-annotations/javadoc/2.13/com/fasterxml/jackson/annotation/JsonAnyGetter.html
- http://fasterxml.github.io/jackson-annotations/javadoc/2.13/com/fasterxml/jackson/annotation/JsonAnySetter.html
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
参考
- https://docs.docker.com/desktop/mac/networking/
- Prometheus, Micrometer
- https://prometheus.io/docs/prometheus/latest/installation/#using-docker
- https://micrometer.io/docs/ref/jvm
- https://github.com/micrometer-metrics/micrometer/blob/main/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmMemoryMetrics.java
- https://docs.oracle.com/en/java/javase/17/docs/api/java.management/java/lang/management/MemoryPoolMXBean.html
- JFR
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
- 検証用のアプリを実行する。
- 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を呼ぶ。
- https://github.com/openjdk/jdk17u/blob/jdk17.0.2/src/java.base/share/classes/java/util/AbstractSet.java#L95
- https://github.com/openjdk/jdk17u/blob/jdk17.0.2/src/java.base/share/classes/java/util/AbstractCollection.java#L309
HashSet
HashSetのcontainsは、内部で保持しているHashMapのcontainsKeyを呼び、==
またはkey.equals
で比較する。
- https://github.com/openjdk/jdk17u/blob/jdk17.0.2/src/java.base/share/classes/java/util/HashSet.java#L204
- https://github.com/openjdk/jdk17u/blob/jdk17.0.2/src/java.base/share/classes/java/util/HashMap.java#L593
- https://github.com/openjdk/jdk17u/blob/jdk17.0.2/src/java.base/share/classes/java/util/HashMap.java#L565
TreeSet
TreeSetのcontainsは、内部で保持しているTreeMapのcontainsKeyを呼び、compareTo(またはcompare)で比較する。
そのため、String.CASE_INSENSITIVE_ORDERなどを使っていると、オブジェクトのequalsと結果が変わってくる。
- https://github.com/openjdk/jdk17u/blob/jdk17.0.2/src/java.base/share/classes/java/util/TreeSet.java#L233
- https://github.com/openjdk/jdk17u/blob/jdk17.0.2/src/java.base/share/classes/java/util/TreeMap.java#L232
- https://github.com/openjdk/jdk17u/blob/jdk17.0.2/src/java.base/share/classes/java/util/TreeMap.java#L341
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なしにする。
- https://fasterxml.github.io/jackson-databind/javadoc/2.13/com/fasterxml/jackson/databind/annotation/JsonDeserialize.html
- https://fasterxml.github.io/jackson-databind/javadoc/2.13/com/fasterxml/jackson/databind/annotation/JsonPOJOBuilder.html
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; }