diff --git a/core-java-modules/core-java-jpms/decoupling-pattern2/consumermodule/pom.xml b/core-java-modules/core-java-jpms/decoupling-pattern2/consumermodule/pom.xml
index 816f5cf9e8..c7bff60a8b 100644
--- a/core-java-modules/core-java-jpms/decoupling-pattern2/consumermodule/pom.xml
+++ b/core-java-modules/core-java-jpms/decoupling-pattern2/consumermodule/pom.xml
@@ -32,6 +32,11 @@
org.apache.maven.plugins
maven-compiler-plugin
+ ${compiler.plugin.version}
+
+ ${source.version}
+ ${target.version}
+
diff --git a/core-java-modules/core-java-jpms/decoupling-pattern2/pom.xml b/core-java-modules/core-java-jpms/decoupling-pattern2/pom.xml
index 17bca54edb..91b92b9df5 100644
--- a/core-java-modules/core-java-jpms/decoupling-pattern2/pom.xml
+++ b/core-java-modules/core-java-jpms/decoupling-pattern2/pom.xml
@@ -9,6 +9,12 @@
1.0-SNAPSHOT
pom
+
+ com.baeldung.core-java-modules
+ core-java-jpms
+ 0.0.1-SNAPSHOT
+
+
servicemodule
providermodule
diff --git a/core-java-modules/core-java-jpms/decoupling-pattern2/providermodule/pom.xml b/core-java-modules/core-java-jpms/decoupling-pattern2/providermodule/pom.xml
index 3e8d5c0c39..6c0f1956b9 100644
--- a/core-java-modules/core-java-jpms/decoupling-pattern2/providermodule/pom.xml
+++ b/core-java-modules/core-java-jpms/decoupling-pattern2/providermodule/pom.xml
@@ -27,6 +27,11 @@
org.apache.maven.plugins
maven-compiler-plugin
+ ${compiler.plugin.version}
+
+ ${source.version}
+ ${target.version}
+
diff --git a/core-java-modules/core-java-jpms/decoupling-pattern2/servicemodule/pom.xml b/core-java-modules/core-java-jpms/decoupling-pattern2/servicemodule/pom.xml
index 587cb8ab4b..50201911a6 100644
--- a/core-java-modules/core-java-jpms/decoupling-pattern2/servicemodule/pom.xml
+++ b/core-java-modules/core-java-jpms/decoupling-pattern2/servicemodule/pom.xml
@@ -5,7 +5,7 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
com.baeldung.servicemodule
- servicemodule
+ servicemodule2
1.0
@@ -19,6 +19,11 @@
org.apache.maven.plugins
maven-compiler-plugin
+ ${compiler.plugin.version}
+
+ ${source.version}
+ ${target.version}
+
diff --git a/core-java-modules/core-java-jpms/pom.xml b/core-java-modules/core-java-jpms/pom.xml
index 5809c0f579..ee7eb02f10 100644
--- a/core-java-modules/core-java-jpms/pom.xml
+++ b/core-java-modules/core-java-jpms/pom.xml
@@ -20,4 +20,26 @@
decoupling-pattern2
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ ${compiler.plugin.version}
+
+ ${source.version}
+ ${target.version}
+
+
+
+
+
+
+
+ 3.8.0
+ 11
+ 11
+
+
diff --git a/core-java-modules/core-java-lang-4/README.md b/core-java-modules/core-java-lang-4/README.md
index 77d14f0fc8..8e8d6185b0 100644
--- a/core-java-modules/core-java-lang-4/README.md
+++ b/core-java-modules/core-java-lang-4/README.md
@@ -3,3 +3,4 @@
This module contains articles about core features in the Java language
- [The Java final Keyword – Impact on Performance](https://www.baeldung.com/java-final-performance)
+- [The package-info.java File](https://www.baeldung.com/java-package-info)
diff --git a/core-java-modules/core-java-lang-4/src/main/java/com/baeldung/finalkeyword/package-info.java b/core-java-modules/core-java-lang-4/src/main/java/com/baeldung/finalkeyword/package-info.java
new file mode 100644
index 0000000000..fd1e3cf8d8
--- /dev/null
+++ b/core-java-modules/core-java-lang-4/src/main/java/com/baeldung/finalkeyword/package-info.java
@@ -0,0 +1,13 @@
+/**
+* This module is about impact of the final keyword on performance
+ *
+ * This module explores if there are any performance benefits from
+ * using the final keyword in our code. This module examines the performance
+ * implications of using final on a variable, method, and class level.
+ *
+*
+* @since 1.0
+* @author baeldung
+* @version 1.1
+*/
+package com.baeldung.finalkeyword;
diff --git a/core-java-modules/core-java-lang-oop-types-2/README.md b/core-java-modules/core-java-lang-oop-types-2/README.md
new file mode 100644
index 0000000000..70a0273bc4
--- /dev/null
+++ b/core-java-modules/core-java-lang-oop-types-2/README.md
@@ -0,0 +1,7 @@
+## Core Java Lang OOP - Types
+
+This module contains articles about types in Java
+
+### Relevant Articles:
+
+-
\ No newline at end of file
diff --git a/core-java-modules/core-java-lang-oop-types-2/pom.xml b/core-java-modules/core-java-lang-oop-types-2/pom.xml
new file mode 100644
index 0000000000..94a04fe47b
--- /dev/null
+++ b/core-java-modules/core-java-lang-oop-types-2/pom.xml
@@ -0,0 +1,22 @@
+
+
+
+ core-java-modules
+ com.baeldung.core-java-modules
+ 0.0.1-SNAPSHOT
+
+ 4.0.0
+
+ core-java-lang-oop-types-2
+ jar
+
+
+
+ org.apache.commons
+ commons-lang3
+ ${commons-lang3.version}
+
+
+
\ No newline at end of file
diff --git a/core-java-modules/core-java-lang-oop-types-2/src/test/java/com/baeldung/conversions/PrimitiveToObjectArrayUnitTest.java b/core-java-modules/core-java-lang-oop-types-2/src/test/java/com/baeldung/conversions/PrimitiveToObjectArrayUnitTest.java
new file mode 100644
index 0000000000..e0f292f26e
--- /dev/null
+++ b/core-java-modules/core-java-lang-oop-types-2/src/test/java/com/baeldung/conversions/PrimitiveToObjectArrayUnitTest.java
@@ -0,0 +1,81 @@
+package com.baeldung.conversions;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class PrimitiveToObjectArrayUnitTest {
+
+ @Test
+ void givenUsingIteration_whenConvertingToObjects_thenSuccess() {
+ int[] input = new int[] { 0, 1, 2, 3, 4 };
+ Integer[] expected = new Integer[] { 0, 1, 2, 3, 4 };
+
+ Integer[] output = new Integer[input.length];
+ for (int i = 0; i < input.length; i++) {
+ output[i] = input[i];
+ }
+
+ assertArrayEquals(expected, output);
+ }
+
+ @Test
+ void givenUsingIteration_whenConvertingToPrimitives_thenSuccess() {
+ Integer[] input = new Integer[] { 0, 1, 2, 3, 4 };
+ int[] expected = new int[] { 0, 1, 2, 3, 4 };
+
+ int[] output = new int[input.length];
+ for (int i = 0; i < input.length; i++) {
+ output[i] = input[i];
+ }
+
+ assertArrayEquals(expected, output);
+ }
+
+ @Test
+ void givenUsingStreams_whenConvertingToObjects_thenSuccess() {
+ int[] input = new int[] { 0, 1, 2, 3, 4 };
+ Integer[] expected = new Integer[] { 0, 1, 2, 3, 4 };
+
+ Integer[] output = Arrays.stream(input)
+ .boxed()
+ .toArray(Integer[]::new);
+
+ assertArrayEquals(expected, output);
+ }
+
+ @Test
+ void givenUsingStreams_whenConvertingToPrimitives_thenSuccess() {
+ Integer[] input = new Integer[] { 0, 1, 2, 3, 4 };
+ int[] expected = new int[] { 0, 1, 2, 3, 4 };
+
+ int[] output = Arrays.stream(input)
+ .mapToInt(Integer::intValue)
+ .toArray();
+
+ assertArrayEquals(expected, output);
+ }
+
+ @Test
+ void givenUsingApacheCommons_whenConvertingToObjects_thenSuccess() {
+ int[] input = new int[] { 0, 1, 2, 3, 4 };
+ Integer[] expected = new Integer[] { 0, 1, 2, 3, 4 };
+
+ Integer[] output = ArrayUtils.toObject(input);
+
+ assertArrayEquals(expected, output);
+ }
+
+ @Test
+ void givenUsingApacheCommons_whenConvertingToPrimitives_thenSuccess() {
+ Integer[] input = new Integer[] { 0, 1, 2, 3, 4 };
+ int[] expected = new int[] { 0, 1, 2, 3, 4 };
+
+ int[] output = ArrayUtils.toPrimitive(input);
+
+ assertArrayEquals(expected, output);
+ }
+}
\ No newline at end of file
diff --git a/core-java-modules/core-java-nio-2/README.md b/core-java-modules/core-java-nio-2/README.md
index ef73159f66..ab54899501 100644
--- a/core-java-modules/core-java-nio-2/README.md
+++ b/core-java-modules/core-java-nio-2/README.md
@@ -9,4 +9,5 @@ This module contains articles about core Java non-blocking input and output (IO)
- [Introduction to the Java NIO Selector](https://www.baeldung.com/java-nio-selector)
- [Using Java MappedByteBuffer](https://www.baeldung.com/java-mapped-byte-buffer)
- [How to Lock a File in Java](https://www.baeldung.com/java-lock-files)
+- [Java NIO DatagramChannel](https://www.baeldung.com/java-nio-datagramchannel)
- [[<-- Prev]](/core-java-modules/core-java-nio)
diff --git a/core-java-modules/core-java-os/src/test/java/com/baeldung/java9/process/ProcessUnderstandingUnitTest.java b/core-java-modules/core-java-os/src/test/java/com/baeldung/java9/process/ProcessUnderstandingUnitTest.java
index 69b65852cc..cb5a20f184 100644
--- a/core-java-modules/core-java-os/src/test/java/com/baeldung/java9/process/ProcessUnderstandingUnitTest.java
+++ b/core-java-modules/core-java-os/src/test/java/com/baeldung/java9/process/ProcessUnderstandingUnitTest.java
@@ -101,7 +101,8 @@ class ProcessUnderstandingUnitTest {
.replace("/", File.separator));
BufferedReader output = new BufferedReader(new InputStreamReader(process.getInputStream()));
- int value = Integer.parseInt(output.readLine());
+ String line = output.readLine();
+ int value = Integer.parseInt(line);
assertEquals(3, value);
}
diff --git a/core-java-modules/core-java-security-2/README.md b/core-java-modules/core-java-security-2/README.md
index 3f9520b888..684f2504cc 100644
--- a/core-java-modules/core-java-security-2/README.md
+++ b/core-java-modules/core-java-security-2/README.md
@@ -16,4 +16,5 @@ This module contains articles about core Java Security
- [Java AES Encryption and Decryption](https://www.baeldung.com/java-aes-encryption-decryption)
- [InvalidAlgorithmParameterException: Wrong IV Length](https://www.baeldung.com/java-invalidalgorithmparameter-exception)
- [The java.security.egd JVM Option](https://www.baeldung.com/java-security-egd)
+- [RSA in Java](https://www.baeldung.com/java-rsa)
- More articles: [[<-- prev]](/core-java-modules/core-java-security)
diff --git a/core-java-modules/core-java-streams-3/README.md b/core-java-modules/core-java-streams-3/README.md
index f81dca485b..26b4dfe975 100644
--- a/core-java-modules/core-java-streams-3/README.md
+++ b/core-java-modules/core-java-streams-3/README.md
@@ -11,4 +11,5 @@ This module contains articles about the Stream API in Java.
- [Add BigDecimals using the Stream API](https://www.baeldung.com/java-stream-add-bigdecimals)
- [Should We Close a Java Stream?](https://www.baeldung.com/java-stream-close)
- [Returning Stream vs. Collection](https://www.baeldung.com/java-return-stream-collection)
+- [Convert a Java Enumeration Into a Stream](https://www.baeldung.com/java-enumeration-to-stream)
- More articles: [[<-- prev>]](/../core-java-streams-2)
diff --git a/core-java-modules/core-java-streams-3/src/main/java/com/baeldung/streams/conversion/EnumerationSpliterator.java b/core-java-modules/core-java-streams-3/src/main/java/com/baeldung/streams/conversion/EnumerationSpliterator.java
new file mode 100644
index 0000000000..5227fcb6c4
--- /dev/null
+++ b/core-java-modules/core-java-streams-3/src/main/java/com/baeldung/streams/conversion/EnumerationSpliterator.java
@@ -0,0 +1,30 @@
+package com.baeldung.streams.conversion;
+
+import java.util.Enumeration;
+import java.util.Spliterators.AbstractSpliterator;
+import java.util.function.Consumer;
+
+public class EnumerationSpliterator extends AbstractSpliterator {
+
+ private final Enumeration enumeration;
+
+ public EnumerationSpliterator(long est, int additionalCharacteristics, Enumeration enumeration) {
+ super(est, additionalCharacteristics);
+ this.enumeration = enumeration;
+ }
+
+ @Override
+ public boolean tryAdvance(Consumer super T> action) {
+ if (enumeration.hasMoreElements()) {
+ action.accept(enumeration.nextElement());
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void forEachRemaining(Consumer super T> action) {
+ while (enumeration.hasMoreElements())
+ action.accept(enumeration.nextElement());
+ }
+}
diff --git a/core-java-modules/core-java-streams-3/src/main/java/com/baeldung/streams/conversion/EnumerationStreamConversion.java b/core-java-modules/core-java-streams-3/src/main/java/com/baeldung/streams/conversion/EnumerationStreamConversion.java
new file mode 100644
index 0000000000..cf041b9426
--- /dev/null
+++ b/core-java-modules/core-java-streams-3/src/main/java/com/baeldung/streams/conversion/EnumerationStreamConversion.java
@@ -0,0 +1,16 @@
+package com.baeldung.streams.conversion;
+
+import java.util.Enumeration;
+import java.util.Spliterator;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+public class EnumerationStreamConversion {
+
+ public static Stream convert(Enumeration enumeration) {
+ EnumerationSpliterator spliterator = new EnumerationSpliterator(Long.MAX_VALUE, Spliterator.ORDERED, enumeration);
+ Stream stream = StreamSupport.stream(spliterator, false);
+
+ return stream;
+ }
+}
diff --git a/core-java-modules/core-java-streams-3/src/test/java/com/baeldung/streams/conversion/EnumerationStreamConversionUnitTest.java b/core-java-modules/core-java-streams-3/src/test/java/com/baeldung/streams/conversion/EnumerationStreamConversionUnitTest.java
new file mode 100644
index 0000000000..a075312fb5
--- /dev/null
+++ b/core-java-modules/core-java-streams-3/src/test/java/com/baeldung/streams/conversion/EnumerationStreamConversionUnitTest.java
@@ -0,0 +1,35 @@
+package com.baeldung.streams.conversion;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Vector;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.junit.Assert;
+import org.junit.jupiter.api.Test;
+
+public class EnumerationStreamConversionUnitTest {
+
+ @Test
+ public void givenEnumeration_whenConvertedToStream_thenNotNull() {
+ Vector input = new Vector<>(Arrays.asList(1, 2, 3, 4, 5));
+
+ Stream resultingStream = EnumerationStreamConversion.convert(input.elements());
+
+ Assert.assertNotNull(resultingStream);
+ }
+
+ @Test
+ public void whenConvertedToList_thenCorrect() {
+ Vector input = new Vector<>(Arrays.asList(1, 2, 3, 4, 5));
+
+ Stream stream = EnumerationStreamConversion.convert(input.elements());
+ List list = stream.filter(e -> e >= 3)
+ .collect(Collectors.toList());
+ assertThat(list, contains(3, 4, 5));
+ }
+}
diff --git a/core-java-modules/core-java-string-operations-3/src/test/java/com/baeldung/splitstringbynewline/SplitStringByNewLineUnitTest.java b/core-java-modules/core-java-string-operations-3/src/test/java/com/baeldung/splitstringbynewline/SplitStringByNewLineUnitTest.java
new file mode 100644
index 0000000000..9afba62947
--- /dev/null
+++ b/core-java-modules/core-java-string-operations-3/src/test/java/com/baeldung/splitstringbynewline/SplitStringByNewLineUnitTest.java
@@ -0,0 +1,29 @@
+package com.baeldung.splitstringbynewline;
+
+import org.junit.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SplitStringByNewLineUnitTest {
+ @Test
+ public void givenString_whenSplitByNewLineUsingSystemLineSeparator_thenReturnsArray() {
+ assertThat("Line1\nLine2\nLine3".split(System.lineSeparator())).containsExactly("Line1", "Line2", "Line3");
+ }
+
+ @Test
+ public void givenString_whenSplitByNewLineUsingRegularExpressionPattern_thenReturnsArray() {
+ assertThat("Line1\nLine2\nLine3".split("\\r?\\n|\\r")).containsExactly("Line1", "Line2", "Line3");
+
+ assertThat("Line1\rLine2\rLine3".split("\\r?\\n|\\r")).containsExactly("Line1", "Line2", "Line3");
+
+ assertThat("Line1\r\nLine2\r\nLine3".split("\\r?\\n|\\r")).containsExactly("Line1", "Line2", "Line3");
+ }
+
+ @Test
+ public void givenString_whenSplitByNewLineUsingJava8Pattern_thenReturnsArray() {
+ assertThat("Line1\nLine2\nLine3".split("\\R")).containsExactly("Line1", "Line2", "Line3");
+
+ assertThat("Line1\rLine2\rLine3".split("\\R")).containsExactly("Line1", "Line2", "Line3");
+
+ assertThat("Line1\r\nLine2\r\nLine3".split("\\R")).containsExactly("Line1", "Line2", "Line3");
+ }
+}
diff --git a/core-java-modules/pom.xml b/core-java-modules/pom.xml
index d44ce97f14..1c36ed73c8 100644
--- a/core-java-modules/pom.xml
+++ b/core-java-modules/pom.xml
@@ -91,6 +91,7 @@
core-java-lang-oop-generics
core-java-lang-oop-modifiers
core-java-lang-oop-types
+ core-java-lang-oop-types-2
core-java-lang-oop-inheritance
core-java-lang-oop-methods
core-java-lang-oop-others
diff --git a/kubernetes/k8s-intro/src/main/java/com/baeldung/kubernetes/intro/WatchPods.java b/kubernetes/k8s-intro/src/main/java/com/baeldung/kubernetes/intro/WatchPods.java
new file mode 100644
index 0000000000..7df972b738
--- /dev/null
+++ b/kubernetes/k8s-intro/src/main/java/com/baeldung/kubernetes/intro/WatchPods.java
@@ -0,0 +1,72 @@
+package com.baeldung.kubernetes.intro;
+
+import java.util.concurrent.TimeUnit;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.reflect.TypeToken;
+
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.apis.CoreV1Api;
+import io.kubernetes.client.openapi.models.V1ObjectMeta;
+import io.kubernetes.client.openapi.models.V1Pod;
+import io.kubernetes.client.util.Config;
+import io.kubernetes.client.util.Watch;
+import io.kubernetes.client.util.Watch.Response;
+import okhttp3.OkHttpClient;
+import okhttp3.logging.HttpLoggingInterceptor;
+
+public class WatchPods {
+
+ private static Logger log = LoggerFactory.getLogger(WatchPods.class);
+
+ public static void main(String[] args) throws Exception {
+
+ ApiClient client = Config.defaultClient();
+
+ // Optional, put helpful during tests: disable client timeout and enable
+ // HTTP wire-level logs
+ HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(message -> log.info(message));
+ interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
+ OkHttpClient newClient = client.getHttpClient()
+ .newBuilder()
+ .addInterceptor(interceptor)
+ .readTimeout(0, TimeUnit.SECONDS)
+ .build();
+
+ client.setHttpClient(newClient);
+ CoreV1Api api = new CoreV1Api(client);
+
+ // Create the watch object that monitors pod creation/deletion/update events
+ while (true) {
+ log.info("[I46] Creating watch...");
+ try (Watch watch = Watch.createWatch(
+ client,
+ api.listPodForAllNamespacesCall(false, null, null, null, null, "false", null, null, 10, true, null),
+ new TypeToken>(){}.getType())) {
+
+ log.info("[I52] Receiving events:");
+ for (Response event : watch) {
+ V1Pod pod = event.object;
+ V1ObjectMeta meta = pod.getMetadata();
+ switch (event.type) {
+ case "ADDED":
+ case "MODIFIED":
+ case "DELETED":
+ log.info("event.type: {}, namespace={}, name={}",
+ event.type,
+ meta.getNamespace(),
+ meta.getName());
+ break;
+ default:
+ log.warn("[W66] Unknown event type: {}", event.type);
+ }
+ }
+ } catch (ApiException ex) {
+ log.error("[E70] ApiError", ex);
+ }
+ }
+ }
+}
diff --git a/kubernetes/k8s-intro/src/main/java/com/baeldung/kubernetes/intro/WatchPodsUsingBookmarks.java b/kubernetes/k8s-intro/src/main/java/com/baeldung/kubernetes/intro/WatchPodsUsingBookmarks.java
new file mode 100644
index 0000000000..9dfccfec08
--- /dev/null
+++ b/kubernetes/k8s-intro/src/main/java/com/baeldung/kubernetes/intro/WatchPodsUsingBookmarks.java
@@ -0,0 +1,87 @@
+package com.baeldung.kubernetes.intro;
+
+import java.util.concurrent.TimeUnit;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.reflect.TypeToken;
+
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.apis.CoreV1Api;
+import io.kubernetes.client.openapi.models.V1ObjectMeta;
+import io.kubernetes.client.openapi.models.V1Pod;
+import io.kubernetes.client.openapi.models.V1PodList;
+import io.kubernetes.client.util.Config;
+import io.kubernetes.client.util.Watch;
+import io.kubernetes.client.util.Watch.Response;
+import okhttp3.OkHttpClient;
+import okhttp3.logging.HttpLoggingInterceptor;
+
+public class WatchPodsUsingBookmarks {
+
+ private static Logger log = LoggerFactory.getLogger(WatchPodsUsingBookmarks.class);
+
+ public static void main(String[] args) throws Exception {
+
+ ApiClient client = Config.defaultClient();
+
+ // Optional, put helpful during tests: disable client timeout and enable
+ // HTTP wire-level logs
+ HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(message -> log.info(message));
+ interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
+ OkHttpClient newClient = client.getHttpClient()
+ .newBuilder()
+ .addInterceptor(interceptor)
+ .readTimeout(0, TimeUnit.SECONDS)
+ .build();
+
+ client.setHttpClient(newClient);
+ CoreV1Api api = new CoreV1Api(client);
+
+ String resourceVersion = null;
+ while (true) {
+ // Get a fresh list only we need to resync
+ if ( resourceVersion == null ) {
+ log.info("[I48] Creating initial POD list...");
+ V1PodList podList = api.listPodForAllNamespaces(true, null, null, null, null, "false", resourceVersion, null, null, null);
+ resourceVersion = podList.getMetadata().getResourceVersion();
+ }
+
+ while (true) {
+ log.info("[I54] Creating watch: resourceVersion={}", resourceVersion);
+ try (Watch watch = Watch.createWatch(
+ client,
+ api.listPodForAllNamespacesCall(true, null, null, null, null, "false", resourceVersion, null, 10, true, null),
+ new TypeToken>(){}.getType())) {
+
+ log.info("[I60] Receiving events:");
+ for (Response event : watch) {
+ V1Pod pod = event.object;
+ V1ObjectMeta meta = pod.getMetadata();
+ switch (event.type) {
+ case "BOOKMARK":
+ resourceVersion = meta.getResourceVersion();
+ log.info("[I67] event.type: {}, resourceVersion={}", event.type,resourceVersion);
+ break;
+ case "ADDED":
+ case "MODIFIED":
+ case "DELETED":
+ log.info("event.type: {}, namespace={}, name={}",
+ event.type,
+ meta.getNamespace(),
+ meta.getName());
+ break;
+ default:
+ log.warn("[W76] Unknown event type: {}", event.type);
+ }
+ }
+ } catch (ApiException ex) {
+ log.error("[E80] ApiError", ex);
+ resourceVersion = null;
+ }
+ }
+ }
+ }
+}
diff --git a/kubernetes/k8s-intro/src/main/java/com/baeldung/kubernetes/intro/WatchPodsUsingResourceVersions.java b/kubernetes/k8s-intro/src/main/java/com/baeldung/kubernetes/intro/WatchPodsUsingResourceVersions.java
new file mode 100644
index 0000000000..2165d7fc0b
--- /dev/null
+++ b/kubernetes/k8s-intro/src/main/java/com/baeldung/kubernetes/intro/WatchPodsUsingResourceVersions.java
@@ -0,0 +1,111 @@
+package com.baeldung.kubernetes.intro;
+
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.apis.CoreV1Api;
+import io.kubernetes.client.openapi.models.V1ObjectMeta;
+import io.kubernetes.client.openapi.models.V1Pod;
+import io.kubernetes.client.openapi.models.V1PodList;
+import io.kubernetes.client.util.Config;
+import io.kubernetes.client.util.Watch;
+import io.kubernetes.client.util.Watch.Response;
+import okhttp3.OkHttpClient;
+import okhttp3.logging.HttpLoggingInterceptor;
+
+public class WatchPodsUsingResourceVersions {
+
+ private static Logger log = LoggerFactory.getLogger(WatchPodsUsingResourceVersions.class);
+
+ public static void main(String[] args) throws Exception {
+
+ ApiClient client = Config.defaultClient();
+
+ // Optional, put helpful during tests: disable client timeout and enable
+ // HTTP wire-level logs
+ HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(message -> log.info(message));
+ interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
+ OkHttpClient newClient = client.getHttpClient()
+ .newBuilder()
+ .addInterceptor(interceptor)
+ .readTimeout(0, TimeUnit.SECONDS)
+ .build();
+
+ client.setHttpClient(newClient);
+ CoreV1Api api = new CoreV1Api(client);
+
+ String resourceVersion = null;
+ while (true) {
+ try {
+ if ( resourceVersion == null ) {
+ V1PodList podList = api.listPodForAllNamespaces(null, null, null, null, null, null, resourceVersion, null, null, null);
+ resourceVersion = podList.getMetadata().getResourceVersion();
+ }
+
+ log.info("[I59] Creating watch: resourceVersion={}", resourceVersion);
+ try (Watch watch = Watch.createWatch(
+ client,
+ api.listPodForAllNamespacesCall(null, null, null, null, null, null, resourceVersion, null, 60, true, null),
+ new TypeToken>(){}.getType())) {
+
+ log.info("[I65] Receiving events:");
+ for (Response event : watch) {
+ V1Pod pod = event.object;
+ V1ObjectMeta meta = pod.getMetadata();
+ switch (event.type) {
+ case "ADDED":
+ case "MODIFIED":
+ case "DELETED":
+ log.info("event: type={}, namespace={}, name={}",
+ event.type,
+ meta.getNamespace(),
+ meta.getName());
+ break;
+ default:
+ log.warn("[W76] Unknown event type: {}", event.type);
+ }
+ }
+ }
+ }
+ catch (ApiException ex) {
+ if ( ex.getCode() == 504 || ex.getCode() == 410 ) {
+ resourceVersion = extractResourceVersionFromException(ex);
+ }
+ else {
+ // Reset resource version
+ resourceVersion = null;
+ }
+ }
+ }
+ }
+
+ private static String extractResourceVersionFromException(ApiException ex) {
+
+ String body = ex.getResponseBody();
+ if (body == null) {
+ return null;
+ }
+
+ Gson gson = new Gson();
+ Map,?> st = gson.fromJson(body, Map.class);
+ Pattern p = Pattern.compile("Timeout: Too large resource version: (\\d+), current: (\\d+)");
+ String msg = (String)st.get("message");
+ Matcher m = p.matcher(msg);
+ if (!m.matches()) {
+ return null;
+ }
+
+ return m.group(2);
+ }
+
+}
diff --git a/kubernetes/k8s-intro/src/test/java/com/baeldung/kubernetes/intro/WatchPodsLiveTest.java b/kubernetes/k8s-intro/src/test/java/com/baeldung/kubernetes/intro/WatchPodsLiveTest.java
new file mode 100644
index 0000000000..37828d7a2d
--- /dev/null
+++ b/kubernetes/k8s-intro/src/test/java/com/baeldung/kubernetes/intro/WatchPodsLiveTest.java
@@ -0,0 +1,10 @@
+package com.baeldung.kubernetes.intro;
+
+import org.junit.jupiter.api.Test;
+
+class WatchPodsLiveTest {
+ @Test
+ void whenWatchPods_thenSuccess() throws Exception {
+ WatchPods.main(new String[] {});
+ }
+}
diff --git a/kubernetes/k8s-intro/src/test/java/com/baeldung/kubernetes/intro/WatchPodsUsingBookmarksLiveTest.java b/kubernetes/k8s-intro/src/test/java/com/baeldung/kubernetes/intro/WatchPodsUsingBookmarksLiveTest.java
new file mode 100644
index 0000000000..ea111f22a2
--- /dev/null
+++ b/kubernetes/k8s-intro/src/test/java/com/baeldung/kubernetes/intro/WatchPodsUsingBookmarksLiveTest.java
@@ -0,0 +1,10 @@
+package com.baeldung.kubernetes.intro;
+
+import org.junit.jupiter.api.Test;
+
+class WatchPodsUsingBookmarksLiveTest {
+ @Test
+ void whenWatchPods_thenSuccess() throws Exception {
+ WatchPodsUsingBookmarks.main(new String[] {});
+ }
+}
diff --git a/kubernetes/k8s-intro/src/test/java/com/baeldung/kubernetes/intro/WatchPodsUsingResourceVersionsLiveTest.java b/kubernetes/k8s-intro/src/test/java/com/baeldung/kubernetes/intro/WatchPodsUsingResourceVersionsLiveTest.java
new file mode 100644
index 0000000000..79c3a13eb2
--- /dev/null
+++ b/kubernetes/k8s-intro/src/test/java/com/baeldung/kubernetes/intro/WatchPodsUsingResourceVersionsLiveTest.java
@@ -0,0 +1,10 @@
+package com.baeldung.kubernetes.intro;
+
+import org.junit.jupiter.api.Test;
+
+class WatchPodsUsingResourceVersionsLiveTest {
+ @Test
+ void whenWatchPods_thenSuccess() throws Exception {
+ WatchPodsUsingResourceVersions.main(new String[] {});
+ }
+}
diff --git a/libraries-data-db/pom.xml b/libraries-data-db/pom.xml
index d51580ccbc..01e078d18d 100644
--- a/libraries-data-db/pom.xml
+++ b/libraries-data-db/pom.xml
@@ -8,8 +8,9 @@
com.baeldung
- parent-modules
- 1.0.0-SNAPSHOT
+ parent-boot-2
+ 0.0.1-SNAPSHOT
+ ../parent-boot-2
@@ -76,6 +77,71 @@
io.ebean
ebean
${ebean.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+
+
+
+
+
+
+ io.debezium
+ debezium-api
+ ${debezium.version}
+
+
+ io.debezium
+ debezium-embedded
+ ${debezium.version}
+
+
+ org.slf4j
+ slf4j-log4j12
+
+
+
+
+ io.debezium
+ debezium-connector-mysql
+ ${debezium.version}
+
+
+
+ org.testcontainers
+ testcontainers
+ ${testcontainers-version}
+
+
+
+ org.testcontainers
+ mysql
+ ${testcontainers-version}
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+
+ mysql
+ mysql-connector-java
+
+
+
+
+ org.projectlombok
+ lombok
@@ -213,6 +279,8 @@
3.2.0-m7
3.4.5
11.22.4
+ 1.4.2.Final
+ 1.15.3
-
\ No newline at end of file
+
diff --git a/libraries-data-db/src/main/java/com/baeldung/libraries/debezium/DebeziumCDCApplication.java b/libraries-data-db/src/main/java/com/baeldung/libraries/debezium/DebeziumCDCApplication.java
new file mode 100644
index 0000000000..e690d096b1
--- /dev/null
+++ b/libraries-data-db/src/main/java/com/baeldung/libraries/debezium/DebeziumCDCApplication.java
@@ -0,0 +1,13 @@
+package com.baeldung.libraries.debezium;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class DebeziumCDCApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(DebeziumCDCApplication.class, args);
+ }
+
+}
diff --git a/libraries-data-db/src/main/java/com/baeldung/libraries/debezium/config/DebeziumConnectorConfig.java b/libraries-data-db/src/main/java/com/baeldung/libraries/debezium/config/DebeziumConnectorConfig.java
new file mode 100644
index 0000000000..64e3bda4d6
--- /dev/null
+++ b/libraries-data-db/src/main/java/com/baeldung/libraries/debezium/config/DebeziumConnectorConfig.java
@@ -0,0 +1,58 @@
+package com.baeldung.libraries.debezium.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.io.File;
+import java.io.IOException;
+
+@Configuration
+public class DebeziumConnectorConfig {
+
+ /**
+ * Database details.
+ */
+ @Value("${customer.datasource.host}")
+ private String customerDbHost;
+
+ @Value("${customer.datasource.database}")
+ private String customerDbName;
+
+ @Value("${customer.datasource.port}")
+ private String customerDbPort;
+
+ @Value("${customer.datasource.username}")
+ private String customerDbUsername;
+
+ @Value("${customer.datasource.password}")
+ private String customerDbPassword;
+
+ /**
+ * Customer Database Connector Configuration
+ */
+ @Bean
+ public io.debezium.config.Configuration customerConnector() throws IOException {
+ File offsetStorageTempFile = File.createTempFile("offsets_", ".dat");
+ File dbHistoryTempFile = File.createTempFile("dbhistory_", ".dat");
+ return io.debezium.config.Configuration.create()
+ .with("name", "customer-mysql-connector")
+ .with("connector.class", "io.debezium.connector.mysql.MySqlConnector")
+ .with("offset.storage", "org.apache.kafka.connect.storage.FileOffsetBackingStore")
+ .with("offset.storage.file.filename", offsetStorageTempFile.getAbsolutePath())
+ .with("offset.flush.interval.ms", "60000")
+ .with("database.hostname", customerDbHost)
+ .with("database.port", customerDbPort)
+ .with("database.user", customerDbUsername)
+ .with("database.password", customerDbPassword)
+ .with("database.dbname", customerDbName)
+ .with("database.include.list", customerDbName)
+ .with("include.schema.changes", "false")
+ .with("database.allowPublicKeyRetrieval", "true")
+ .with("database.server.id", "10181")
+ .with("database.server.name", "customer-mysql-db-server")
+ .with("database.history", "io.debezium.relational.history.FileDatabaseHistory")
+ .with("database.history.file.filename", dbHistoryTempFile.getAbsolutePath())
+ .build();
+ }
+}
diff --git a/libraries-data-db/src/main/java/com/baeldung/libraries/debezium/entity/Customer.java b/libraries-data-db/src/main/java/com/baeldung/libraries/debezium/entity/Customer.java
new file mode 100644
index 0000000000..2100b135af
--- /dev/null
+++ b/libraries-data-db/src/main/java/com/baeldung/libraries/debezium/entity/Customer.java
@@ -0,0 +1,18 @@
+package com.baeldung.libraries.debezium.entity;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
+
+@Entity
+@Getter
+@Setter
+public class Customer {
+ @Id
+ private Long id;
+
+ private String fullname;
+ private String email;
+}
diff --git a/libraries-data-db/src/main/java/com/baeldung/libraries/debezium/listener/DebeziumListener.java b/libraries-data-db/src/main/java/com/baeldung/libraries/debezium/listener/DebeziumListener.java
new file mode 100644
index 0000000000..6826fe6d6d
--- /dev/null
+++ b/libraries-data-db/src/main/java/com/baeldung/libraries/debezium/listener/DebeziumListener.java
@@ -0,0 +1,83 @@
+package com.baeldung.libraries.debezium.listener;
+
+import com.baeldung.libraries.debezium.service.CustomerService;
+import io.debezium.config.Configuration;
+import io.debezium.embedded.Connect;
+import io.debezium.engine.DebeziumEngine;
+import io.debezium.engine.RecordChangeEvent;
+import io.debezium.engine.format.ChangeEventFormat;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.kafka.connect.data.Field;
+import org.apache.kafka.connect.data.Struct;
+import org.apache.kafka.connect.source.SourceRecord;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import static io.debezium.data.Envelope.FieldName.*;
+import static io.debezium.data.Envelope.Operation;
+import static java.util.stream.Collectors.toMap;
+
+@Slf4j
+@Component
+public class DebeziumListener {
+
+ private final Executor executor = Executors.newSingleThreadExecutor();
+ private final CustomerService customerService;
+ private final DebeziumEngine> debeziumEngine;
+
+ public DebeziumListener(Configuration customerConnectorConfiguration, CustomerService customerService) {
+
+ this.debeziumEngine = DebeziumEngine.create(ChangeEventFormat.of(Connect.class))
+ .using(customerConnectorConfiguration.asProperties())
+ .notifying(this::handleChangeEvent)
+ .build();
+
+ this.customerService = customerService;
+ }
+
+ private void handleChangeEvent(RecordChangeEvent sourceRecordRecordChangeEvent) {
+ SourceRecord sourceRecord = sourceRecordRecordChangeEvent.record();
+
+ log.info("Key = '" + sourceRecord.key() + "' value = '" + sourceRecord.value() + "'");
+
+ Struct sourceRecordChangeValue= (Struct) sourceRecord.value();
+
+ if (sourceRecordChangeValue != null) {
+ Operation operation = Operation.forCode((String) sourceRecordChangeValue.get(OPERATION));
+
+ if(operation != Operation.READ) {
+ String record = operation == Operation.DELETE ? BEFORE : AFTER; // Handling Update & Insert operations.
+
+ Struct struct = (Struct) sourceRecordChangeValue.get(record);
+ Map payload = struct.schema().fields().stream()
+ .map(Field::name)
+ .filter(fieldName -> struct.get(fieldName) != null)
+ .map(fieldName -> Pair.of(fieldName, struct.get(fieldName)))
+ .collect(toMap(Pair::getKey, Pair::getValue));
+
+ this.customerService.replicateData(payload, operation);
+ log.info("Updated Data: {} with Operation: {}", payload, operation.name());
+ }
+ }
+ }
+
+ @PostConstruct
+ private void start() {
+ this.executor.execute(debeziumEngine);
+ }
+
+ @PreDestroy
+ private void stop() throws IOException {
+ if (this.debeziumEngine != null) {
+ this.debeziumEngine.close();
+ }
+ }
+
+}
diff --git a/libraries-data-db/src/main/java/com/baeldung/libraries/debezium/repository/CustomerRepository.java b/libraries-data-db/src/main/java/com/baeldung/libraries/debezium/repository/CustomerRepository.java
new file mode 100644
index 0000000000..fc199cb85c
--- /dev/null
+++ b/libraries-data-db/src/main/java/com/baeldung/libraries/debezium/repository/CustomerRepository.java
@@ -0,0 +1,9 @@
+package com.baeldung.libraries.debezium.repository;
+
+import com.baeldung.libraries.debezium.entity.Customer;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface CustomerRepository extends JpaRepository {
+}
diff --git a/libraries-data-db/src/main/java/com/baeldung/libraries/debezium/service/CustomerService.java b/libraries-data-db/src/main/java/com/baeldung/libraries/debezium/service/CustomerService.java
new file mode 100644
index 0000000000..4b35a94eb5
--- /dev/null
+++ b/libraries-data-db/src/main/java/com/baeldung/libraries/debezium/service/CustomerService.java
@@ -0,0 +1,30 @@
+package com.baeldung.libraries.debezium.service;
+
+import com.baeldung.libraries.debezium.entity.Customer;
+import com.baeldung.libraries.debezium.repository.CustomerRepository;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.debezium.data.Envelope.Operation;
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+
+@Service
+public class CustomerService {
+
+ private final CustomerRepository customerRepository;
+
+ public CustomerService(CustomerRepository customerRepository) {
+ this.customerRepository = customerRepository;
+ }
+
+ public void replicateData(Map customerData, Operation operation) {
+ final ObjectMapper mapper = new ObjectMapper();
+ final Customer customer = mapper.convertValue(customerData, Customer.class);
+
+ if (Operation.DELETE.name().equals(operation.name())) {
+ customerRepository.deleteById(customer.getId());
+ } else {
+ customerRepository.save(customer);
+ }
+ }
+}
diff --git a/libraries-data-db/src/main/resources/application.yml b/libraries-data-db/src/main/resources/application.yml
new file mode 100644
index 0000000000..3fcf4a12a9
--- /dev/null
+++ b/libraries-data-db/src/main/resources/application.yml
@@ -0,0 +1,34 @@
+## Server properties
+server:
+ port: 8080
+
+## Primary/Target Database Properties
+spring:
+ datasource:
+ url: jdbc:mysql://localhost:3306/customerdb
+ username: root
+ password: root
+ jpa.hibernate.ddl-auto: create-drop
+ jpa.show-sql: true
+
+## Source Database Properties
+customer:
+ datasource:
+ host: localhost
+ port: 3305
+ database: customerdb
+ username: root
+ password: root
+
+ ## Logging properties
+logging:
+ level:
+ root: INFO
+ io:
+ debezium:
+ mysql:
+ BinlogReader: INFO
+ com:
+ baeldung:
+ libraries:
+ debezium: DEBUG
diff --git a/libraries-data-db/src/main/resources/customer.sql b/libraries-data-db/src/main/resources/customer.sql
new file mode 100644
index 0000000000..25e3bcf569
--- /dev/null
+++ b/libraries-data-db/src/main/resources/customer.sql
@@ -0,0 +1,9 @@
+drop table if exists customer;
+
+CREATE TABLE customer
+(
+ id integer NOT NULL,
+ fullname character varying(255),
+ email character varying(255),
+ CONSTRAINT customer_pkey PRIMARY KEY (id)
+);
diff --git a/libraries-data-db/src/main/resources/docker-compose.yml b/libraries-data-db/src/main/resources/docker-compose.yml
new file mode 100644
index 0000000000..ddf865cc78
--- /dev/null
+++ b/libraries-data-db/src/main/resources/docker-compose.yml
@@ -0,0 +1,25 @@
+version: "3.9"
+services:
+ # Install Source MySQL DB and setup the Customer database
+ mysql-1:
+ container_name: source-database
+ image: mysql
+ command: --default-authentication-plugin=mysql_native_password
+ restart: always
+ ports:
+ - 3305:3306
+ environment:
+ MYSQL_ROOT_PASSWORD: root
+ MYSQL_DATABASE: customerdb
+
+ # Install Target MySQL DB and setup the Customer database
+ mysql-2:
+ container_name: target-database
+ image: mysql
+ command: --default-authentication-plugin=mysql_native_password
+ restart: always
+ ports:
+ - 3306:3306
+ environment:
+ MYSQL_ROOT_PASSWORD: root
+ MYSQL_DATABASE: customerdb
diff --git a/libraries-data-db/src/test/java/com/baeldung/libraries/debezium/DebeziumCDCLiveTest.java b/libraries-data-db/src/test/java/com/baeldung/libraries/debezium/DebeziumCDCLiveTest.java
new file mode 100644
index 0000000000..916ea4a120
--- /dev/null
+++ b/libraries-data-db/src/test/java/com/baeldung/libraries/debezium/DebeziumCDCLiveTest.java
@@ -0,0 +1,61 @@
+package com.baeldung.libraries.debezium;
+
+import com.baeldung.libraries.debezium.repository.CustomerRepository;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(classes = DebeziumCDCApplication.class)
+@ActiveProfiles("test")
+public class DebeziumCDCLiveTest {
+
+ @Autowired
+ private CustomerRepository customerRepository;
+
+ @Autowired
+ @Qualifier("sourceJdbcTemplate")
+ private NamedParameterJdbcTemplate jdbcTemplate;
+
+ @Before
+ public void clearData() {
+ jdbcTemplate.update("delete from customer where id = :id", Collections.singletonMap("id", 1));
+ }
+
+ @DynamicPropertySource
+ static void registerProperties(DynamicPropertyRegistry registry) {
+ registry.add("customer.datasource.port", MySQLTestContainerConfiguration::getPort);
+ }
+
+ @Test
+ public void whenInsertDataToSourceDatabase_thenCdcOk() throws InterruptedException {
+ assertThat(customerRepository.findAll().size()).isZero();
+
+ // insert data to source DB
+ Map map = new HashMap<>();
+ map.put("id", 1);
+ map.put("fullname", "John Doe");
+ map.put("email", "test@test.com");
+
+ jdbcTemplate.update("INSERT INTO customer(id, fullname, email) VALUES (:id, :fullname, :email)", map);
+
+ // verify target DB
+ Thread.sleep(10000);
+ assertThat(customerRepository.findAll().size()).isNotZero();
+ }
+
+}
diff --git a/libraries-data-db/src/test/java/com/baeldung/libraries/debezium/MySQLTestContainerConfiguration.java b/libraries-data-db/src/test/java/com/baeldung/libraries/debezium/MySQLTestContainerConfiguration.java
new file mode 100644
index 0000000000..e7394e190b
--- /dev/null
+++ b/libraries-data-db/src/test/java/com/baeldung/libraries/debezium/MySQLTestContainerConfiguration.java
@@ -0,0 +1,61 @@
+package com.baeldung.libraries.debezium;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.jdbc.datasource.DriverManagerDataSource;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
+import org.testcontainers.containers.MySQLContainer;
+import org.testcontainers.utility.DockerImageName;
+
+import javax.sql.DataSource;
+
+@Configuration
+public class MySQLTestContainerConfiguration {
+
+ public static final DockerImageName MYSQL_IMAGE = DockerImageName.parse("mysql:8.0");
+
+ private static final MySQLContainer> mysqlContainer = new MySQLContainer<>(MYSQL_IMAGE)
+ .withCommand("--default-authentication-plugin=mysql_native_password")
+ .withInitScript("debezium/customer.sql")
+ .withDatabaseName("SOURCE_DB")
+ .withUsername("user")
+ .withPassword("user")
+ .withEnv("MYSQL_ROOT_PASSWORD", "user");
+
+ MySQLTestContainerConfiguration() {
+ mysqlContainer.start();
+ }
+
+ public static int getPort() {
+ return mysqlContainer.getFirstMappedPort();
+ }
+
+ @Bean
+ @Primary
+ public EmbeddedDatabase targetDatasource() {
+ return new EmbeddedDatabaseBuilder()
+ .setType(EmbeddedDatabaseType.H2)
+ .setName("TAGRET_DB")
+ .build();
+ }
+
+ @Bean(name = "SOURCE_DS")
+ public DataSource sourceDataSource() {
+ DriverManagerDataSource dataSource = new DriverManagerDataSource();
+ dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
+ dataSource.setUrl(mysqlContainer.getJdbcUrl());
+ dataSource.setUsername(mysqlContainer.getUsername());
+ dataSource.setPassword(mysqlContainer.getPassword());
+ return dataSource;
+ }
+
+ @Bean(name = "sourceJdbcTemplate")
+ public NamedParameterJdbcTemplate getJdbcTemplate(@Qualifier("SOURCE_DS") DataSource sourceDataSource) {
+ return new NamedParameterJdbcTemplate(sourceDataSource);
+ }
+}
diff --git a/libraries-data-db/src/test/resources/application-test.yml b/libraries-data-db/src/test/resources/application-test.yml
new file mode 100644
index 0000000000..96aaa2b727
--- /dev/null
+++ b/libraries-data-db/src/test/resources/application-test.yml
@@ -0,0 +1,7 @@
+## Source Database Properties
+customer:
+ datasource:
+ host: localhost
+ database: SOURCE_DB
+ username: root
+ password: user
diff --git a/libraries-data-db/src/test/resources/debezium/customer.sql b/libraries-data-db/src/test/resources/debezium/customer.sql
new file mode 100644
index 0000000000..25e3bcf569
--- /dev/null
+++ b/libraries-data-db/src/test/resources/debezium/customer.sql
@@ -0,0 +1,9 @@
+drop table if exists customer;
+
+CREATE TABLE customer
+(
+ id integer NOT NULL,
+ fullname character varying(255),
+ email character varying(255),
+ CONSTRAINT customer_pkey PRIMARY KEY (id)
+);
diff --git a/persistence-modules/hibernate-annotations/README.md b/persistence-modules/hibernate-annotations/README.md
index 393cf82bfd..7d7740a069 100644
--- a/persistence-modules/hibernate-annotations/README.md
+++ b/persistence-modules/hibernate-annotations/README.md
@@ -8,3 +8,4 @@ This module contains articles about Annotations used in Hibernate.
- [Difference Between @JoinColumn and mappedBy](https://www.baeldung.com/jpa-joincolumn-vs-mappedby)
- [Hibernate One to Many Annotation Tutorial](https://www.baeldung.com/hibernate-one-to-many)
- [Hibernate @WhereJoinTable Annotation](https://www.baeldung.com/hibernate-wherejointable)
+- [Usage of the Hibernate @LazyCollection Annotation](https://www.baeldung.com/hibernate-lazycollection)
diff --git a/spring-5-reactive-client/src/main/java/com/baeldung/webclient/timeout/WebClientTimeoutProvider.java b/spring-5-reactive-client/src/main/java/com/baeldung/webclient/timeout/WebClientTimeoutProvider.java
new file mode 100644
index 0000000000..6bb5a2db38
--- /dev/null
+++ b/spring-5-reactive-client/src/main/java/com/baeldung/webclient/timeout/WebClientTimeoutProvider.java
@@ -0,0 +1,89 @@
+package com.baeldung.webclient.timeout;
+
+import io.netty.channel.ChannelOption;
+import io.netty.channel.epoll.EpollChannelOption;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.timeout.ReadTimeoutHandler;
+import io.netty.handler.timeout.WriteTimeoutHandler;
+import lombok.experimental.UtilityClass;
+import org.springframework.http.client.reactive.ReactorClientHttpConnector;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.netty.http.client.HttpClient;
+import reactor.netty.tcp.SslProvider;
+import reactor.netty.transport.ProxyProvider;
+
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+@UtilityClass
+public class WebClientTimeoutProvider {
+
+ public static WebClient defaultWebClient() {
+ HttpClient httpClient = HttpClient.create();
+
+ return buildWebClient(httpClient);
+ }
+
+ public WebClient responseTimeoutClient() {
+ HttpClient httpClient = HttpClient.create()
+ .responseTimeout(Duration.ofSeconds(1));
+
+ return buildWebClient(httpClient);
+ }
+
+ public WebClient connectionTimeoutClient() {
+ HttpClient httpClient = HttpClient.create()
+ .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000);
+
+ return buildWebClient(httpClient);
+ }
+
+ public WebClient connectionTimeoutWithKeepAliveClient() {
+ HttpClient httpClient = HttpClient.create()
+ .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
+ .option(ChannelOption.SO_KEEPALIVE, true)
+ .option(EpollChannelOption.TCP_KEEPIDLE, 300)
+ .option(EpollChannelOption.TCP_KEEPINTVL, 60)
+ .option(EpollChannelOption.TCP_KEEPCNT, 8);
+
+ return buildWebClient(httpClient);
+ }
+
+ public WebClient readWriteTimeoutClient() {
+ HttpClient httpClient = HttpClient.create()
+ .doOnConnected(conn -> conn
+ .addHandler(new ReadTimeoutHandler(5, TimeUnit.SECONDS))
+ .addHandler(new WriteTimeoutHandler(5)));
+
+ return buildWebClient(httpClient);
+ }
+
+ public WebClient sslTimeoutClient() {
+ HttpClient httpClient = HttpClient.create()
+ .secure(spec -> spec
+ .sslContext(SslContextBuilder.forClient())
+ .defaultConfiguration(SslProvider.DefaultConfigurationType.TCP)
+ .handshakeTimeout(Duration.ofSeconds(30))
+ .closeNotifyFlushTimeout(Duration.ofSeconds(10))
+ .closeNotifyReadTimeout(Duration.ofSeconds(10)));
+
+ return buildWebClient(httpClient);
+ }
+
+ public WebClient proxyTimeoutClient() {
+ HttpClient httpClient = HttpClient.create()
+ .proxy(spec -> spec
+ .type(ProxyProvider.Proxy.HTTP)
+ .host("http://proxy")
+ .port(8080)
+ .connectTimeoutMillis(3000));
+
+ return buildWebClient(httpClient);
+ }
+
+ private WebClient buildWebClient(HttpClient httpClient) {
+ return WebClient.builder()
+ .clientConnector(new ReactorClientHttpConnector(httpClient))
+ .build();
+ }
+}
diff --git a/spring-5-reactive-client/src/test/java/com/baeldung/webclient/timeout/WebClientTimeoutIntegrationTest.java b/spring-5-reactive-client/src/test/java/com/baeldung/webclient/timeout/WebClientTimeoutIntegrationTest.java
new file mode 100644
index 0000000000..d2e009fe6a
--- /dev/null
+++ b/spring-5-reactive-client/src/test/java/com/baeldung/webclient/timeout/WebClientTimeoutIntegrationTest.java
@@ -0,0 +1,129 @@
+package com.baeldung.webclient.timeout;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import io.netty.handler.timeout.ReadTimeoutException;
+import lombok.val;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.jupiter.api.AfterEach;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.reactive.function.client.WebClientRequestException;
+import reactor.core.publisher.Mono;
+import reactor.netty.http.client.HttpClientRequest;
+
+
+import java.time.Duration;
+import java.util.concurrent.TimeoutException;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.configureFor;
+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 static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class WebClientTimeoutIntegrationTest {
+
+ private WireMockServer wireMockServer;
+
+ @Before
+ public void setup() {
+ wireMockServer = new WireMockServer(wireMockConfig().dynamicPort());
+ wireMockServer.start();
+ configureFor("localhost", wireMockServer.port());
+ }
+
+ @After
+ public void tearDown() {
+ wireMockServer.stop();
+ }
+
+ @AfterEach
+ public void tearDownEach() {
+ wireMockServer.resetAll();
+ }
+
+ @Test
+ public void givenResponseTimeoutClientWhenRequestTimeoutThenReadTimeoutException() {
+ val path = "/response-timeout";
+ val delay = Math.toIntExact(Duration.ofSeconds(2).toMillis());
+ stubFor(get(urlEqualTo(path)).willReturn(aResponse().withFixedDelay(delay)
+ .withStatus(HttpStatus.OK.value())));
+
+ val webClient = WebClientTimeoutProvider.responseTimeoutClient();
+
+ val ex = assertThrows(RuntimeException.class, () ->
+ webClient.get()
+ .uri(wireMockServer.baseUrl() + path)
+ .exchangeToMono(Mono::just)
+ .log()
+ .block());
+ assertThat(ex).isInstanceOf(WebClientRequestException.class)
+ .getCause().isInstanceOf(ReadTimeoutException.class);
+ }
+
+ @Test
+ public void givenReadWriteTimeoutClientWhenRequestTimeoutThenReadTimeoutException() {
+ val path = "/read-write-timeout";
+ val delay = Math.toIntExact(Duration.ofSeconds(6).toMillis());
+ stubFor(get(urlEqualTo(path)).willReturn(aResponse().withFixedDelay(delay)
+ .withStatus(HttpStatus.OK.value())));
+
+ val webClient = WebClientTimeoutProvider.readWriteTimeoutClient();
+
+ val ex = assertThrows(RuntimeException.class, () ->
+ webClient.get()
+ .uri(wireMockServer.baseUrl() + path)
+ .exchangeToMono(Mono::just)
+ .log()
+ .block());
+ assertThat(ex).isInstanceOf(WebClientRequestException.class)
+ .getCause().isInstanceOf(ReadTimeoutException.class);
+ }
+
+ @Test
+ public void givenNoTimeoutClientAndReactorTimeoutWhenRequestTimeoutThenTimeoutException() {
+ val path = "/reactor-timeout";
+ val delay = Math.toIntExact(Duration.ofSeconds(5).toMillis());
+ stubFor(get(urlEqualTo(path)).willReturn(aResponse().withFixedDelay(delay)
+ .withStatus(HttpStatus.OK.value())));
+
+ val webClient = WebClientTimeoutProvider.defaultWebClient();
+
+ val ex = assertThrows(RuntimeException.class, () ->
+ webClient.get()
+ .uri(wireMockServer.baseUrl() + path)
+ .exchangeToMono(Mono::just)
+ .timeout(Duration.ofSeconds(1))
+ .log()
+ .block());
+ assertThat(ex).hasMessageContaining("Did not observe any item")
+ .getCause().isInstanceOf(TimeoutException.class);
+ }
+
+ @Test
+ public void givenNoTimeoutClientAndTimeoutHttpRequestWhenRequestTimeoutThenReadTimeoutException() {
+ val path = "/reactor-http-request-timeout";
+ val delay = Math.toIntExact(Duration.ofSeconds(5).toMillis());
+ stubFor(get(urlEqualTo(path)).willReturn(aResponse().withFixedDelay(delay)
+ .withStatus(HttpStatus.OK.value())));
+
+ val webClient = WebClientTimeoutProvider.defaultWebClient();
+
+ val ex = assertThrows(RuntimeException.class, () ->
+ webClient.get()
+ .uri(wireMockServer.baseUrl() + path)
+ .httpRequest(httpRequest -> {
+ HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
+ reactorRequest.responseTimeout(Duration.ofSeconds(1));
+ })
+ .exchangeToMono(Mono::just)
+ .log()
+ .block());
+ assertThat(ex).isInstanceOf(WebClientRequestException.class)
+ .getCause().isInstanceOf(ReadTimeoutException.class);
+ }
+}
diff --git a/spring-aop/pom.xml b/spring-aop/pom.xml
index 74b6f48b46..09949a95f5 100644
--- a/spring-aop/pom.xml
+++ b/spring-aop/pom.xml
@@ -14,6 +14,15 @@