From 7e1c4c7f8e012c691218d4631eee25036640e430 Mon Sep 17 00:00:00 2001 From: saikat Date: Thu, 11 Dec 2025 10:41:58 +0100 Subject: [PATCH 01/38] Implement detach and reattach entity in JPA --- .../detachentity/SpringJpaApplication.java | 17 ++++ .../detachentity/client/UserApiClient.java | 5 ++ .../baeldung/detachentity/domain/User.java | 52 ++++++++++++ .../repository/DetachableRepository.java | 5 ++ .../repository/DetachableRepositoryImpl.java | 16 ++++ .../repository/UserRepository.java | 10 +++ .../detachentity/service/UserDataService.java | 40 +++++++++ .../service/UserRegistrationService.java | 28 +++++++ ...serRegistrationServiceIntegrationTest.java | 81 +++++++++++++++++++ 9 files changed, 254 insertions(+) create mode 100644 persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/SpringJpaApplication.java create mode 100644 persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/client/UserApiClient.java create mode 100644 persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/domain/User.java create mode 100644 persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/repository/DetachableRepository.java create mode 100644 persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/repository/DetachableRepositoryImpl.java create mode 100644 persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/repository/UserRepository.java create mode 100644 persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/service/UserDataService.java create mode 100644 persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/service/UserRegistrationService.java create mode 100644 persistence-modules/spring-boot-persistence-5/src/test/java/com/baeldung/detachentity/UserRegistrationServiceIntegrationTest.java diff --git a/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/SpringJpaApplication.java b/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/SpringJpaApplication.java new file mode 100644 index 000000000000..a66e963518fd --- /dev/null +++ b/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/SpringJpaApplication.java @@ -0,0 +1,17 @@ +package com.baeldung.detachentity; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +@SpringBootApplication +@ComponentScan("com.baeldung.detachentity") +@EnableJpaRepositories +public class SpringJpaApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringJpaApplication.class, args); + } + +} diff --git a/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/client/UserApiClient.java b/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/client/UserApiClient.java new file mode 100644 index 000000000000..170e9bea566e --- /dev/null +++ b/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/client/UserApiClient.java @@ -0,0 +1,5 @@ +package com.baeldung.detachentity.client; + +public interface UserApiClient { + boolean verify(String email); +} diff --git a/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/domain/User.java b/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/domain/User.java new file mode 100644 index 000000000000..c8a501d839ae --- /dev/null +++ b/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/domain/User.java @@ -0,0 +1,52 @@ +package com.baeldung.detachentity.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + @Column(unique = true) + private String email; + + private boolean activated; + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public boolean isActivated() { + return activated; + } + + public void setActivated(boolean activated) { + this.activated = activated; + } +} diff --git a/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/repository/DetachableRepository.java b/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/repository/DetachableRepository.java new file mode 100644 index 000000000000..ef7b33ab1cd6 --- /dev/null +++ b/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/repository/DetachableRepository.java @@ -0,0 +1,5 @@ +package com.baeldung.detachentity.repository; + +public interface DetachableRepository { + void detach(T t); +} diff --git a/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/repository/DetachableRepositoryImpl.java b/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/repository/DetachableRepositoryImpl.java new file mode 100644 index 000000000000..baa97608a5af --- /dev/null +++ b/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/repository/DetachableRepositoryImpl.java @@ -0,0 +1,16 @@ +package com.baeldung.detachentity.repository; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +public class DetachableRepositoryImpl implements DetachableRepository { + @PersistenceContext + private EntityManager entityManager; + + @Override + public void detach(T entity) { + if(entity != null) { + entityManager.detach(entity); + } + } +} diff --git a/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/repository/UserRepository.java b/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/repository/UserRepository.java new file mode 100644 index 000000000000..53e0f1fdb1a4 --- /dev/null +++ b/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/repository/UserRepository.java @@ -0,0 +1,10 @@ +package com.baeldung.detachentity.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.baeldung.detachentity.domain.User; + +@Repository +public interface UserRepository extends JpaRepository, DetachableRepository { +} diff --git a/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/service/UserDataService.java b/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/service/UserDataService.java new file mode 100644 index 000000000000..3e3bea81e918 --- /dev/null +++ b/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/service/UserDataService.java @@ -0,0 +1,40 @@ +package com.baeldung.detachentity.service; + +import jakarta.transaction.Transactional; + +import org.springframework.stereotype.Service; + +import com.baeldung.detachentity.domain.User; +import com.baeldung.detachentity.repository.UserRepository; + +@Service +public class UserDataService { + private final UserRepository userRepository; + + public UserDataService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Transactional + public User createUser(String name, String email) { + User user = new User(); + user.setName(name); + user.setEmail(email); + user.setActivated(false); + + User savedUser = userRepository.save(user); + userRepository.detach(savedUser); + + return savedUser; + } + + @Transactional + public User activateUser(Long id) { + User user = userRepository.findById(id) + .orElseThrow(() -> new RuntimeException("User not found for Id" + id)); + + user.setActivated(true); + + return user; + } +} diff --git a/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/service/UserRegistrationService.java b/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/service/UserRegistrationService.java new file mode 100644 index 000000000000..09981fc35316 --- /dev/null +++ b/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/service/UserRegistrationService.java @@ -0,0 +1,28 @@ +package com.baeldung.detachentity.service; + +import org.springframework.stereotype.Service; + +import com.baeldung.detachentity.client.UserApiClient; +import com.baeldung.detachentity.domain.User; + +@Service +public class UserRegistrationService { + private final UserDataService userDataService; + private final UserApiClient userApiClient; + + public UserRegistrationService(UserDataService userDataService, UserApiClient userApiClient) { + this.userDataService = userDataService; + this.userApiClient = userApiClient; + } + + public User handleRegistration(String name, String email) { + User user = userDataService.createUser(name, email); + + if (userApiClient.verify(email)) { + user = userDataService.activateUser(user.getId()); + } + + return user; + } + +} diff --git a/persistence-modules/spring-boot-persistence-5/src/test/java/com/baeldung/detachentity/UserRegistrationServiceIntegrationTest.java b/persistence-modules/spring-boot-persistence-5/src/test/java/com/baeldung/detachentity/UserRegistrationServiceIntegrationTest.java new file mode 100644 index 000000000000..a97a5cb4620e --- /dev/null +++ b/persistence-modules/spring-boot-persistence-5/src/test/java/com/baeldung/detachentity/UserRegistrationServiceIntegrationTest.java @@ -0,0 +1,81 @@ +package com.baeldung.detachentity; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import com.baeldung.detachentity.client.UserApiClient; +import com.baeldung.detachentity.domain.User; +import com.baeldung.detachentity.repository.UserRepository; +import com.baeldung.detachentity.service.UserRegistrationService; +import com.baeldung.detachentity.service.UserDataService; + +@SpringBootTest(classes = SpringJpaApplication.class) +public class UserRegistrationServiceIntegrationTest { + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserDataService userDataService; + + @Autowired + private UserRegistrationService userRegistrationService; + + @Autowired + private UserApiClient userApiClient; + + @TestConfiguration + static class MockUserApiClientConfig { + @Bean + public UserApiClient userApiClient() { + return Mockito.mock(UserApiClient.class); + } + } + + @Test + void givenValidUser_whenUserIsRegistrationIsCalled_thenSaveActiveUser() { + Mockito.when(userApiClient.verify(any())).thenReturn(true); + + User user = userRegistrationService.handleRegistration("test1", "test1@mail.com"); + Optional savedUser = userRepository.findById(user.getId()); + + assertNotNull(savedUser); + assertTrue(savedUser.isPresent()); + assertEquals("test1", savedUser.get().getName()); + assertEquals("test1@mail.com", savedUser.get().getEmail()); + assertTrue(savedUser.get().isActivated()); + } + + @Test + void givenInValidUser_whenUserIsRegistrationIsCalled_thenSaveInActiveUser() { + Mockito.when(userApiClient.verify(any())).thenReturn(false); + + User user = userRegistrationService.handleRegistration("test2", "test2@mail.com"); + Optional savedUser = userRepository.findById(user.getId()); + + assertNotNull(savedUser); + assertTrue(savedUser.isPresent()); + assertEquals("test2", savedUser.get().getName()); + assertEquals("test2@mail.com", savedUser.get().getEmail()); + assertFalse(savedUser.get().isActivated()); + } + + @Test + void givenValidUser_whenUserIsRegistrationIsCalled_ExternalServiceFails_thenSaveInActiveUser() { + Mockito.when(userApiClient.verify(any())).thenThrow(RuntimeException.class); + assertThrows(RuntimeException.class, () -> userRegistrationService.handleRegistration("test3", "test3@mail.com")); + } +} From 4368b19e8da3d6a9d669fdad9be51ddc8eb9da06 Mon Sep 17 00:00:00 2001 From: saikat Date: Thu, 11 Dec 2025 13:55:09 +0100 Subject: [PATCH 02/38] Implement additional tests --- .../detachentity/service/UserDataService.java | 1 - .../UserRepositoryIntegrationTest.java | 69 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 persistence-modules/spring-boot-persistence-5/src/test/java/com/baeldung/detachentity/UserRepositoryIntegrationTest.java diff --git a/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/service/UserDataService.java b/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/service/UserDataService.java index 3e3bea81e918..b6639907a7b2 100644 --- a/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/service/UserDataService.java +++ b/persistence-modules/spring-boot-persistence-5/src/main/java/com/baeldung/detachentity/service/UserDataService.java @@ -34,7 +34,6 @@ public User activateUser(Long id) { .orElseThrow(() -> new RuntimeException("User not found for Id" + id)); user.setActivated(true); - return user; } } diff --git a/persistence-modules/spring-boot-persistence-5/src/test/java/com/baeldung/detachentity/UserRepositoryIntegrationTest.java b/persistence-modules/spring-boot-persistence-5/src/test/java/com/baeldung/detachentity/UserRepositoryIntegrationTest.java new file mode 100644 index 000000000000..d6f6d61ed31f --- /dev/null +++ b/persistence-modules/spring-boot-persistence-5/src/test/java/com/baeldung/detachentity/UserRepositoryIntegrationTest.java @@ -0,0 +1,69 @@ +package com.baeldung.detachentity; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Optional; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import com.baeldung.detachentity.domain.User; +import com.baeldung.detachentity.repository.UserRepository; + + +@DataJpaTest +public class UserRepositoryIntegrationTest { + + @Autowired + private UserRepository userRepository; + + @PersistenceContext + private EntityManager entityManager; + + @Test + void givenValidUserIsDetached_whenUserSaveIsCalled_AndUpdated_thenUserIsNotUpdated() { + User user = new User(); + user.setName("test1"); + user.setEmail("test1@mail.com"); + user.setActivated(true); + + userRepository.save(user); + userRepository.detach(user); + user.setName("test1_updated"); + entityManager.flush(); + + Optional savedUser = userRepository.findById(user.getId()); + + assertNotNull(savedUser); + assertTrue(savedUser.isPresent()); + assertEquals("test1", savedUser.get().getName()); + assertEquals("test1@mail.com", savedUser.get().getEmail()); + assertTrue(savedUser.get().isActivated()); + } + + @Test + void givenUserIsNotDetached_whenUserSaveIsCalled_AndUpdated_thenUserIsUpdated() { + User user = new User(); + user.setName("test2"); + user.setEmail("test2@mail.com"); + user.setActivated(true); + + userRepository.save(user); + user.setName("test2_updated"); + entityManager.flush(); + + Optional savedUser = userRepository.findById(user.getId()); + + assertNotNull(savedUser); + assertTrue(savedUser.isPresent()); + assertEquals("test2_updated", savedUser.get().getName()); + assertEquals("test2@mail.com", savedUser.get().getEmail()); + assertTrue(savedUser.get().isActivated()); + } +} From a7ae8dfc9f9de37c7d3837ff0380188090b46a9f Mon Sep 17 00:00:00 2001 From: saikat Date: Mon, 19 Jan 2026 00:52:19 +0530 Subject: [PATCH 03/38] implemented kafka offset reset --- .../resetoffset/admin/OffsetResetService.java | 62 +++++++ .../consumer/KafkaConsumerService.java | 59 ++++++ .../consumer/ReplayRebalanceListener.java | 53 ++++++ .../kafka/admin/OffsetResetServiceTest.java | 169 ++++++++++++++++++ .../KafkaConsumerServiceLiveTest.java | 155 ++++++++++++++++ .../test/resources/docker/docker-compose.yml | 27 +++ 6 files changed, 525 insertions(+) create mode 100644 apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/OffsetResetService.java create mode 100644 apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/KafkaConsumerService.java create mode 100644 apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/ReplayRebalanceListener.java create mode 100644 apache-kafka-4/src/test/java/com/baeldung/kafka/admin/OffsetResetServiceTest.java create mode 100644 apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java create mode 100644 apache-kafka-4/src/test/resources/docker/docker-compose.yml diff --git a/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/OffsetResetService.java b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/OffsetResetService.java new file mode 100644 index 000000000000..132178d5480b --- /dev/null +++ b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/OffsetResetService.java @@ -0,0 +1,62 @@ +package com.baeldung.kafka.resetoffset.admin; + +import org.apache.kafka.clients.admin.*; +import org.apache.kafka.common.TopicPartition; +import org.apache.kafka.clients.consumer.OffsetAndMetadata; + +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +public class OffsetResetService { + private final AdminClient adminClient; + + public OffsetResetService(String bootstrapServers) { + this.adminClient = AdminClient.create(Map.of(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers)); + } + + public void reset(String topic, String consumerGroup) throws ExecutionException, InterruptedException { + List partitions = fetchPartitions(topic); + + Map earliestOffsets = fetchEarliestOffsets(partitions); + + adminClient.alterConsumerGroupOffsets(consumerGroup, earliestOffsets) + .all() + .get(); + } + + private List fetchPartitions(String topic) throws ExecutionException, InterruptedException { + return adminClient.describeTopics(List.of(topic)) + .values() + .get(topic) + .get() + .partitions() + .stream() + .map(p -> new TopicPartition(topic, p.partition())) + .toList(); + } + + private Map fetchEarliestOffsets(List partitions) { + Map offsetSpecs = partitions.stream() + .collect(Collectors.toMap(tp -> tp, tp -> OffsetSpec.earliest())); + + ListOffsetsResult offsetsResult = adminClient.listOffsets(offsetSpecs); + Map offsets = new HashMap<>(); + + for (var tp : partitions) { + long offset; + try { + offset = offsetsResult.partitionResult(tp).get().offset(); + } catch (InterruptedException | ExecutionException ex) { + throw new RuntimeException(ex); + } + offsets.put(tp, new OffsetAndMetadata(offset)); + } + + return offsets; + } + + public void close() { + adminClient.close(); + } +} diff --git a/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/KafkaConsumerService.java b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/KafkaConsumerService.java new file mode 100644 index 000000000000..fc8b95888f02 --- /dev/null +++ b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/KafkaConsumerService.java @@ -0,0 +1,59 @@ +package com.baeldung.kafka.resetoffset.consumer; + +import org.apache.kafka.clients.consumer.*; +import org.apache.kafka.common.errors.WakeupException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.util.Collections; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicBoolean; + +public class KafkaConsumerService { + + private static final Logger log = LoggerFactory.getLogger(KafkaConsumerService.class); + private final KafkaConsumer consumer; + private final AtomicBoolean running = new AtomicBoolean(true); + private final boolean replayEnabled; + private final long replayFromTimestamp; + + public KafkaConsumerService(Properties consumerProps, String topic, boolean replayEnabled, long replayFromTimestamp) { + if (replayEnabled && replayFromTimestamp == 0L) { + throw new IllegalArgumentException("replayFromTimestamp must be provided when replayEnabled=true"); + } + + this.consumer = new KafkaConsumer<>(consumerProps); + this.replayEnabled = replayEnabled; + this.replayFromTimestamp = replayFromTimestamp; + ConsumerRebalanceListener replayRebalanceListener = new ReplayRebalanceListener(consumer, replayEnabled, replayFromTimestamp); + + consumer.subscribe(Collections.singletonList(topic), replayRebalanceListener); + } + + public void start() { + try { + while (running.get()) { + ConsumerRecords records = consumer.poll(Duration.ofSeconds(1)); + records.forEach(this::process); + } + } catch (WakeupException ex) { + log.error("Error in the Kafka Consumer with exception {}", ex.getMessage(), ex); + } finally { + try { + consumer.commitSync(); + } finally { + consumer.close(); + } + } + } + + public void shutdown() { + running.set(false); + consumer.wakeup(); + } + + private void process(ConsumerRecord record) { + log.info("topic={} partition={} offset={} key={} value={}", record.topic(), record.partition(), record.offset(), record.key(), record.value()); + } +} diff --git a/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/ReplayRebalanceListener.java b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/ReplayRebalanceListener.java new file mode 100644 index 000000000000..e25980c8d202 --- /dev/null +++ b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/ReplayRebalanceListener.java @@ -0,0 +1,53 @@ +package com.baeldung.kafka.resetoffset.consumer; + +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.clients.consumer.OffsetAndTimestamp; +import org.apache.kafka.common.TopicPartition; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class ReplayRebalanceListener implements ConsumerRebalanceListener { + + private final KafkaConsumer consumer; + private final boolean replayEnabled; + private final long replayFromTimestamp; + + private boolean seekDone = false; + + public ReplayRebalanceListener(KafkaConsumer consumer, boolean replayEnabled, long replayFromTimestamp) { + + this.consumer = consumer; + this.replayEnabled = replayEnabled; + this.replayFromTimestamp = replayFromTimestamp; + } + + @Override + public void onPartitionsRevoked(Collection partitions) { + consumer.commitSync(); + } + + @Override + public void onPartitionsAssigned(Collection partitions) { + if (!replayEnabled || seekDone || partitions.isEmpty()) { + return; + } + + Map partitionsTimestamp = partitions.stream() + .collect(Collectors.toMap(Function.identity(), tp -> replayFromTimestamp)); + + Map offsets = consumer.offsetsForTimes(partitionsTimestamp); + + partitions.forEach(tp -> { + OffsetAndTimestamp offsetAndTimestamp = offsets.get(tp); + + if (offsetAndTimestamp != null) { + consumer.seek(tp, offsetAndTimestamp.offset()); + } + }); + + seekDone = true; + } +} diff --git a/apache-kafka-4/src/test/java/com/baeldung/kafka/admin/OffsetResetServiceTest.java b/apache-kafka-4/src/test/java/com/baeldung/kafka/admin/OffsetResetServiceTest.java new file mode 100644 index 000000000000..587331bc9e8f --- /dev/null +++ b/apache-kafka-4/src/test/java/com/baeldung/kafka/admin/OffsetResetServiceTest.java @@ -0,0 +1,169 @@ +package com.baeldung.kafka.admin; + +import org.apache.kafka.clients.admin.*; +import org.apache.kafka.clients.consumer.*; +import org.apache.kafka.clients.producer.*; +import org.apache.kafka.common.TopicPartition; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.apache.kafka.common.serialization.StringSerializer; +import org.junit.jupiter.api.*; + +import org.testcontainers.containers.KafkaContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.time.Duration; +import java.util.*; +import java.util.concurrent.ExecutionException; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.testcontainers.shaded.org.awaitility.Awaitility.await; + +import com.baeldung.kafka.resetoffset.admin.OffsetResetService; + +@Testcontainers +class OffsetResetServiceTest { + + @Container + private static final KafkaContainer KAFKA_CONTAINER = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.9.0")); + private static String bootstrapServers; + private static OffsetResetService resetService; + + @BeforeAll + static void startKafka() { + KAFKA_CONTAINER.start(); + bootstrapServers = KAFKA_CONTAINER.getBootstrapServers(); + resetService = new OffsetResetService(bootstrapServers); + } + + @AfterAll + static void stopKafka() { + resetService.close(); + KAFKA_CONTAINER.stop(); + } + + @Test + void givenMessagesConsumed_whenOffsetIsReset_thenOffsetIsSetToEarliest() throws Exception { + try (KafkaProducer producer = new KafkaProducer<>(getProducerConfig())) { + producer.send(new ProducerRecord<>("test-topic", "msg-1")); + producer.send(new ProducerRecord<>("test-topic", "msg-2")); + producer.flush(); + } + + consumeAndCommitAll(); + awaitCommittedOffset(2L); + awaitGroupInactive("test-group"); + + resetService.reset("test-topic", "test-group"); + + awaitCommittedOffset(0L); + } + + @Test + void givenNoMessagesConsumed_whenOffsetIsReset_thenOffsetIsSetToEarliest() throws Exception { + try (KafkaProducer producer = new KafkaProducer<>(getProducerConfig())) { + producer.send(new ProducerRecord<>("test-topic", "msg-1")); + producer.send(new ProducerRecord<>("test-topic", "msg-2")); + producer.flush(); + } + + awaitCommittedOffset(0L); + awaitGroupInactive("test-group"); + + resetService.reset("test-topic", "test-group"); + + awaitCommittedOffset(0L); + } + + private void consumeAndCommitAll() { + try (KafkaConsumer consumer = new KafkaConsumer<>(getConsumerConfig())) { + consumer.subscribe(List.of("test-topic")); + + int consumed = 0; + while (consumed < 2) { + ConsumerRecords records = consumer.poll(Duration.ofSeconds(1)); + consumed += records.count(); + } + + consumer.commitSync(); + } + } + + private long fetchCommittedOffset() throws ExecutionException, InterruptedException { + + Properties props = new Properties(); + props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + + try (AdminClient admin = AdminClient.create(props)) { + Map offsets = admin.listConsumerGroupOffsets("test-group") + .partitionsToOffsetAndMetadata() + .get(); + + return offsets.values() + .iterator() + .next() + .offset(); + } + } + + private void awaitGroupInactive(String groupId) { + Properties props = new Properties(); + props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + + await().atMost(10, SECONDS) + .pollInterval(Duration.ofMillis(300)) + .untilAsserted(() -> { + try (AdminClient admin = AdminClient.create(props)) { + ConsumerGroupDescription description = admin.describeConsumerGroups(List.of(groupId)) + .describedGroups() + .get(groupId) + .get(); + + Assertions.assertTrue(description.members() + .isEmpty(), "Consumer group is still active"); + } + }); + } + + private void awaitCommittedOffset(long expectedOffset) { + await().atMost(10, SECONDS) + .pollInterval(Duration.ofMillis(300)) + .untilAsserted(() -> assertEquals(expectedOffset, fetchCommittedOffset())); + } + + private KafkaConsumer startActiveConsumer() { + KafkaConsumer consumer = new KafkaConsumer<>(getConsumerConfig()); + consumer.subscribe(List.of("test-topic")); + + new Thread(() -> { + while (!Thread.currentThread() + .isInterrupted()) { + consumer.poll(Duration.ofMillis(500)); + } + }).start(); + + return consumer; + } + + private static Properties getProducerConfig() { + Properties producerProperties = new Properties(); + producerProperties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_CONTAINER.getBootstrapServers()); + producerProperties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + producerProperties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + + return producerProperties; + } + + private static Properties getConsumerConfig() { + Properties props = new Properties(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_CONTAINER.getBootstrapServers()); + props.put(ConsumerConfig.GROUP_ID_CONFIG, "test-group"); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); + + return props; + } +} diff --git a/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java b/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java new file mode 100644 index 000000000000..283bc1a2a0dc --- /dev/null +++ b/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java @@ -0,0 +1,155 @@ +package com.baeldung.kafka.consumer; + +import org.apache.kafka.clients.consumer.*; +import org.apache.kafka.clients.producer.*; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.apache.kafka.common.serialization.StringSerializer; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.*; +import org.testcontainers.containers.KafkaContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import org.awaitility.Duration; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +import com.baeldung.kafka.resetoffset.consumer.KafkaConsumerService; + +@Testcontainers +public class KafkaConsumerServiceLiveTest { + + @Container + private static final KafkaContainer KAFKA_CONTAINER = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.9.0")); + private static KafkaProducer producer; + + @BeforeAll + static void setup() { + KAFKA_CONTAINER.start(); + producer = new KafkaProducer<>(getProducerConfig()); + } + + @AfterAll + static void cleanup() { + producer.close(); + KAFKA_CONTAINER.stop(); + } + + @Test + void givenReplayEnabled_WhenTimestampProvided_ThenConsumesFromTimestamp() { + producer.send(new ProducerRecord<>("test-topic", "x1", "test1")); + producer.flush(); + + long baseTs = System.currentTimeMillis(); + producer.send(new ProducerRecord<>("test-topic", "x2", "test2")); + producer.flush(); + + String groupId = "test-group-1"; + + KafkaConsumerService service = new KafkaConsumerService(getConsumerConfig(groupId), "test-topic", true, baseTs); + new Thread(service::start).start(); + + Awaitility.await() + .atMost(Duration.FIVE_SECONDS) + .pollInterval(Duration.FIVE_HUNDRED_MILLISECONDS) + .untilAsserted(() -> { + List consumed = consumeFromCommittedOffset(groupId); + assertEquals(0, consumed.size()); + assertFalse(consumed.contains("test1")); + assertFalse(consumed.contains("test2")); + }); + + service.shutdown(); + } + + @Test + void givenProducerMessagesSent_WhenConsumerIsRunningWithReplayDisabled_ThenConsumesLatestOffset() { + producer.send(new ProducerRecord<>("test-topic", "x3", "test3")); + producer.send(new ProducerRecord<>("test-topic", "x4", "test4")); + producer.flush(); + + KafkaConsumerService service = new KafkaConsumerService(getConsumerConfig("test-group-2"), "test-topic", false, 0L); + new Thread(service::start).start(); + + Awaitility.await() + .atMost(Duration.FIVE_SECONDS) + .pollInterval(Duration.FIVE_HUNDRED_MILLISECONDS) + .untilAsserted(() -> { + List consumed = consumeFromCommittedOffset("test-group-2"); + assertEquals(0, consumed.size()); + assertFalse(consumed.contains("test3")); + assertFalse(consumed.contains("test4")); + }); + + service.shutdown(); + } + + @Test + void givenConsumerWithReplayedDisabledRuns_whenReplayIsEnabled_WhenTimestampProvided_ThenConsumesFromTimestamp() throws InterruptedException { + producer.send(new ProducerRecord<>("test-topic", "x5", "test5")); + producer.flush(); + + String groupId = "test-group-3"; + KafkaConsumerService service1 = new KafkaConsumerService(getConsumerConfig(groupId), "test-topic", false, 0L); + new Thread(service1::start).start(); + Thread.sleep(5000); + service1.shutdown(); + + long baseTs = System.currentTimeMillis(); + producer.send(new ProducerRecord<>("test-topic", "x6", "test6")); + producer.flush(); + + KafkaConsumerService service2 = new KafkaConsumerService(getConsumerConfig(groupId), "test-topic", true, baseTs); + new Thread(service2::start).start(); + + Awaitility.await() + .atMost(Duration.FIVE_SECONDS) + .pollInterval(Duration.FIVE_HUNDRED_MILLISECONDS) + .untilAsserted(() -> { + List consumed = consumeFromCommittedOffset(groupId); + assertEquals(0, consumed.size()); + assertFalse(consumed.contains("test5")); + assertFalse(consumed.contains("test6")); + assertFalse(consumed.contains("test6")); + }); + + service2.shutdown(); + } + + private List consumeFromCommittedOffset(String groupId) { + List values = new ArrayList<>(); + + try (KafkaConsumer consumer = new KafkaConsumer<>(getConsumerConfig(groupId))) { + consumer.subscribe(Collections.singleton("test-topic")); + + ConsumerRecords records = consumer.poll(java.time.Duration.ofSeconds(2)); + for (ConsumerRecord r : records) { + values.add(r.value()); + } + } + + return values; + } + + private static Properties getProducerConfig() { + Properties producerProperties = new Properties(); + producerProperties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_CONTAINER.getBootstrapServers()); + producerProperties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + producerProperties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + return producerProperties; + } + + private static Properties getConsumerConfig(String groupId) { + Properties props = new Properties(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_CONTAINER.getBootstrapServers()); + props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest"); + props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); + return props; + } +} diff --git a/apache-kafka-4/src/test/resources/docker/docker-compose.yml b/apache-kafka-4/src/test/resources/docker/docker-compose.yml new file mode 100644 index 000000000000..82e19bc0d4b5 --- /dev/null +++ b/apache-kafka-4/src/test/resources/docker/docker-compose.yml @@ -0,0 +1,27 @@ +version: "3.8" + +services: + zookeeper: + image: confluentinc/cp-zookeeper:7.6.0 + hostname: zookeeper + container_name: zookeeper + ports: + - "2181:2181" + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + + kafka: + image: confluentinc/cp-kafka:7.6.0 + hostname: kafka + container_name: kafka + depends_on: + - zookeeper + ports: + - "9092:9092" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 From ca2ed3d72f56dfbeb942f1731ce0b3fd072ceea5 Mon Sep 17 00:00:00 2001 From: saikat Date: Mon, 19 Jan 2026 14:36:45 +0530 Subject: [PATCH 04/38] refactored code and test cases --- ...etService.java => ResetOffsetService.java} | 30 ++++--- .../consumer/KafkaConsumerService.java | 32 +++----- .../consumer/ReplayRebalanceListener.java | 15 ++-- ...eTest.java => ResetOffsetServiceTest.java} | 80 +++++++------------ .../KafkaConsumerServiceLiveTest.java | 38 +++++---- 5 files changed, 87 insertions(+), 108 deletions(-) rename apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/{OffsetResetService.java => ResetOffsetService.java} (66%) rename apache-kafka-4/src/test/java/com/baeldung/kafka/admin/{OffsetResetServiceTest.java => ResetOffsetServiceTest.java} (66%) diff --git a/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/OffsetResetService.java b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/ResetOffsetService.java similarity index 66% rename from apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/OffsetResetService.java rename to apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/ResetOffsetService.java index 132178d5480b..d03df6e62d23 100644 --- a/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/OffsetResetService.java +++ b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/ResetOffsetService.java @@ -8,21 +8,31 @@ import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; -public class OffsetResetService { +public class ResetOffsetService { + private final AdminClient adminClient; - public OffsetResetService(String bootstrapServers) { + public ResetOffsetService(String bootstrapServers) { this.adminClient = AdminClient.create(Map.of(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers)); } - public void reset(String topic, String consumerGroup) throws ExecutionException, InterruptedException { - List partitions = fetchPartitions(topic); + public void reset(String topic, String consumerGroup) { + List partitions; + try { + partitions = fetchPartitions(topic); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } Map earliestOffsets = fetchEarliestOffsets(partitions); - adminClient.alterConsumerGroupOffsets(consumerGroup, earliestOffsets) - .all() - .get(); + try { + adminClient.alterConsumerGroupOffsets(consumerGroup, earliestOffsets) + .all() + .get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } } private List fetchPartitions(String topic) throws ExecutionException, InterruptedException { @@ -37,7 +47,7 @@ private List fetchPartitions(String topic) throws ExecutionExcep } private Map fetchEarliestOffsets(List partitions) { - Map offsetSpecs = partitions.stream() + Map offsetSpecs = partitions.stream() .collect(Collectors.toMap(tp -> tp, tp -> OffsetSpec.earliest())); ListOffsetsResult offsetsResult = adminClient.listOffsets(offsetSpecs); @@ -46,7 +56,9 @@ private Map fetchEarliestOffsets(List consumer; private final AtomicBoolean running = new AtomicBoolean(true); - private final boolean replayEnabled; - private final long replayFromTimestamp; - - public KafkaConsumerService(Properties consumerProps, String topic, boolean replayEnabled, long replayFromTimestamp) { - if (replayEnabled && replayFromTimestamp == 0L) { - throw new IllegalArgumentException("replayFromTimestamp must be provided when replayEnabled=true"); - } + public KafkaConsumerService(Properties consumerProps, String topic, Long replayFromTimestampInEpoch) { this.consumer = new KafkaConsumer<>(consumerProps); - this.replayEnabled = replayEnabled; - this.replayFromTimestamp = replayFromTimestamp; - ConsumerRebalanceListener replayRebalanceListener = new ReplayRebalanceListener(consumer, replayEnabled, replayFromTimestamp); - - consumer.subscribe(Collections.singletonList(topic), replayRebalanceListener); + if (replayFromTimestampInEpoch != null) { + consumer.subscribe(List.of(topic), new ReplayRebalanceListener(consumer, replayFromTimestampInEpoch)); + } else { + consumer.subscribe(List.of(topic)); + } } public void start() { @@ -36,15 +30,15 @@ public void start() { while (running.get()) { ConsumerRecords records = consumer.poll(Duration.ofSeconds(1)); records.forEach(this::process); + consumer.commitSync(); } } catch (WakeupException ex) { - log.error("Error in the Kafka Consumer with exception {}", ex.getMessage(), ex); - } finally { - try { - consumer.commitSync(); - } finally { - consumer.close(); + if (running.get()) { + log.error("Error in the Kafka Consumer with exception {}", ex.getMessage(), ex); + throw ex; } + } finally { + consumer.close(); } } diff --git a/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/ReplayRebalanceListener.java b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/ReplayRebalanceListener.java index e25980c8d202..195341a42927 100644 --- a/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/ReplayRebalanceListener.java +++ b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/ReplayRebalanceListener.java @@ -12,16 +12,12 @@ public class ReplayRebalanceListener implements ConsumerRebalanceListener { private final KafkaConsumer consumer; - private final boolean replayEnabled; - private final long replayFromTimestamp; - + private final long replayFromTimeInEpoch; private boolean seekDone = false; - public ReplayRebalanceListener(KafkaConsumer consumer, boolean replayEnabled, long replayFromTimestamp) { - + public ReplayRebalanceListener(KafkaConsumer consumer, long replayFromTimeInEpoch) { this.consumer = consumer; - this.replayEnabled = replayEnabled; - this.replayFromTimestamp = replayFromTimestamp; + this.replayFromTimeInEpoch = replayFromTimeInEpoch; } @Override @@ -31,18 +27,17 @@ public void onPartitionsRevoked(Collection partitions) { @Override public void onPartitionsAssigned(Collection partitions) { - if (!replayEnabled || seekDone || partitions.isEmpty()) { + if (seekDone || partitions.isEmpty()) { return; } Map partitionsTimestamp = partitions.stream() - .collect(Collectors.toMap(Function.identity(), tp -> replayFromTimestamp)); + .collect(Collectors.toMap(Function.identity(), tp -> replayFromTimeInEpoch)); Map offsets = consumer.offsetsForTimes(partitionsTimestamp); partitions.forEach(tp -> { OffsetAndTimestamp offsetAndTimestamp = offsets.get(tp); - if (offsetAndTimestamp != null) { consumer.seek(tp, offsetAndTimestamp.offset()); } diff --git a/apache-kafka-4/src/test/java/com/baeldung/kafka/admin/OffsetResetServiceTest.java b/apache-kafka-4/src/test/java/com/baeldung/kafka/admin/ResetOffsetServiceTest.java similarity index 66% rename from apache-kafka-4/src/test/java/com/baeldung/kafka/admin/OffsetResetServiceTest.java rename to apache-kafka-4/src/test/java/com/baeldung/kafka/admin/ResetOffsetServiceTest.java index 587331bc9e8f..735f705a449a 100644 --- a/apache-kafka-4/src/test/java/com/baeldung/kafka/admin/OffsetResetServiceTest.java +++ b/apache-kafka-4/src/test/java/com/baeldung/kafka/admin/ResetOffsetServiceTest.java @@ -21,21 +21,25 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.testcontainers.shaded.org.awaitility.Awaitility.await; -import com.baeldung.kafka.resetoffset.admin.OffsetResetService; +import com.baeldung.kafka.resetoffset.admin.ResetOffsetService; @Testcontainers -class OffsetResetServiceTest { +class ResetOffsetServiceTest { @Container private static final KafkaContainer KAFKA_CONTAINER = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.9.0")); private static String bootstrapServers; - private static OffsetResetService resetService; + private static ResetOffsetService resetService; + private static KafkaProducer producer; + private static KafkaConsumer consumer; @BeforeAll static void startKafka() { KAFKA_CONTAINER.start(); bootstrapServers = KAFKA_CONTAINER.getBootstrapServers(); - resetService = new OffsetResetService(bootstrapServers); + resetService = new ResetOffsetService(bootstrapServers); + producer = new KafkaProducer<>(getProducerConfig()); + consumer = new KafkaConsumer<>(getConsumerConfig()); } @AfterAll @@ -45,16 +49,21 @@ static void stopKafka() { } @Test - void givenMessagesConsumed_whenOffsetIsReset_thenOffsetIsSetToEarliest() throws Exception { - try (KafkaProducer producer = new KafkaProducer<>(getProducerConfig())) { - producer.send(new ProducerRecord<>("test-topic", "msg-1")); - producer.send(new ProducerRecord<>("test-topic", "msg-2")); - producer.flush(); + void givenMessagesAreConsumed_whenOffsetIsReset_thenOffsetIsSetToEarliest() { + producer.send(new ProducerRecord<>("test-topic", "msg-1")); + producer.send(new ProducerRecord<>("test-topic", "msg-2")); + producer.flush(); + + consumer.subscribe(List.of("test-topic")); + + int consumed = 0; + while (consumed < 2) { + ConsumerRecords records = consumer.poll(Duration.ofSeconds(2)); + consumed += records.count(); } - consumeAndCommitAll(); awaitCommittedOffset(2L); - awaitGroupInactive("test-group"); + awaitGroupInactive(); resetService.reset("test-topic", "test-group"); @@ -62,37 +71,21 @@ void givenMessagesConsumed_whenOffsetIsReset_thenOffsetIsSetToEarliest() throws } @Test - void givenNoMessagesConsumed_whenOffsetIsReset_thenOffsetIsSetToEarliest() throws Exception { - try (KafkaProducer producer = new KafkaProducer<>(getProducerConfig())) { - producer.send(new ProducerRecord<>("test-topic", "msg-1")); - producer.send(new ProducerRecord<>("test-topic", "msg-2")); - producer.flush(); - } + void givenNoMessagesConsumed_whenOffsetIsReset_thenOffsetIsSetToEarliest() { + producer.send(new ProducerRecord<>("test-topic", "msg-1")); + producer.send(new ProducerRecord<>("test-topic", "msg-2")); + producer.flush(); awaitCommittedOffset(0L); - awaitGroupInactive("test-group"); + awaitGroupInactive(); resetService.reset("test-topic", "test-group"); awaitCommittedOffset(0L); } - private void consumeAndCommitAll() { - try (KafkaConsumer consumer = new KafkaConsumer<>(getConsumerConfig())) { - consumer.subscribe(List.of("test-topic")); - - int consumed = 0; - while (consumed < 2) { - ConsumerRecords records = consumer.poll(Duration.ofSeconds(1)); - consumed += records.count(); - } - - consumer.commitSync(); - } - } private long fetchCommittedOffset() throws ExecutionException, InterruptedException { - Properties props = new Properties(); props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); @@ -108,7 +101,7 @@ private long fetchCommittedOffset() throws ExecutionException, InterruptedExcept } } - private void awaitGroupInactive(String groupId) { + private void awaitGroupInactive() { Properties props = new Properties(); props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); @@ -116,9 +109,9 @@ private void awaitGroupInactive(String groupId) { .pollInterval(Duration.ofMillis(300)) .untilAsserted(() -> { try (AdminClient admin = AdminClient.create(props)) { - ConsumerGroupDescription description = admin.describeConsumerGroups(List.of(groupId)) + ConsumerGroupDescription description = admin.describeConsumerGroups(List.of("test-group")) .describedGroups() - .get(groupId) + .get("test-group") .get(); Assertions.assertTrue(description.members() @@ -133,20 +126,6 @@ private void awaitCommittedOffset(long expectedOffset) { .untilAsserted(() -> assertEquals(expectedOffset, fetchCommittedOffset())); } - private KafkaConsumer startActiveConsumer() { - KafkaConsumer consumer = new KafkaConsumer<>(getConsumerConfig()); - consumer.subscribe(List.of("test-topic")); - - new Thread(() -> { - while (!Thread.currentThread() - .isInterrupted()) { - consumer.poll(Duration.ofMillis(500)); - } - }).start(); - - return consumer; - } - private static Properties getProducerConfig() { Properties producerProperties = new Properties(); producerProperties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_CONTAINER.getBootstrapServers()); @@ -162,7 +141,8 @@ private static Properties getConsumerConfig() { props.put(ConsumerConfig.GROUP_ID_CONFIG, "test-group"); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); - props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest"); + props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true"); return props; } diff --git a/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java b/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java index 283bc1a2a0dc..e4d5b14ed8fd 100644 --- a/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java +++ b/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java @@ -11,9 +11,8 @@ import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; -import org.awaitility.Duration; - import java.util.*; +import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.*; @@ -21,7 +20,6 @@ @Testcontainers public class KafkaConsumerServiceLiveTest { - @Container private static final KafkaContainer KAFKA_CONTAINER = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.9.0")); private static KafkaProducer producer; @@ -39,7 +37,7 @@ static void cleanup() { } @Test - void givenReplayEnabled_WhenTimestampProvided_ThenConsumesFromTimestamp() { + void givenConsumerReplayIsEnabled_whenReplayTimestampIsProvided_thenConsumesFromTimestamp() { producer.send(new ProducerRecord<>("test-topic", "x1", "test1")); producer.flush(); @@ -47,22 +45,20 @@ void givenReplayEnabled_WhenTimestampProvided_ThenConsumesFromTimestamp() { producer.send(new ProducerRecord<>("test-topic", "x2", "test2")); producer.flush(); - String groupId = "test-group-1"; - - KafkaConsumerService service = new KafkaConsumerService(getConsumerConfig(groupId), "test-topic", true, baseTs); - new Thread(service::start).start(); + KafkaConsumerService kafkaConsumerService = new KafkaConsumerService(getConsumerConfig("test-group-1"), "test-topic", baseTs); + new Thread(kafkaConsumerService::start).start(); Awaitility.await() - .atMost(Duration.FIVE_SECONDS) - .pollInterval(Duration.FIVE_HUNDRED_MILLISECONDS) + .atMost(45, TimeUnit.SECONDS) + .pollInterval(1, TimeUnit.SECONDS) .untilAsserted(() -> { - List consumed = consumeFromCommittedOffset(groupId); + List consumed = consumeFromCommittedOffset("test-group-1"); assertEquals(0, consumed.size()); assertFalse(consumed.contains("test1")); assertFalse(consumed.contains("test2")); }); - service.shutdown(); + kafkaConsumerService.shutdown(); } @Test @@ -71,12 +67,13 @@ void givenProducerMessagesSent_WhenConsumerIsRunningWithReplayDisabled_ThenConsu producer.send(new ProducerRecord<>("test-topic", "x4", "test4")); producer.flush(); - KafkaConsumerService service = new KafkaConsumerService(getConsumerConfig("test-group-2"), "test-topic", false, 0L); + KafkaConsumerService service = new KafkaConsumerService(getConsumerConfig("test-group-2"), + "test-topic", null); new Thread(service::start).start(); Awaitility.await() - .atMost(Duration.FIVE_SECONDS) - .pollInterval(Duration.FIVE_HUNDRED_MILLISECONDS) + .atMost(45, TimeUnit.SECONDS) + .pollInterval(1, TimeUnit.SECONDS) .untilAsserted(() -> { List consumed = consumeFromCommittedOffset("test-group-2"); assertEquals(0, consumed.size()); @@ -93,21 +90,22 @@ void givenConsumerWithReplayedDisabledRuns_whenReplayIsEnabled_WhenTimestampProv producer.flush(); String groupId = "test-group-3"; - KafkaConsumerService service1 = new KafkaConsumerService(getConsumerConfig(groupId), "test-topic", false, 0L); + KafkaConsumerService service1 = new KafkaConsumerService(getConsumerConfig(groupId), + "test-topic", null); new Thread(service1::start).start(); Thread.sleep(5000); service1.shutdown(); - long baseTs = System.currentTimeMillis(); producer.send(new ProducerRecord<>("test-topic", "x6", "test6")); producer.flush(); - KafkaConsumerService service2 = new KafkaConsumerService(getConsumerConfig(groupId), "test-topic", true, baseTs); + KafkaConsumerService service2 = new KafkaConsumerService(getConsumerConfig(groupId), + "test-topic", null); new Thread(service2::start).start(); Awaitility.await() - .atMost(Duration.FIVE_SECONDS) - .pollInterval(Duration.FIVE_HUNDRED_MILLISECONDS) + .atMost(45, TimeUnit.SECONDS) + .pollInterval(1, TimeUnit.SECONDS) .untilAsserted(() -> { List consumed = consumeFromCommittedOffset(groupId); assertEquals(0, consumed.size()); From 83a68a09bff4512d2a0e91852887ea85db001f4b Mon Sep 17 00:00:00 2001 From: saikat Date: Mon, 19 Jan 2026 16:53:12 +0530 Subject: [PATCH 05/38] refactored code --- .../kafka/resetoffset/consumer/KafkaConsumerService.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/KafkaConsumerService.java b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/KafkaConsumerService.java index 1ef76f495f7c..a254e5f74638 100644 --- a/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/KafkaConsumerService.java +++ b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/KafkaConsumerService.java @@ -29,7 +29,9 @@ public void start() { try { while (running.get()) { ConsumerRecords records = consumer.poll(Duration.ofSeconds(1)); - records.forEach(this::process); + records.forEach(record -> + log.info("topic={} partition={} offset={} key={} value={}", record.topic(), record.partition(), + record.offset(), record.key(), record.value())); consumer.commitSync(); } } catch (WakeupException ex) { @@ -46,8 +48,4 @@ public void shutdown() { running.set(false); consumer.wakeup(); } - - private void process(ConsumerRecord record) { - log.info("topic={} partition={} offset={} key={} value={}", record.topic(), record.partition(), record.offset(), record.key(), record.value()); - } } From 5b153526eeca8bea04801e7d8232ffcc683a664c Mon Sep 17 00:00:00 2001 From: saikat Date: Mon, 19 Jan 2026 21:33:11 +0530 Subject: [PATCH 06/38] refactored code --- .../resetoffset/admin/ResetOffsetService.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/ResetOffsetService.java b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/ResetOffsetService.java index d03df6e62d23..d523e513e8cf 100644 --- a/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/ResetOffsetService.java +++ b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/ResetOffsetService.java @@ -3,13 +3,17 @@ import org.apache.kafka.clients.admin.*; import org.apache.kafka.common.TopicPartition; import org.apache.kafka.clients.consumer.OffsetAndMetadata; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.*; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; -public class ResetOffsetService { +import com.baeldung.kafka.resetoffset.consumer.KafkaConsumerService; +public class ResetOffsetService { + private static final Logger log = LoggerFactory.getLogger(KafkaConsumerService.class); private final AdminClient adminClient; public ResetOffsetService(String bootstrapServers) { @@ -20,8 +24,9 @@ public void reset(String topic, String consumerGroup) { List partitions; try { partitions = fetchPartitions(topic); - } catch (ExecutionException | InterruptedException e) { - throw new RuntimeException(e); + } catch (ExecutionException | InterruptedException ex) { + log.error("Error in the fetching partitions with exception {}", ex.getMessage(), ex); + throw new RuntimeException(ex); } Map earliestOffsets = fetchEarliestOffsets(partitions); @@ -30,8 +35,9 @@ public void reset(String topic, String consumerGroup) { adminClient.alterConsumerGroupOffsets(consumerGroup, earliestOffsets) .all() .get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); + } catch (InterruptedException | ExecutionException ex) { + log.error("Error in the Kafka Consumer reset with exception {}", ex.getMessage(), ex); + throw new RuntimeException(ex); } } @@ -60,6 +66,7 @@ private Map fetchEarliestOffsets(List Date: Tue, 20 Jan 2026 01:08:01 +0530 Subject: [PATCH 07/38] refactored test code --- .../kafka/admin/ResetOffsetServiceTest.java | 114 ++++++++---------- .../KafkaConsumerServiceLiveTest.java | 41 +++---- 2 files changed, 71 insertions(+), 84 deletions(-) diff --git a/apache-kafka-4/src/test/java/com/baeldung/kafka/admin/ResetOffsetServiceTest.java b/apache-kafka-4/src/test/java/com/baeldung/kafka/admin/ResetOffsetServiceTest.java index 735f705a449a..9edc2cef9b35 100644 --- a/apache-kafka-4/src/test/java/com/baeldung/kafka/admin/ResetOffsetServiceTest.java +++ b/apache-kafka-4/src/test/java/com/baeldung/kafka/admin/ResetOffsetServiceTest.java @@ -19,6 +19,7 @@ import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.testcontainers.shaded.org.awaitility.Awaitility.await; import com.baeldung.kafka.resetoffset.admin.ResetOffsetService; @@ -28,102 +29,89 @@ class ResetOffsetServiceTest { @Container private static final KafkaContainer KAFKA_CONTAINER = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.9.0")); - private static String bootstrapServers; private static ResetOffsetService resetService; - private static KafkaProducer producer; - private static KafkaConsumer consumer; + private static AdminClient adminClient; @BeforeAll static void startKafka() { KAFKA_CONTAINER.start(); - bootstrapServers = KAFKA_CONTAINER.getBootstrapServers(); - resetService = new ResetOffsetService(bootstrapServers); - producer = new KafkaProducer<>(getProducerConfig()); - consumer = new KafkaConsumer<>(getConsumerConfig()); + resetService = new ResetOffsetService(KAFKA_CONTAINER.getBootstrapServers()); + + Properties props = new Properties(); + props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_CONTAINER.getBootstrapServers()); + adminClient = AdminClient.create(props); } @AfterAll static void stopKafka() { + adminClient.close(); resetService.close(); KAFKA_CONTAINER.stop(); } @Test void givenMessagesAreConsumed_whenOffsetIsReset_thenOffsetIsSetToEarliest() { - producer.send(new ProducerRecord<>("test-topic", "msg-1")); - producer.send(new ProducerRecord<>("test-topic", "msg-2")); + KafkaProducer producer = new KafkaProducer<>(getProducerConfig()); + producer.send(new ProducerRecord<>("test-topic-1", "msg-1")); + producer.send(new ProducerRecord<>("test-topic-1", "msg-2")); producer.flush(); - consumer.subscribe(List.of("test-topic")); + KafkaConsumer consumer = new KafkaConsumer<>(getConsumerConfig("test-group-1")); + consumer.subscribe(List.of("test-topic-1")); int consumed = 0; while (consumed < 2) { - ConsumerRecords records = consumer.poll(Duration.ofSeconds(2)); + ConsumerRecords records = consumer.poll(Duration.ofSeconds(1)); consumed += records.count(); } - awaitCommittedOffset(2L); - awaitGroupInactive(); + consumer.commitSync(); + consumer.close(); - resetService.reset("test-topic", "test-group"); + await().atMost(5, SECONDS) + .pollInterval(Duration.ofMillis(300)) + .untilAsserted(() -> assertEquals(2L, fetchCommittedOffset("test-group-1"))); - awaitCommittedOffset(0L); + resetService.reset("test-topic-1", "test-group-1"); + + await().atMost(5, SECONDS) + .pollInterval(Duration.ofMillis(300)) + .untilAsserted(() -> assertEquals(0L, fetchCommittedOffset("test-group-1"))); } @Test - void givenNoMessagesConsumed_whenOffsetIsReset_thenOffsetIsSetToEarliest() { - producer.send(new ProducerRecord<>("test-topic", "msg-1")); - producer.send(new ProducerRecord<>("test-topic", "msg-2")); + void givenConsumerIsStillActive_whenOffsetResetIsCalled_thenThrowRuntimeException_NoOffsetReset() { + KafkaProducer producer = new KafkaProducer<>(getProducerConfig()); + producer.send(new ProducerRecord<>("test-topic-2", "msg-1")); + producer.send(new ProducerRecord<>("test-topic-2", "msg-2")); producer.flush(); - awaitCommittedOffset(0L); - awaitGroupInactive(); + KafkaConsumer consumer = new KafkaConsumer<>(getConsumerConfig("test-group-2")); + consumer.subscribe(List.of("test-topic-2")); - resetService.reset("test-topic", "test-group"); - - awaitCommittedOffset(0L); - } - - - private long fetchCommittedOffset() throws ExecutionException, InterruptedException { - Properties props = new Properties(); - props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); - - try (AdminClient admin = AdminClient.create(props)) { - Map offsets = admin.listConsumerGroupOffsets("test-group") - .partitionsToOffsetAndMetadata() - .get(); - - return offsets.values() - .iterator() - .next() - .offset(); + int consumed = 0; + while (consumed < 2) { + ConsumerRecords records = consumer.poll(Duration.ofSeconds(1)); + consumed += records.count(); } - } + consumer.commitSync(); - private void awaitGroupInactive() { - Properties props = new Properties(); - props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + assertThrows(RuntimeException.class, () -> resetService.reset("test-topic-2", "test-group-2")); - await().atMost(10, SECONDS) + await().atMost(5, SECONDS) .pollInterval(Duration.ofMillis(300)) - .untilAsserted(() -> { - try (AdminClient admin = AdminClient.create(props)) { - ConsumerGroupDescription description = admin.describeConsumerGroups(List.of("test-group")) - .describedGroups() - .get("test-group") - .get(); - - Assertions.assertTrue(description.members() - .isEmpty(), "Consumer group is still active"); - } - }); + .untilAsserted(() -> assertEquals(2L, fetchCommittedOffset("test-group-2"))); } - private void awaitCommittedOffset(long expectedOffset) { - await().atMost(10, SECONDS) - .pollInterval(Duration.ofMillis(300)) - .untilAsserted(() -> assertEquals(expectedOffset, fetchCommittedOffset())); + private long fetchCommittedOffset(String groupId) throws ExecutionException, InterruptedException { + Map offsets = adminClient.listConsumerGroupOffsets(groupId) + .partitionsToOffsetAndMetadata() + .get(); + + return offsets.values() + .iterator() + .next() + .offset(); } private static Properties getProducerConfig() { @@ -135,14 +123,14 @@ private static Properties getProducerConfig() { return producerProperties; } - private static Properties getConsumerConfig() { + private static Properties getConsumerConfig(String groupId) { Properties props = new Properties(); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_CONTAINER.getBootstrapServers()); - props.put(ConsumerConfig.GROUP_ID_CONFIG, "test-group"); + props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); - props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest"); - props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true"); + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); + props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); return props; } diff --git a/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java b/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java index e4d5b14ed8fd..35b70a83650b 100644 --- a/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java +++ b/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java @@ -22,37 +22,35 @@ public class KafkaConsumerServiceLiveTest { @Container private static final KafkaContainer KAFKA_CONTAINER = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.9.0")); - private static KafkaProducer producer; @BeforeAll static void setup() { KAFKA_CONTAINER.start(); - producer = new KafkaProducer<>(getProducerConfig()); } @AfterAll static void cleanup() { - producer.close(); KAFKA_CONTAINER.stop(); } @Test void givenConsumerReplayIsEnabled_whenReplayTimestampIsProvided_thenConsumesFromTimestamp() { - producer.send(new ProducerRecord<>("test-topic", "x1", "test1")); + KafkaProducer producer = new KafkaProducer<>(getProducerConfig()); + producer.send(new ProducerRecord<>("test-topic-1", "x1", "test1")); producer.flush(); long baseTs = System.currentTimeMillis(); - producer.send(new ProducerRecord<>("test-topic", "x2", "test2")); + producer.send(new ProducerRecord<>("test-topic-1", "x2", "test2")); producer.flush(); - KafkaConsumerService kafkaConsumerService = new KafkaConsumerService(getConsumerConfig("test-group-1"), "test-topic", baseTs); + KafkaConsumerService kafkaConsumerService = new KafkaConsumerService(getConsumerConfig("test-group-1"), "test-topic-1", baseTs); new Thread(kafkaConsumerService::start).start(); Awaitility.await() .atMost(45, TimeUnit.SECONDS) .pollInterval(1, TimeUnit.SECONDS) .untilAsserted(() -> { - List consumed = consumeFromCommittedOffset("test-group-1"); + List consumed = consumeFromCommittedOffset("test-topic-1", "test-group-1"); assertEquals(0, consumed.size()); assertFalse(consumed.contains("test1")); assertFalse(consumed.contains("test2")); @@ -63,19 +61,20 @@ void givenConsumerReplayIsEnabled_whenReplayTimestampIsProvided_thenConsumesFrom @Test void givenProducerMessagesSent_WhenConsumerIsRunningWithReplayDisabled_ThenConsumesLatestOffset() { - producer.send(new ProducerRecord<>("test-topic", "x3", "test3")); - producer.send(new ProducerRecord<>("test-topic", "x4", "test4")); + KafkaProducer producer = new KafkaProducer<>(getProducerConfig()); + producer.send(new ProducerRecord<>("test-topic-2", "x3", "test3")); + producer.send(new ProducerRecord<>("test-topic-2", "x4", "test4")); producer.flush(); KafkaConsumerService service = new KafkaConsumerService(getConsumerConfig("test-group-2"), - "test-topic", null); + "test-topic-2", null); new Thread(service::start).start(); Awaitility.await() .atMost(45, TimeUnit.SECONDS) .pollInterval(1, TimeUnit.SECONDS) .untilAsserted(() -> { - List consumed = consumeFromCommittedOffset("test-group-2"); + List consumed = consumeFromCommittedOffset("test-topic-2", "test-group-2"); assertEquals(0, consumed.size()); assertFalse(consumed.contains("test3")); assertFalse(consumed.contains("test4")); @@ -86,28 +85,28 @@ void givenProducerMessagesSent_WhenConsumerIsRunningWithReplayDisabled_ThenConsu @Test void givenConsumerWithReplayedDisabledRuns_whenReplayIsEnabled_WhenTimestampProvided_ThenConsumesFromTimestamp() throws InterruptedException { - producer.send(new ProducerRecord<>("test-topic", "x5", "test5")); + KafkaProducer producer = new KafkaProducer<>(getProducerConfig()); + producer.send(new ProducerRecord<>("test-topic-3", "x5", "test5")); producer.flush(); - String groupId = "test-group-3"; - KafkaConsumerService service1 = new KafkaConsumerService(getConsumerConfig(groupId), - "test-topic", null); + KafkaConsumerService service1 = new KafkaConsumerService(getConsumerConfig("test-group-3"), + "test-topic-3", null); new Thread(service1::start).start(); Thread.sleep(5000); service1.shutdown(); - producer.send(new ProducerRecord<>("test-topic", "x6", "test6")); + producer.send(new ProducerRecord<>("test-topic-3", "x6", "test6")); producer.flush(); - KafkaConsumerService service2 = new KafkaConsumerService(getConsumerConfig(groupId), - "test-topic", null); + KafkaConsumerService service2 = new KafkaConsumerService(getConsumerConfig("test-group-3"), + "test-topic-3", null); new Thread(service2::start).start(); Awaitility.await() .atMost(45, TimeUnit.SECONDS) .pollInterval(1, TimeUnit.SECONDS) .untilAsserted(() -> { - List consumed = consumeFromCommittedOffset(groupId); + List consumed = consumeFromCommittedOffset("test-topic-3", "test-group-3"); assertEquals(0, consumed.size()); assertFalse(consumed.contains("test5")); assertFalse(consumed.contains("test6")); @@ -117,11 +116,11 @@ void givenConsumerWithReplayedDisabledRuns_whenReplayIsEnabled_WhenTimestampProv service2.shutdown(); } - private List consumeFromCommittedOffset(String groupId) { + private List consumeFromCommittedOffset(String topic, String groupId) { List values = new ArrayList<>(); try (KafkaConsumer consumer = new KafkaConsumer<>(getConsumerConfig(groupId))) { - consumer.subscribe(Collections.singleton("test-topic")); + consumer.subscribe(Collections.singleton(topic)); ConsumerRecords records = consumer.poll(java.time.Duration.ofSeconds(2)); for (ConsumerRecord r : records) { From f9fae8ee1cf94ef223f1ddb77aee4916fe27e1e6 Mon Sep 17 00:00:00 2001 From: saikat Date: Tue, 20 Jan 2026 11:27:46 +0530 Subject: [PATCH 08/38] refactored test code --- .../kafka/admin/ResetOffsetServiceTest.java | 18 +++++++++--------- .../consumer/KafkaConsumerServiceLiveTest.java | 18 ++++++++++-------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/apache-kafka-4/src/test/java/com/baeldung/kafka/admin/ResetOffsetServiceTest.java b/apache-kafka-4/src/test/java/com/baeldung/kafka/admin/ResetOffsetServiceTest.java index 9edc2cef9b35..b60dada4fcdb 100644 --- a/apache-kafka-4/src/test/java/com/baeldung/kafka/admin/ResetOffsetServiceTest.java +++ b/apache-kafka-4/src/test/java/com/baeldung/kafka/admin/ResetOffsetServiceTest.java @@ -124,14 +124,14 @@ private static Properties getProducerConfig() { } private static Properties getConsumerConfig(String groupId) { - Properties props = new Properties(); - props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_CONTAINER.getBootstrapServers()); - props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); - props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); - props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); - props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); - - return props; + Properties consumerProperties = new Properties(); + consumerProperties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_CONTAINER.getBootstrapServers()); + consumerProperties.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); + consumerProperties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); + consumerProperties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); + consumerProperties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); + consumerProperties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); + + return consumerProperties; } } diff --git a/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java b/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java index 35b70a83650b..5ed78df73ee8 100644 --- a/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java +++ b/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java @@ -136,17 +136,19 @@ private static Properties getProducerConfig() { producerProperties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_CONTAINER.getBootstrapServers()); producerProperties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); producerProperties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + return producerProperties; } private static Properties getConsumerConfig(String groupId) { - Properties props = new Properties(); - props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_CONTAINER.getBootstrapServers()); - props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); - props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); - props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); - props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest"); - props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); - return props; + Properties consumerProperties = new Properties(); + consumerProperties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_CONTAINER.getBootstrapServers()); + consumerProperties.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); + consumerProperties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); + consumerProperties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); + consumerProperties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest"); + consumerProperties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); + + return consumerProperties; } } From 77ba1548c45de143740947ed902ea7e616e29929 Mon Sep 17 00:00:00 2001 From: saikat Date: Tue, 20 Jan 2026 12:26:09 +0530 Subject: [PATCH 09/38] refactored code --- .../kafka/resetoffset/admin/ResetOffsetService.java | 1 + .../kafka/resetoffset/consumer/KafkaConsumerService.java | 5 +++-- .../kafka/consumer/KafkaConsumerServiceLiveTest.java | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/ResetOffsetService.java b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/ResetOffsetService.java index d523e513e8cf..fb6da8503362 100644 --- a/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/ResetOffsetService.java +++ b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/ResetOffsetService.java @@ -13,6 +13,7 @@ import com.baeldung.kafka.resetoffset.consumer.KafkaConsumerService; public class ResetOffsetService { + private static final Logger log = LoggerFactory.getLogger(KafkaConsumerService.class); private final AdminClient adminClient; diff --git a/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/KafkaConsumerService.java b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/KafkaConsumerService.java index a254e5f74638..a4e6afebc9dd 100644 --- a/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/KafkaConsumerService.java +++ b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/KafkaConsumerService.java @@ -16,9 +16,10 @@ public class KafkaConsumerService { private final KafkaConsumer consumer; private final AtomicBoolean running = new AtomicBoolean(true); - public KafkaConsumerService(Properties consumerProps, String topic, Long replayFromTimestampInEpoch) { + public KafkaConsumerService(Properties consumerProps, String topic, long replayFromTimestampInEpoch) { this.consumer = new KafkaConsumer<>(consumerProps); - if (replayFromTimestampInEpoch != null) { + + if (replayFromTimestampInEpoch > 0) { consumer.subscribe(List.of(topic), new ReplayRebalanceListener(consumer, replayFromTimestampInEpoch)); } else { consumer.subscribe(List.of(topic)); diff --git a/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java b/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java index 5ed78df73ee8..98cfc6ad697a 100644 --- a/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java +++ b/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java @@ -67,7 +67,7 @@ void givenProducerMessagesSent_WhenConsumerIsRunningWithReplayDisabled_ThenConsu producer.flush(); KafkaConsumerService service = new KafkaConsumerService(getConsumerConfig("test-group-2"), - "test-topic-2", null); + "test-topic-2", 0L); new Thread(service::start).start(); Awaitility.await() @@ -90,7 +90,7 @@ void givenConsumerWithReplayedDisabledRuns_whenReplayIsEnabled_WhenTimestampProv producer.flush(); KafkaConsumerService service1 = new KafkaConsumerService(getConsumerConfig("test-group-3"), - "test-topic-3", null); + "test-topic-3", 0L); new Thread(service1::start).start(); Thread.sleep(5000); service1.shutdown(); @@ -99,7 +99,7 @@ void givenConsumerWithReplayedDisabledRuns_whenReplayIsEnabled_WhenTimestampProv producer.flush(); KafkaConsumerService service2 = new KafkaConsumerService(getConsumerConfig("test-group-3"), - "test-topic-3", null); + "test-topic-3", 0L); new Thread(service2::start).start(); Awaitility.await() From 7ca07b9712adfb47e934231906541bc5fe44494d Mon Sep 17 00:00:00 2001 From: saikat Date: Tue, 20 Jan 2026 22:34:30 +0530 Subject: [PATCH 10/38] refactored code --- .../resetoffset/admin/ResetOffsetService.java | 4 +-- .../kafka/admin/ResetOffsetServiceTest.java | 28 ++++++++--------- .../test/resources/docker/docker-compose.yml | 30 ++++++++----------- 3 files changed, 28 insertions(+), 34 deletions(-) diff --git a/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/ResetOffsetService.java b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/ResetOffsetService.java index fb6da8503362..489fb8199e1f 100644 --- a/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/ResetOffsetService.java +++ b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/ResetOffsetService.java @@ -10,11 +10,9 @@ import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; -import com.baeldung.kafka.resetoffset.consumer.KafkaConsumerService; - public class ResetOffsetService { - private static final Logger log = LoggerFactory.getLogger(KafkaConsumerService.class); + private static final Logger log = LoggerFactory.getLogger(ResetOffsetService.class); private final AdminClient adminClient; public ResetOffsetService(String bootstrapServers) { diff --git a/apache-kafka-4/src/test/java/com/baeldung/kafka/admin/ResetOffsetServiceTest.java b/apache-kafka-4/src/test/java/com/baeldung/kafka/admin/ResetOffsetServiceTest.java index b60dada4fcdb..65a8b9b9efb8 100644 --- a/apache-kafka-4/src/test/java/com/baeldung/kafka/admin/ResetOffsetServiceTest.java +++ b/apache-kafka-4/src/test/java/com/baeldung/kafka/admin/ResetOffsetServiceTest.java @@ -30,7 +30,7 @@ class ResetOffsetServiceTest { @Container private static final KafkaContainer KAFKA_CONTAINER = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.9.0")); private static ResetOffsetService resetService; - private static AdminClient adminClient; + private static AdminClient testAdminClient; @BeforeAll static void startKafka() { @@ -39,12 +39,12 @@ static void startKafka() { Properties props = new Properties(); props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_CONTAINER.getBootstrapServers()); - adminClient = AdminClient.create(props); + testAdminClient = AdminClient.create(props); } @AfterAll static void stopKafka() { - adminClient.close(); + testAdminClient.close(); resetService.close(); KAFKA_CONTAINER.stop(); } @@ -103,17 +103,6 @@ void givenConsumerIsStillActive_whenOffsetResetIsCalled_thenThrowRuntimeExceptio .untilAsserted(() -> assertEquals(2L, fetchCommittedOffset("test-group-2"))); } - private long fetchCommittedOffset(String groupId) throws ExecutionException, InterruptedException { - Map offsets = adminClient.listConsumerGroupOffsets(groupId) - .partitionsToOffsetAndMetadata() - .get(); - - return offsets.values() - .iterator() - .next() - .offset(); - } - private static Properties getProducerConfig() { Properties producerProperties = new Properties(); producerProperties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_CONTAINER.getBootstrapServers()); @@ -134,4 +123,15 @@ private static Properties getConsumerConfig(String groupId) { return consumerProperties; } + + private long fetchCommittedOffset(String groupId) throws ExecutionException, InterruptedException { + Map offsets = testAdminClient.listConsumerGroupOffsets(groupId) + .partitionsToOffsetAndMetadata() + .get(); + + return offsets.values() + .iterator() + .next() + .offset(); + } } diff --git a/apache-kafka-4/src/test/resources/docker/docker-compose.yml b/apache-kafka-4/src/test/resources/docker/docker-compose.yml index 82e19bc0d4b5..418eb031c22d 100644 --- a/apache-kafka-4/src/test/resources/docker/docker-compose.yml +++ b/apache-kafka-4/src/test/resources/docker/docker-compose.yml @@ -1,27 +1,23 @@ version: "3.8" services: - zookeeper: - image: confluentinc/cp-zookeeper:7.6.0 - hostname: zookeeper - container_name: zookeeper - ports: - - "2181:2181" - environment: - ZOOKEEPER_CLIENT_PORT: 2181 - ZOOKEEPER_TICK_TIME: 2000 - kafka: - image: confluentinc/cp-kafka:7.6.0 + image: confluentinc/cp-kafka:7.9.0 hostname: kafka container_name: kafka - depends_on: - - zookeeper ports: - "9092:9092" + - "9101:9101" + expose: + - '29092' environment: - KAFKA_BROKER_ID: 1 - KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 - KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 + KAFKA_NODE_ID: 1 + CLUSTER_ID: 'MkU3OEVBNTcwNTJENDM2Qk' + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT' + KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092' + KAFKA_LISTENERS: 'PLAINTEXT://kafka:29092,CONTROLLER://kafka:29093,PLAINTEXT_HOST://0.0.0.0:9092' KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_PROCESS_ROLES: 'broker,controller' + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka:29093' + KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT' + KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' From 755d33f0eb34a356f0e054a6783e61a97f1290a7 Mon Sep 17 00:00:00 2001 From: saikat Date: Wed, 21 Jan 2026 22:54:19 +0530 Subject: [PATCH 11/38] refactored code --- apache-kafka-4/src/test/resources/docker/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apache-kafka-4/src/test/resources/docker/docker-compose.yml b/apache-kafka-4/src/test/resources/docker/docker-compose.yml index 418eb031c22d..665e6c3119f2 100644 --- a/apache-kafka-4/src/test/resources/docker/docker-compose.yml +++ b/apache-kafka-4/src/test/resources/docker/docker-compose.yml @@ -12,7 +12,6 @@ services: - '29092' environment: KAFKA_NODE_ID: 1 - CLUSTER_ID: 'MkU3OEVBNTcwNTJENDM2Qk' KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT' KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092' KAFKA_LISTENERS: 'PLAINTEXT://kafka:29092,CONTROLLER://kafka:29093,PLAINTEXT_HOST://0.0.0.0:9092' @@ -21,3 +20,4 @@ services: KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka:29093' KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT' KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER' + CLUSTER_ID: 'MkU3OEVBNTcwNTJENDM2Qk' From 46bdd7f10ab53d338425efdb7f9fac0a988e0c32 Mon Sep 17 00:00:00 2001 From: saikat Date: Thu, 22 Jan 2026 14:27:32 +0530 Subject: [PATCH 12/38] refactored code --- .../baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java b/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java index 98cfc6ad697a..2464f2c01f29 100644 --- a/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java +++ b/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java @@ -11,6 +11,7 @@ import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; +import java.time.Duration; import java.util.*; import java.util.concurrent.TimeUnit; @@ -122,7 +123,7 @@ private List consumeFromCommittedOffset(String topic, String groupId) { try (KafkaConsumer consumer = new KafkaConsumer<>(getConsumerConfig(groupId))) { consumer.subscribe(Collections.singleton(topic)); - ConsumerRecords records = consumer.poll(java.time.Duration.ofSeconds(2)); + ConsumerRecords records = consumer.poll(Duration.ofSeconds(2)); for (ConsumerRecord r : records) { values.add(r.value()); } From 7d025bbbc4fe51f10e900d1deeb201a24f9b0842 Mon Sep 17 00:00:00 2001 From: saikat Date: Fri, 23 Jan 2026 14:42:33 +0530 Subject: [PATCH 13/38] refactored code --- .../resetoffset/admin/ResetOffsetService.java | 25 +++++++++++-------- .../consumer/ReplayRebalanceListener.java | 7 +++--- ...t.java => ResetOffsetServiceLiveTest.java} | 2 +- .../KafkaConsumerServiceLiveTest.java | 8 ++++-- 4 files changed, 25 insertions(+), 17 deletions(-) rename apache-kafka-4/src/test/java/com/baeldung/kafka/admin/{ResetOffsetServiceTest.java => ResetOffsetServiceLiveTest.java} (99%) diff --git a/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/ResetOffsetService.java b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/ResetOffsetService.java index 489fb8199e1f..98bbfcac1cc4 100644 --- a/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/ResetOffsetService.java +++ b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/admin/ResetOffsetService.java @@ -58,18 +58,21 @@ private Map fetchEarliestOffsets(List offsets = new HashMap<>(); - for (var tp : partitions) { - long offset; - try { - offset = offsetsResult.partitionResult(tp) - .get() - .offset(); - } catch (InterruptedException | ExecutionException ex) { - log.error("Error in the Kafka Consumer reset with exception {}", ex.getMessage(), ex); - throw new RuntimeException(ex); - } + partitions.forEach(tp -> { + long offset = Optional.ofNullable(offsetsResult.partitionResult(tp)) + .map(kafkaFuture -> { + try { + return kafkaFuture.get(); + } catch (InterruptedException | ExecutionException ex) { + log.error("Error in the Kafka Consumer reset with exception {}", ex.getMessage(), ex); + throw new RuntimeException(ex); + } + }) + .map(ListOffsetsResult.ListOffsetsResultInfo::offset) + .orElseThrow(() -> new RuntimeException("No offset result returned for partition " + tp)); + offsets.put(tp, new OffsetAndMetadata(offset)); - } + }); return offsets; } diff --git a/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/ReplayRebalanceListener.java b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/ReplayRebalanceListener.java index 195341a42927..abed553485ac 100644 --- a/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/ReplayRebalanceListener.java +++ b/apache-kafka-4/src/main/java/com/baeldung/kafka/resetoffset/consumer/ReplayRebalanceListener.java @@ -6,6 +6,7 @@ import org.apache.kafka.common.TopicPartition; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import java.util.stream.Collectors; @@ -13,7 +14,7 @@ public class ReplayRebalanceListener implements ConsumerRebalanceListener { private final KafkaConsumer consumer; private final long replayFromTimeInEpoch; - private boolean seekDone = false; + private final AtomicBoolean seekDone = new AtomicBoolean(false); public ReplayRebalanceListener(KafkaConsumer consumer, long replayFromTimeInEpoch) { this.consumer = consumer; @@ -27,7 +28,7 @@ public void onPartitionsRevoked(Collection partitions) { @Override public void onPartitionsAssigned(Collection partitions) { - if (seekDone || partitions.isEmpty()) { + if (seekDone.get() || partitions.isEmpty()) { return; } @@ -43,6 +44,6 @@ public void onPartitionsAssigned(Collection partitions) { } }); - seekDone = true; + seekDone.set(true); } } diff --git a/apache-kafka-4/src/test/java/com/baeldung/kafka/admin/ResetOffsetServiceTest.java b/apache-kafka-4/src/test/java/com/baeldung/kafka/admin/ResetOffsetServiceLiveTest.java similarity index 99% rename from apache-kafka-4/src/test/java/com/baeldung/kafka/admin/ResetOffsetServiceTest.java rename to apache-kafka-4/src/test/java/com/baeldung/kafka/admin/ResetOffsetServiceLiveTest.java index 65a8b9b9efb8..6bdc9c4875dd 100644 --- a/apache-kafka-4/src/test/java/com/baeldung/kafka/admin/ResetOffsetServiceTest.java +++ b/apache-kafka-4/src/test/java/com/baeldung/kafka/admin/ResetOffsetServiceLiveTest.java @@ -25,7 +25,7 @@ import com.baeldung.kafka.resetoffset.admin.ResetOffsetService; @Testcontainers -class ResetOffsetServiceTest { +class ResetOffsetServiceLiveTest { @Container private static final KafkaContainer KAFKA_CONTAINER = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.9.0")); diff --git a/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java b/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java index 2464f2c01f29..234e18c82f66 100644 --- a/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java +++ b/apache-kafka-4/src/test/java/com/baeldung/kafka/consumer/KafkaConsumerServiceLiveTest.java @@ -37,11 +37,14 @@ static void cleanup() { @Test void givenConsumerReplayIsEnabled_whenReplayTimestampIsProvided_thenConsumesFromTimestamp() { KafkaProducer producer = new KafkaProducer<>(getProducerConfig()); - producer.send(new ProducerRecord<>("test-topic-1", "x1", "test1")); + long firstMsgTs = System.currentTimeMillis(); + producer.send(new ProducerRecord<>("test-topic-1", 0, firstMsgTs, "x1", "test1")); producer.flush(); long baseTs = System.currentTimeMillis(); - producer.send(new ProducerRecord<>("test-topic-1", "x2", "test2")); + + long secondMsgTs = baseTs + 1L; + producer.send(new ProducerRecord<>("test-topic-1", 0, secondMsgTs, "x2", "test2")); producer.flush(); KafkaConsumerService kafkaConsumerService = new KafkaConsumerService(getConsumerConfig("test-group-1"), "test-topic-1", baseTs); @@ -150,6 +153,7 @@ private static Properties getConsumerConfig(String groupId) { consumerProperties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest"); consumerProperties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); + return consumerProperties; } } From 6a77f5a85919e36d043fc6a9f02849fa73d88e1a Mon Sep 17 00:00:00 2001 From: saikat Date: Sat, 14 Feb 2026 22:45:50 +0530 Subject: [PATCH 14/38] API Versioning in Spring boot 4 --- spring-boot-modules/spring-boot-5/pom.xml | 61 ++++++++++++ .../apiversions/ExampleApplication.java | 12 +++ .../config/WebHeaderBasedConfig.java | 16 +++ .../config/WebMediaTypeConfig.java | 19 ++++ .../config/WebPathSegmentConfig.java | 17 ++++ .../config/WebQueryParamConfig.java | 17 ++++ .../controller/ProductController.java | 46 +++++++++ .../ProductControllerWithCustomMedia.java | 48 +++++++++ .../ProductControllerWithPathSegment.java | 46 +++++++++ .../baeldung/apiversions/model/Product.java | 5 + .../baeldung/apiversions/model/ProductV2.java | 5 + .../src/main/resources/application.properties | 18 ++++ .../ProductControllerHeaderLiveTest.java | 75 ++++++++++++++ .../ProductControllerMediaTypeLiveTest.java | 98 +++++++++++++++++++ .../ProductControllerPathSegmentLiveTest.java | 73 ++++++++++++++ .../ProductControllerQueryParamLiveTest.java | 78 +++++++++++++++ 16 files changed, 634 insertions(+) create mode 100644 spring-boot-modules/spring-boot-5/pom.xml create mode 100644 spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/ExampleApplication.java create mode 100644 spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebHeaderBasedConfig.java create mode 100644 spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebMediaTypeConfig.java create mode 100644 spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebPathSegmentConfig.java create mode 100644 spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebQueryParamConfig.java create mode 100644 spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/controller/ProductController.java create mode 100644 spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/controller/ProductControllerWithCustomMedia.java create mode 100644 spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/controller/ProductControllerWithPathSegment.java create mode 100644 spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/Product.java create mode 100644 spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/ProductV2.java create mode 100644 spring-boot-modules/spring-boot-5/src/main/resources/application.properties create mode 100644 spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerHeaderLiveTest.java create mode 100644 spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerMediaTypeLiveTest.java create mode 100644 spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerPathSegmentLiveTest.java create mode 100644 spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerQueryParamLiveTest.java diff --git a/spring-boot-modules/spring-boot-5/pom.xml b/spring-boot-modules/spring-boot-5/pom.xml new file mode 100644 index 000000000000..d42bf0bd44eb --- /dev/null +++ b/spring-boot-modules/spring-boot-5/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + spring-boot-5 + 0.0.1-SNAPSHOT + spring-boot-5 + Demo project for Spring Boot 4 + + + com.baeldung.spring-boot-modules + spring-boot-modules + 1.0.0-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + 21 + 21 + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + + + 4.0.2 + 6.0.0 + 1.5.18 + + diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/ExampleApplication.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/ExampleApplication.java new file mode 100644 index 000000000000..74e6ceb79d85 --- /dev/null +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/ExampleApplication.java @@ -0,0 +1,12 @@ +package com.baeldung.apiversions; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ExampleApplication { + + public static void main(String[] args) { + SpringApplication.run(ExampleApplication.class, args); + } +} diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebHeaderBasedConfig.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebHeaderBasedConfig.java new file mode 100644 index 000000000000..bc2548b94831 --- /dev/null +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebHeaderBasedConfig.java @@ -0,0 +1,16 @@ +package com.baeldung.apiversions.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebHeaderBasedConfig implements WebMvcConfigurer { + + @Override + public void configureApiVersioning(ApiVersionConfigurer configurer) { + configurer.addSupportedVersions("1.0.0", "2.0.0") + .setDefaultVersion("2.0.0") + .useRequestHeader("X-API-Version"); + } +} diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebMediaTypeConfig.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebMediaTypeConfig.java new file mode 100644 index 000000000000..e5ec1ff67752 --- /dev/null +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebMediaTypeConfig.java @@ -0,0 +1,19 @@ +package com.baeldung.apiversions.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMediaTypeConfig implements WebMvcConfigurer { + + @Override + public void configureApiVersioning(ApiVersionConfigurer configurer) { + configurer + .addSupportedVersions("1.0.0", "2.0.0") + .setDefaultVersion("1.0.0") + .useMediaTypeParameter(MediaType.parseMediaType("application/vnd.baeldung.product+json"), + "version"); + } +} diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebPathSegmentConfig.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebPathSegmentConfig.java new file mode 100644 index 000000000000..321a3666d13c --- /dev/null +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebPathSegmentConfig.java @@ -0,0 +1,17 @@ +package com.baeldung.apiversions.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebPathSegmentConfig implements WebMvcConfigurer { + + @Override + public void configureApiVersioning(ApiVersionConfigurer configurer) { + configurer + .usePathSegment(1) + .setDefaultVersion(null) + .addSupportedVersions("1.0.0", "2.0.0"); + } +} diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebQueryParamConfig.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebQueryParamConfig.java new file mode 100644 index 000000000000..abc3d966ac97 --- /dev/null +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebQueryParamConfig.java @@ -0,0 +1,17 @@ +package com.baeldung.apiversions.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebQueryParamConfig implements WebMvcConfigurer { + + @Override + public void configureApiVersioning(ApiVersionConfigurer configurer) { + configurer + .addSupportedVersions("1.0.0", "2.0.0") + .setDefaultVersion("1.0.0") + .useQueryParam("version"); + } +} diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/controller/ProductController.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/controller/ProductController.java new file mode 100644 index 000000000000..19132b316b31 --- /dev/null +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/controller/ProductController.java @@ -0,0 +1,46 @@ +package com.baeldung.apiversions.controller; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.annotation.PostConstruct; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.baeldung.apiversions.model.Product; +import com.baeldung.apiversions.model.ProductV2; + +@RestController +@RequestMapping(path = "/api/products") +public class ProductController { + + private static final Logger LOGGER = LoggerFactory.getLogger(ProductController.class); + private final Map productsMap = new HashMap<>(); + private final Map productsV2Map = new HashMap<>(); + + @GetMapping(value = "/{id}", version = "1.0.0") + public ResponseEntity getProductV1ById(@PathVariable String id) { + LOGGER.info("Get Product version 1 for id {}", id); + return new ResponseEntity<>(productsMap.get(id), HttpStatus.OK); + } + + @GetMapping(value = "/{id}", version = "2.0.0") + public ResponseEntity getProductV2ById(@PathVariable String id) { + LOGGER.info("Get Product version 2 for id {}", id); + return new ResponseEntity<>(productsV2Map.get(id), HttpStatus.OK); + } + + @PostConstruct + public void init(){ + productsMap.put("1001", new Product("1001", "apple", + "apple_long_desc", 1.99)); + productsV2Map.put("1001", new ProductV2("1001", "apple", 1.99)); + } +} diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/controller/ProductControllerWithCustomMedia.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/controller/ProductControllerWithCustomMedia.java new file mode 100644 index 000000000000..5e9732172d5d --- /dev/null +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/controller/ProductControllerWithCustomMedia.java @@ -0,0 +1,48 @@ +package com.baeldung.apiversions.controller; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.annotation.PostConstruct; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.baeldung.apiversions.model.Product; +import com.baeldung.apiversions.model.ProductV2; + +@RestController +@RequestMapping(path = "/api/products") +public class ProductControllerWithCustomMedia { + + private static final Logger LOGGER = LoggerFactory.getLogger(ProductControllerWithCustomMedia.class); + private final Map productsMap = new HashMap<>(); + private final Map productsV2Map = new HashMap<>(); + + @GetMapping(value = "/{id}", version = "1.0.0", + produces = "application/vnd.baeldung.product+json") + public ResponseEntity getProductByIdCustomMedia(@PathVariable String id) { + LOGGER.info("Get Product with custom media version 1 for id {}", id); + return new ResponseEntity<>(productsMap.get(id), HttpStatus.OK); + } + + @GetMapping(value = "/{id}", version = "2.0.0", + produces = "application/vnd.baeldung.product+json") + public ResponseEntity getProductV2ByIdCustomMedia(@PathVariable String id) { + LOGGER.info("Get Product with custom media version 2 for id {}", id); + return new ResponseEntity<>(productsV2Map.get(id), HttpStatus.OK); + } + + @PostConstruct + public void init(){ + productsMap.put("1001", new Product("1001", "apple", + "apple_long_desc", 1.99)); + productsV2Map.put("1001", new ProductV2("1001", "apple", 1.99)); + } +} diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/controller/ProductControllerWithPathSegment.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/controller/ProductControllerWithPathSegment.java new file mode 100644 index 000000000000..308b588873c8 --- /dev/null +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/controller/ProductControllerWithPathSegment.java @@ -0,0 +1,46 @@ +package com.baeldung.apiversions.controller; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.annotation.PostConstruct; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.baeldung.apiversions.model.Product; +import com.baeldung.apiversions.model.ProductV2; + +@RestController +@RequestMapping(path = "/api/v{version}/products") +public class ProductControllerWithPathSegment { + + private static final Logger LOGGER = LoggerFactory.getLogger(ProductControllerWithPathSegment.class); + private final Map productsMap = new HashMap<>(); + private final Map productsV2Map = new HashMap<>(); + + @GetMapping(value = "/{id}", version = "1.0.0") + public ResponseEntity getProductV1ByIdPath(@PathVariable String id) { + LOGGER.info("Get Product with Path specific version 1 for id {}", id); + return new ResponseEntity<>(productsMap.get(id), HttpStatus.OK); + } + + @GetMapping(value = "/{id}", version = "2.0.0") + public ResponseEntity getProductV2ByIdPath(@PathVariable String id) { + LOGGER.info("Get Product with Path specific version 2 for id {}", id); + return new ResponseEntity<>(productsV2Map.get(id), HttpStatus.OK); + } + + @PostConstruct + public void init(){ + productsMap.put("1001", new Product("1001", "apple", + "apple_long_desc", 1.99)); + productsV2Map.put("1001", new ProductV2("1001", "apple", 1.99)); + } +} diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/Product.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/Product.java new file mode 100644 index 000000000000..af70113e67fb --- /dev/null +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/Product.java @@ -0,0 +1,5 @@ +package com.baeldung.apiversions.model; + +public record Product(String id, String name, String desc, double price) { + +} diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/ProductV2.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/ProductV2.java new file mode 100644 index 000000000000..6de10144022e --- /dev/null +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/ProductV2.java @@ -0,0 +1,5 @@ +package com.baeldung.apiversions.model; + +public record ProductV2(String id, String name, double price) { + +} diff --git a/spring-boot-modules/spring-boot-5/src/main/resources/application.properties b/spring-boot-modules/spring-boot-5/src/main/resources/application.properties new file mode 100644 index 000000000000..2e679c331207 --- /dev/null +++ b/spring-boot-modules/spring-boot-5/src/main/resources/application.properties @@ -0,0 +1,18 @@ +spring.application.name=demo +server.port=8081 + +#spring.mvc.apiversion.supported=1.0.0,2.0.0 +#spring.mvc.apiversion.default=1.0.0 +#spring.mvc.apiversion.enabled=true + +#spring.mvc.apiversion.strategy=HEADER +#spring.mvc.apiversion.use.header=X-API-VERSION + +#spring.mvc.apiversion.strategy=QUERY +#spring.mvc.apiversion.use.query-parameter=version + +#spring.mvc.apiversion.strategy=MEDIA_TYPE +#spring.mvc.apiversion.use.media-type-parameter[application/vnd.baeldung.product+json]=version + +#spring.mvc.apiversion.strategy=PATH +#spring.mvc.apiversion.use.path-segment=1 diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerHeaderLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerHeaderLiveTest.java new file mode 100644 index 000000000000..5dbb184e4575 --- /dev/null +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerHeaderLiveTest.java @@ -0,0 +1,75 @@ +package com.baeldung.apiversions.controller; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.web.context.WebApplicationContext; + +import com.baeldung.apiversions.ExampleApplication; +import com.baeldung.apiversions.config.WebHeaderBasedConfig; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = ExampleApplication.class) +@ContextConfiguration(classes = { ProductController.class, WebHeaderBasedConfig.class, }) +class ProductControllerHeaderLiveTest { + + private RestTestClient restTestClient; + + @BeforeEach + void setUp(WebApplicationContext context) { + restTestClient = RestTestClient.bindToApplicationContext(context) + .build(); + } + + @Test + void givenProductExists_WhenProductAPIIsCalled_WithHeaderVersion1_thenReturnValidProduct() { + restTestClient.get() + .uri("/api/products/1001") + .header("X-API-Version", "1") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("$.name") + .isEqualTo("apple") + .jsonPath("$.desc") + .isEqualTo("apple_long_desc") + .jsonPath("$.price") + .isEqualTo(1.99); + } + + @Test + void givenProductExists_WhenProductAPIIsCalled_WithHeaderVersion2_thenReturnValidProduct() { + restTestClient.get() + .uri("/api/products/1001") + .header("X-API-Version", "2") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("$.name") + .isEqualTo("apple") + .jsonPath("$.desc") + .doesNotExist() + .jsonPath("$.price") + .isEqualTo(1.99); + } + + @Test + void givenProductExists_WhenProductAPIIsCalled_WithInvalidHeaderVersion_thenReturnBadRequestError() { + restTestClient.get() + .uri("/api/products/1001") + .header("X-API-Version", "3") + .exchange() + .expectStatus() + .is4xxClientError() + .expectBody() + .jsonPath("$.name") + .doesNotExist() + .jsonPath("$.desc") + .doesNotExist() + .jsonPath("$.price") + .doesNotExist(); + } +} diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerMediaTypeLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerMediaTypeLiveTest.java new file mode 100644 index 000000000000..8f75759501d4 --- /dev/null +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerMediaTypeLiveTest.java @@ -0,0 +1,98 @@ +package com.baeldung.apiversions.controller; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.web.context.WebApplicationContext; + +import com.baeldung.apiversions.ExampleApplication; +import com.baeldung.apiversions.config.WebHeaderBasedConfig; +import com.baeldung.apiversions.config.WebMediaTypeConfig; +import com.baeldung.apiversions.config.WebPathSegmentConfig; +import com.baeldung.apiversions.config.WebQueryParamConfig; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = ExampleApplication.class) +@ContextConfiguration(classes = { ProductControllerWithCustomMedia.class, WebMediaTypeConfig.class }) +class ProductControllerMediaTypeLiveTest { + + private RestTestClient restTestClient; + + @BeforeEach + void setUp(WebApplicationContext context) { + restTestClient = RestTestClient.bindToApplicationContext(context) + .build(); + } + + @Test + void givenProductExists_WhenProductAPIIsCalled_WithValidMediaTypeVersion_thenReturnValidProduct() { + restTestClient.get() + .uri("/api/products/1001") + .accept(MediaType.valueOf("application/vnd.baeldung.product+json;version=1")) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("$.name") + .isEqualTo("apple") + .jsonPath("$.desc") + .isEqualTo("apple_long_desc") + .jsonPath("$.price") + .isEqualTo(1.99); + } + + @Test + void givenProductExists_WhenProductAPIIsCalled_WithValidMediaType_thenReturnValidProduct() { + restTestClient.get() + .uri("/api/products/1001") + .accept(MediaType.valueOf("application/vnd.baeldung.product+json;version=2")) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("$.name") + .isEqualTo("apple") + .jsonPath("$.desc") + .doesNotExist() + .jsonPath("$.price") + .isEqualTo(1.99); + } + + @Test + void givenProductExists_WhenProductAPIIsCalled_WithInValidMediaTypeVersion_thenReturnBadRequestError() { + restTestClient.get() + .uri("/api/products/1001") + .accept(MediaType.valueOf("application/vnd.baeldung.product+json;version=3")) + .exchange() + .expectStatus() + .is4xxClientError() + .expectBody() + .jsonPath("$.name") + .doesNotExist() + .jsonPath("$.desc") + .doesNotExist() + .jsonPath("$.price") + .doesNotExist(); + } + + @Test + void givenProductExists_WhenProductAPIIsCalled_WithInValidMediaType_thenReturnBadRequestError() { + restTestClient.get() + .uri("/api/products/1001") + .accept(MediaType.valueOf("application/invalid")) + .exchange() + .expectStatus() + .is4xxClientError() + .expectBody() + .jsonPath("$.name") + .doesNotExist() + .jsonPath("$.desc") + .doesNotExist() + .jsonPath("$.price") + .doesNotExist(); + } +} diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerPathSegmentLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerPathSegmentLiveTest.java new file mode 100644 index 000000000000..bf7cf080e55e --- /dev/null +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerPathSegmentLiveTest.java @@ -0,0 +1,73 @@ +package com.baeldung.apiversions.controller; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.web.context.WebApplicationContext; + +import com.baeldung.apiversions.ExampleApplication; +import com.baeldung.apiversions.config.WebHeaderBasedConfig; +import com.baeldung.apiversions.config.WebMediaTypeConfig; +import com.baeldung.apiversions.config.WebPathSegmentConfig; +import com.baeldung.apiversions.config.WebQueryParamConfig; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = ExampleApplication.class) +@ContextConfiguration(classes = { ProductControllerWithPathSegment.class, WebPathSegmentConfig.class }) +@DisplayName("All Get Products API Versions Tests") +class ProductControllerPathSegmentLiveTest { + + private RestTestClient restTestClient; + + @BeforeEach + void setUp(WebApplicationContext context) { + restTestClient = RestTestClient.bindToApplicationContext(context) + .build(); + } + + @Test + void givenProductExists_WhenGetProductAPIIsCalled_WithPathSegmentV1_thenReturnValidProduct() { + restTestClient.get() + .uri("/api/v1/products/1001") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("$.name") + .isEqualTo("apple") + .jsonPath("$.desc") + .isEqualTo("apple_long_desc") + .jsonPath("$.price") + .isEqualTo(1.99); + } + + @Test + void givenProductExists_WhenProductAPIIsCalled_WithPathSegmentV2_thenReturnValidProduct() { + restTestClient.get() + .uri("/api/v2/products/1001") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("$.name") + .isEqualTo("apple") + .jsonPath("$.desc") + .doesNotExist() + .jsonPath("$.price") + .isEqualTo(1.99); + } + + @Test + void givenProductExists_WhenProductAPIIsCalled_WithPathSegment2_thenThrowNotFoundError() { + restTestClient.get() + .uri("/api/2/products/1001") + .exchange() + .expectStatus() + .isNotFound(); + } + +} diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerQueryParamLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerQueryParamLiveTest.java new file mode 100644 index 000000000000..b90b02ef666f --- /dev/null +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerQueryParamLiveTest.java @@ -0,0 +1,78 @@ +package com.baeldung.apiversions.controller; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.web.context.WebApplicationContext; + +import com.baeldung.apiversions.ExampleApplication; +import com.baeldung.apiversions.config.WebHeaderBasedConfig; +import com.baeldung.apiversions.config.WebMediaTypeConfig; +import com.baeldung.apiversions.config.WebPathSegmentConfig; +import com.baeldung.apiversions.config.WebQueryParamConfig; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = ExampleApplication.class) +@ContextConfiguration(classes = { ProductController.class, WebQueryParamConfig.class }) +class ProductControllerQueryParamLiveTest { + + private RestTestClient restTestClient; + + @BeforeEach + void setUp(WebApplicationContext context) { + restTestClient = RestTestClient.bindToApplicationContext(context) + .build(); + } + + @Test + void givenProductExists_WhenProductAPIIsCalled_WithQueryParamVersion1_thenReturnValidProduct() { + restTestClient.get() + .uri("/api/products/1001?version=1") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("$.name") + .isEqualTo("apple") + .jsonPath("$.desc") + .isEqualTo("apple_long_desc") + .jsonPath("$.price") + .isEqualTo(1.99); + } + + @Test + void givenProductExists_WhenProductAPIIsCalled_WithQueryParamVersion2_thenReturnValidProduct() { + restTestClient.get() + .uri("/api/products/1001?version=2") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("$.name") + .isEqualTo("apple") + .jsonPath("$.desc") + .doesNotExist() + .jsonPath("$.price") + .isEqualTo(1.99); + } + + @Test + void givenProductExists_WhenProductAPIIsCalled_WithInvalidQueryParam_thenReturnBadRequestError() { + restTestClient.get() + .uri("/api/products/1001?version=invalid") + .exchange() + .expectStatus() + .is4xxClientError() + .expectBody() + .jsonPath("$.name") + .doesNotExist() + .jsonPath("$.desc") + .doesNotExist() + .jsonPath("$.price") + .doesNotExist(); + } +} From cf6c0e2c297f437c92f684c63e859cdb9e3406d5 Mon Sep 17 00:00:00 2001 From: saikat Date: Sun, 15 Feb 2026 14:17:32 +0530 Subject: [PATCH 15/38] API Versioning in Spring boot 4 refactoring --- .../{ => header}/ExampleApplication.java | 4 +- .../ProductController.java | 6 +-- .../WebConfig.java} | 8 ++-- .../mediatype/ExampleApplication.java | 12 +++++ .../ProductController.java} | 10 ++-- .../WebConfig.java} | 8 ++-- .../pathsegment/ExampleApplication.java | 12 +++++ .../ProductController.java} | 10 ++-- .../WebConfig.java} | 6 +-- .../queryparam/ExampleApplication.java | 12 +++++ .../queryparam/ProductController.java | 46 +++++++++++++++++++ .../WebConfig.java} | 8 ++-- .../src/main/resources/application.properties | 16 ++----- .../ProductControllerLiveTest.java} | 9 +--- .../ProductControllerLiveTest.java} | 14 +----- .../ProductControllerLiveTest.java} | 16 +------ .../ProductControllerLiveTest.java} | 15 +----- 17 files changed, 124 insertions(+), 88 deletions(-) rename spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/{ => header}/ExampleApplication.java (69%) rename spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/{controller => header}/ProductController.java (91%) rename spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/{config/WebHeaderBasedConfig.java => header/WebConfig.java} (65%) create mode 100644 spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/ExampleApplication.java rename spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/{controller/ProductControllerWithCustomMedia.java => mediatype/ProductController.java} (87%) rename spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/{config/WebMediaTypeConfig.java => mediatype/WebConfig.java} (72%) create mode 100644 spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/ExampleApplication.java rename spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/{controller/ProductControllerWithPathSegment.java => pathsegment/ProductController.java} (86%) rename spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/{config/WebPathSegmentConfig.java => pathsegment/WebConfig.java} (72%) create mode 100644 spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ExampleApplication.java create mode 100644 spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ProductController.java rename spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/{config/WebQueryParamConfig.java => queryparam/WebConfig.java} (66%) rename spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/{controller/ProductControllerHeaderLiveTest.java => header/ProductControllerLiveTest.java} (85%) rename spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/{controller/ProductControllerMediaTypeLiveTest.java => mediatype/ProductControllerLiveTest.java} (82%) rename spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/{controller/ProductControllerPathSegmentLiveTest.java => pathsegment/ProductControllerLiveTest.java} (71%) rename spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/{controller/ProductControllerQueryParamLiveTest.java => queryparam/ProductControllerLiveTest.java} (75%) diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/ExampleApplication.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ExampleApplication.java similarity index 69% rename from spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/ExampleApplication.java rename to spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ExampleApplication.java index 74e6ceb79d85..c609bd306cca 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/ExampleApplication.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ExampleApplication.java @@ -1,9 +1,9 @@ -package com.baeldung.apiversions; +package com.baeldung.apiversions.header; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -@SpringBootApplication +@SpringBootApplication(scanBasePackages = "com.baeldung.apiversions.header") public class ExampleApplication { public static void main(String[] args) { diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/controller/ProductController.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ProductController.java similarity index 91% rename from spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/controller/ProductController.java rename to spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ProductController.java index 19132b316b31..292e0ec91314 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/controller/ProductController.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ProductController.java @@ -1,4 +1,4 @@ -package com.baeldung.apiversions.controller; +package com.baeldung.apiversions.header; import java.util.HashMap; import java.util.Map; @@ -25,13 +25,13 @@ public class ProductController { private final Map productsMap = new HashMap<>(); private final Map productsV2Map = new HashMap<>(); - @GetMapping(value = "/{id}", version = "1.0.0") + @GetMapping(value = "/{id}", version = "1.0") public ResponseEntity getProductV1ById(@PathVariable String id) { LOGGER.info("Get Product version 1 for id {}", id); return new ResponseEntity<>(productsMap.get(id), HttpStatus.OK); } - @GetMapping(value = "/{id}", version = "2.0.0") + @GetMapping(value = "/{id}", version = "2.0") public ResponseEntity getProductV2ById(@PathVariable String id) { LOGGER.info("Get Product version 2 for id {}", id); return new ResponseEntity<>(productsV2Map.get(id), HttpStatus.OK); diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebHeaderBasedConfig.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/WebConfig.java similarity index 65% rename from spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebHeaderBasedConfig.java rename to spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/WebConfig.java index bc2548b94831..f29e76439f0e 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebHeaderBasedConfig.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/WebConfig.java @@ -1,16 +1,16 @@ -package com.baeldung.apiversions.config; +package com.baeldung.apiversions.header; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration -public class WebHeaderBasedConfig implements WebMvcConfigurer { +public class WebConfig implements WebMvcConfigurer { @Override public void configureApiVersioning(ApiVersionConfigurer configurer) { - configurer.addSupportedVersions("1.0.0", "2.0.0") - .setDefaultVersion("2.0.0") + configurer.addSupportedVersions("1.0", "2.0") + .setDefaultVersion("2.0") .useRequestHeader("X-API-Version"); } } diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/ExampleApplication.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/ExampleApplication.java new file mode 100644 index 000000000000..77acfb363745 --- /dev/null +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/ExampleApplication.java @@ -0,0 +1,12 @@ +package com.baeldung.apiversions.mediatype; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication(scanBasePackages = "com.baeldung.apiversions.mediatype") +public class ExampleApplication { + + public static void main(String[] args) { + SpringApplication.run(ExampleApplication.class, args); + } +} diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/controller/ProductControllerWithCustomMedia.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/ProductController.java similarity index 87% rename from spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/controller/ProductControllerWithCustomMedia.java rename to spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/ProductController.java index 5e9732172d5d..c6ac5d4abcc5 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/controller/ProductControllerWithCustomMedia.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/ProductController.java @@ -1,4 +1,4 @@ -package com.baeldung.apiversions.controller; +package com.baeldung.apiversions.mediatype; import java.util.HashMap; import java.util.Map; @@ -19,20 +19,20 @@ @RestController @RequestMapping(path = "/api/products") -public class ProductControllerWithCustomMedia { +public class ProductController { - private static final Logger LOGGER = LoggerFactory.getLogger(ProductControllerWithCustomMedia.class); + private static final Logger LOGGER = LoggerFactory.getLogger(ProductController.class); private final Map productsMap = new HashMap<>(); private final Map productsV2Map = new HashMap<>(); - @GetMapping(value = "/{id}", version = "1.0.0", + @GetMapping(value = "/{id}", version = "1.0", produces = "application/vnd.baeldung.product+json") public ResponseEntity getProductByIdCustomMedia(@PathVariable String id) { LOGGER.info("Get Product with custom media version 1 for id {}", id); return new ResponseEntity<>(productsMap.get(id), HttpStatus.OK); } - @GetMapping(value = "/{id}", version = "2.0.0", + @GetMapping(value = "/{id}", version = "2.0", produces = "application/vnd.baeldung.product+json") public ResponseEntity getProductV2ByIdCustomMedia(@PathVariable String id) { LOGGER.info("Get Product with custom media version 2 for id {}", id); diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebMediaTypeConfig.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/WebConfig.java similarity index 72% rename from spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebMediaTypeConfig.java rename to spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/WebConfig.java index e5ec1ff67752..3362f3e8045d 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebMediaTypeConfig.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/WebConfig.java @@ -1,4 +1,4 @@ -package com.baeldung.apiversions.config; +package com.baeldung.apiversions.mediatype; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; @@ -6,13 +6,13 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration -public class WebMediaTypeConfig implements WebMvcConfigurer { +public class WebConfig implements WebMvcConfigurer { @Override public void configureApiVersioning(ApiVersionConfigurer configurer) { configurer - .addSupportedVersions("1.0.0", "2.0.0") - .setDefaultVersion("1.0.0") + .addSupportedVersions("1.0", "2.0") + .setDefaultVersion("1.0") .useMediaTypeParameter(MediaType.parseMediaType("application/vnd.baeldung.product+json"), "version"); } diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/ExampleApplication.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/ExampleApplication.java new file mode 100644 index 000000000000..d3c6fbfe5d16 --- /dev/null +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/ExampleApplication.java @@ -0,0 +1,12 @@ +package com.baeldung.apiversions.pathsegment; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication(scanBasePackages = "com.baeldung.apiversions.pathsegment") +public class ExampleApplication { + + public static void main(String[] args) { + SpringApplication.run(ExampleApplication.class, args); + } +} diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/controller/ProductControllerWithPathSegment.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/ProductController.java similarity index 86% rename from spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/controller/ProductControllerWithPathSegment.java rename to spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/ProductController.java index 308b588873c8..cb92fd7e3822 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/controller/ProductControllerWithPathSegment.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/ProductController.java @@ -1,4 +1,4 @@ -package com.baeldung.apiversions.controller; +package com.baeldung.apiversions.pathsegment; import java.util.HashMap; import java.util.Map; @@ -19,19 +19,19 @@ @RestController @RequestMapping(path = "/api/v{version}/products") -public class ProductControllerWithPathSegment { +public class ProductController { - private static final Logger LOGGER = LoggerFactory.getLogger(ProductControllerWithPathSegment.class); + private static final Logger LOGGER = LoggerFactory.getLogger(ProductController.class); private final Map productsMap = new HashMap<>(); private final Map productsV2Map = new HashMap<>(); - @GetMapping(value = "/{id}", version = "1.0.0") + @GetMapping(value = "/{id}", version = "1.0") public ResponseEntity getProductV1ByIdPath(@PathVariable String id) { LOGGER.info("Get Product with Path specific version 1 for id {}", id); return new ResponseEntity<>(productsMap.get(id), HttpStatus.OK); } - @GetMapping(value = "/{id}", version = "2.0.0") + @GetMapping(value = "/{id}", version = "2.0") public ResponseEntity getProductV2ByIdPath(@PathVariable String id) { LOGGER.info("Get Product with Path specific version 2 for id {}", id); return new ResponseEntity<>(productsV2Map.get(id), HttpStatus.OK); diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebPathSegmentConfig.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/WebConfig.java similarity index 72% rename from spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebPathSegmentConfig.java rename to spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/WebConfig.java index 321a3666d13c..c11269757552 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebPathSegmentConfig.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/WebConfig.java @@ -1,17 +1,17 @@ -package com.baeldung.apiversions.config; +package com.baeldung.apiversions.pathsegment; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration -public class WebPathSegmentConfig implements WebMvcConfigurer { +public class WebConfig implements WebMvcConfigurer { @Override public void configureApiVersioning(ApiVersionConfigurer configurer) { configurer .usePathSegment(1) .setDefaultVersion(null) - .addSupportedVersions("1.0.0", "2.0.0"); + .addSupportedVersions("1.0", "2.0"); } } diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ExampleApplication.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ExampleApplication.java new file mode 100644 index 000000000000..3cbaf318e73b --- /dev/null +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ExampleApplication.java @@ -0,0 +1,12 @@ +package com.baeldung.apiversions.queryparam; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication(scanBasePackages = "com.baeldung.apiversions.queryparam") +public class ExampleApplication { + + public static void main(String[] args) { + SpringApplication.run(ExampleApplication.class, args); + } +} diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ProductController.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ProductController.java new file mode 100644 index 000000000000..d44f9fe11471 --- /dev/null +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ProductController.java @@ -0,0 +1,46 @@ +package com.baeldung.apiversions.queryparam; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.annotation.PostConstruct; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.baeldung.apiversions.model.Product; +import com.baeldung.apiversions.model.ProductV2; + +@RestController +@RequestMapping(path = "/api/products") +public class ProductController { + + private static final Logger LOGGER = LoggerFactory.getLogger(ProductController.class); + private final Map productsMap = new HashMap<>(); + private final Map productsV2Map = new HashMap<>(); + + @GetMapping(value = "/{id}", version = "1.0") + public ResponseEntity getProductV1ByIdPath(@PathVariable String id) { + LOGGER.info("Get Product version 1 for id {}", id); + return new ResponseEntity<>(productsMap.get(id), HttpStatus.OK); + } + + @GetMapping(value = "/{id}", version = "2.0") + public ResponseEntity getProductV2ByIdPath(@PathVariable String id) { + LOGGER.info("Get Product version 2 for id {}", id); + return new ResponseEntity<>(productsV2Map.get(id), HttpStatus.OK); + } + + @PostConstruct + public void init(){ + productsMap.put("1001", new Product("1001", "apple", + "apple_long_desc", 1.99)); + productsV2Map.put("1001", new ProductV2("1001", "apple", 1.99)); + } +} diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebQueryParamConfig.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/WebConfig.java similarity index 66% rename from spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebQueryParamConfig.java rename to spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/WebConfig.java index abc3d966ac97..37171f1fa6d1 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/config/WebQueryParamConfig.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/WebConfig.java @@ -1,17 +1,17 @@ -package com.baeldung.apiversions.config; +package com.baeldung.apiversions.queryparam; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration -public class WebQueryParamConfig implements WebMvcConfigurer { +public class WebConfig implements WebMvcConfigurer { @Override public void configureApiVersioning(ApiVersionConfigurer configurer) { configurer - .addSupportedVersions("1.0.0", "2.0.0") - .setDefaultVersion("1.0.0") + .addSupportedVersions("1.0", "2.0") + .setDefaultVersion("1.0") .useQueryParam("version"); } } diff --git a/spring-boot-modules/spring-boot-5/src/main/resources/application.properties b/spring-boot-modules/spring-boot-5/src/main/resources/application.properties index 2e679c331207..dd07accecf1f 100644 --- a/spring-boot-modules/spring-boot-5/src/main/resources/application.properties +++ b/spring-boot-modules/spring-boot-5/src/main/resources/application.properties @@ -1,18 +1,10 @@ spring.application.name=demo -server.port=8081 -#spring.mvc.apiversion.supported=1.0.0,2.0.0 -#spring.mvc.apiversion.default=1.0.0 -#spring.mvc.apiversion.enabled=true +spring.mvc.apiversion.supported=1.0,2.0 +spring.mvc.apiversion.default=1.0 +spring.mvc.apiversion.enabled=true -#spring.mvc.apiversion.strategy=HEADER -#spring.mvc.apiversion.use.header=X-API-VERSION - -#spring.mvc.apiversion.strategy=QUERY +#spring.mvc.apiversion.use.header=X-API-Version #spring.mvc.apiversion.use.query-parameter=version - -#spring.mvc.apiversion.strategy=MEDIA_TYPE #spring.mvc.apiversion.use.media-type-parameter[application/vnd.baeldung.product+json]=version - -#spring.mvc.apiversion.strategy=PATH #spring.mvc.apiversion.use.path-segment=1 diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerHeaderLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java similarity index 85% rename from spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerHeaderLiveTest.java rename to spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java index 5dbb184e4575..9bf3c12b3813 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerHeaderLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java @@ -1,18 +1,13 @@ -package com.baeldung.apiversions.controller; +package com.baeldung.apiversions.header; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ContextConfiguration; import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.web.context.WebApplicationContext; -import com.baeldung.apiversions.ExampleApplication; -import com.baeldung.apiversions.config.WebHeaderBasedConfig; - @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = ExampleApplication.class) -@ContextConfiguration(classes = { ProductController.class, WebHeaderBasedConfig.class, }) -class ProductControllerHeaderLiveTest { +class ProductControllerLiveTest { private RestTestClient restTestClient; diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerMediaTypeLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java similarity index 82% rename from spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerMediaTypeLiveTest.java rename to spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java index 8f75759501d4..8dbfd7cb8dba 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerMediaTypeLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java @@ -1,24 +1,14 @@ -package com.baeldung.apiversions.controller; +package com.baeldung.apiversions.mediatype; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; -import org.springframework.test.context.ContextConfiguration; import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.web.context.WebApplicationContext; -import com.baeldung.apiversions.ExampleApplication; -import com.baeldung.apiversions.config.WebHeaderBasedConfig; -import com.baeldung.apiversions.config.WebMediaTypeConfig; -import com.baeldung.apiversions.config.WebPathSegmentConfig; -import com.baeldung.apiversions.config.WebQueryParamConfig; - @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = ExampleApplication.class) -@ContextConfiguration(classes = { ProductControllerWithCustomMedia.class, WebMediaTypeConfig.class }) -class ProductControllerMediaTypeLiveTest { +class ProductControllerLiveTest { private RestTestClient restTestClient; diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerPathSegmentLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java similarity index 71% rename from spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerPathSegmentLiveTest.java rename to spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java index bf7cf080e55e..5d9482990a9e 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerPathSegmentLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java @@ -1,25 +1,13 @@ -package com.baeldung.apiversions.controller; +package com.baeldung.apiversions.pathsegment; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.context.ContextConfiguration; import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.web.context.WebApplicationContext; -import com.baeldung.apiversions.ExampleApplication; -import com.baeldung.apiversions.config.WebHeaderBasedConfig; -import com.baeldung.apiversions.config.WebMediaTypeConfig; -import com.baeldung.apiversions.config.WebPathSegmentConfig; -import com.baeldung.apiversions.config.WebQueryParamConfig; - @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = ExampleApplication.class) -@ContextConfiguration(classes = { ProductControllerWithPathSegment.class, WebPathSegmentConfig.class }) -@DisplayName("All Get Products API Versions Tests") -class ProductControllerPathSegmentLiveTest { +class ProductControllerLiveTest { private RestTestClient restTestClient; diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerQueryParamLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java similarity index 75% rename from spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerQueryParamLiveTest.java rename to spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java index b90b02ef666f..bdbb853dea9a 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/controller/ProductControllerQueryParamLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java @@ -1,24 +1,13 @@ -package com.baeldung.apiversions.controller; +package com.baeldung.apiversions.queryparam; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.context.ContextConfiguration; import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.web.context.WebApplicationContext; -import com.baeldung.apiversions.ExampleApplication; -import com.baeldung.apiversions.config.WebHeaderBasedConfig; -import com.baeldung.apiversions.config.WebMediaTypeConfig; -import com.baeldung.apiversions.config.WebPathSegmentConfig; -import com.baeldung.apiversions.config.WebQueryParamConfig; - @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = ExampleApplication.class) -@ContextConfiguration(classes = { ProductController.class, WebQueryParamConfig.class }) -class ProductControllerQueryParamLiveTest { +class ProductControllerLiveTest { private RestTestClient restTestClient; From 2e69dbbb70d58a7183c01708f3753c07c700cb55 Mon Sep 17 00:00:00 2001 From: saikat Date: Sun, 15 Feb 2026 14:24:51 +0530 Subject: [PATCH 16/38] update pom file --- spring-boot-modules/spring-boot-5/pom.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spring-boot-modules/spring-boot-5/pom.xml b/spring-boot-modules/spring-boot-5/pom.xml index d42bf0bd44eb..791c4fd86743 100644 --- a/spring-boot-modules/spring-boot-5/pom.xml +++ b/spring-boot-modules/spring-boot-5/pom.xml @@ -36,6 +36,9 @@ org.springframework.boot spring-boot-maven-plugin + + com.baeldung.apiversions.header.ExampleApplication + org.apache.maven.plugins @@ -58,4 +61,4 @@ 6.0.0 1.5.18 - + \ No newline at end of file From bbde43faad634e3818198f448abb8c44ec7d9d4c Mon Sep 17 00:00:00 2001 From: saikat Date: Sun, 15 Feb 2026 14:31:14 +0530 Subject: [PATCH 17/38] update pom file --- .../baeldung/apiversions/header/ProductController.java | 10 ++++------ .../apiversions/mediatype/ProductController.java | 8 ++++---- .../apiversions/pathsegment/ProductController.java | 4 ++-- .../apiversions/queryparam/ProductController.java | 10 ++++------ .../src/main/resources/application.properties | 10 ---------- 5 files changed, 14 insertions(+), 28 deletions(-) delete mode 100644 spring-boot-modules/spring-boot-5/src/main/resources/application.properties diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ProductController.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ProductController.java index 292e0ec91314..490d8fb65dbc 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ProductController.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ProductController.java @@ -7,8 +7,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -26,15 +24,15 @@ public class ProductController { private final Map productsV2Map = new HashMap<>(); @GetMapping(value = "/{id}", version = "1.0") - public ResponseEntity getProductV1ById(@PathVariable String id) { + public Product getProductV1ById(@PathVariable String id) { LOGGER.info("Get Product version 1 for id {}", id); - return new ResponseEntity<>(productsMap.get(id), HttpStatus.OK); + return productsMap.get(id); } @GetMapping(value = "/{id}", version = "2.0") - public ResponseEntity getProductV2ById(@PathVariable String id) { + public ProductV2 getProductV2ById(@PathVariable String id) { LOGGER.info("Get Product version 2 for id {}", id); - return new ResponseEntity<>(productsV2Map.get(id), HttpStatus.OK); + return productsV2Map.get(id); } @PostConstruct diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/ProductController.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/ProductController.java index c6ac5d4abcc5..fd43855fe3e3 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/ProductController.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/ProductController.java @@ -27,16 +27,16 @@ public class ProductController { @GetMapping(value = "/{id}", version = "1.0", produces = "application/vnd.baeldung.product+json") - public ResponseEntity getProductByIdCustomMedia(@PathVariable String id) { + public Product getProductByIdCustomMedia(@PathVariable String id) { LOGGER.info("Get Product with custom media version 1 for id {}", id); - return new ResponseEntity<>(productsMap.get(id), HttpStatus.OK); + return productsMap.get(id); } @GetMapping(value = "/{id}", version = "2.0", produces = "application/vnd.baeldung.product+json") - public ResponseEntity getProductV2ByIdCustomMedia(@PathVariable String id) { + public ProductV2 getProductV2ByIdCustomMedia(@PathVariable String id) { LOGGER.info("Get Product with custom media version 2 for id {}", id); - return new ResponseEntity<>(productsV2Map.get(id), HttpStatus.OK); + return productsV2Map.get(id); } @PostConstruct diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/ProductController.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/ProductController.java index cb92fd7e3822..2dd4d2febfb5 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/ProductController.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/ProductController.java @@ -26,9 +26,9 @@ public class ProductController { private final Map productsV2Map = new HashMap<>(); @GetMapping(value = "/{id}", version = "1.0") - public ResponseEntity getProductV1ByIdPath(@PathVariable String id) { + public Product getProductV1ByIdPath(@PathVariable String id) { LOGGER.info("Get Product with Path specific version 1 for id {}", id); - return new ResponseEntity<>(productsMap.get(id), HttpStatus.OK); + return productsMap.get(id); } @GetMapping(value = "/{id}", version = "2.0") diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ProductController.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ProductController.java index d44f9fe11471..77628c136d57 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ProductController.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ProductController.java @@ -7,8 +7,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -26,15 +24,15 @@ public class ProductController { private final Map productsV2Map = new HashMap<>(); @GetMapping(value = "/{id}", version = "1.0") - public ResponseEntity getProductV1ByIdPath(@PathVariable String id) { + public Product getProductV1ByIdPath(@PathVariable String id) { LOGGER.info("Get Product version 1 for id {}", id); - return new ResponseEntity<>(productsMap.get(id), HttpStatus.OK); + return productsMap.get(id); } @GetMapping(value = "/{id}", version = "2.0") - public ResponseEntity getProductV2ByIdPath(@PathVariable String id) { + public ProductV2 getProductV2ByIdPath(@PathVariable String id) { LOGGER.info("Get Product version 2 for id {}", id); - return new ResponseEntity<>(productsV2Map.get(id), HttpStatus.OK); + return productsV2Map.get(id); } @PostConstruct diff --git a/spring-boot-modules/spring-boot-5/src/main/resources/application.properties b/spring-boot-modules/spring-boot-5/src/main/resources/application.properties deleted file mode 100644 index dd07accecf1f..000000000000 --- a/spring-boot-modules/spring-boot-5/src/main/resources/application.properties +++ /dev/null @@ -1,10 +0,0 @@ -spring.application.name=demo - -spring.mvc.apiversion.supported=1.0,2.0 -spring.mvc.apiversion.default=1.0 -spring.mvc.apiversion.enabled=true - -#spring.mvc.apiversion.use.header=X-API-Version -#spring.mvc.apiversion.use.query-parameter=version -#spring.mvc.apiversion.use.media-type-parameter[application/vnd.baeldung.product+json]=version -#spring.mvc.apiversion.use.path-segment=1 From b75584061e8e0a586b67208979056f2fd4844222 Mon Sep 17 00:00:00 2001 From: saikat Date: Sun, 15 Feb 2026 14:31:35 +0530 Subject: [PATCH 18/38] resources file --- .../src/test/resources/application.properties | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 spring-boot-modules/spring-boot-5/src/test/resources/application.properties diff --git a/spring-boot-modules/spring-boot-5/src/test/resources/application.properties b/spring-boot-modules/spring-boot-5/src/test/resources/application.properties new file mode 100644 index 000000000000..232bae01782e --- /dev/null +++ b/spring-boot-modules/spring-boot-5/src/test/resources/application.properties @@ -0,0 +1,10 @@ +spring.application.name=example + +#spring.mvc.apiversion.supported=1.0,2.0 +#spring.mvc.apiversion.default=1.0 +#spring.mvc.apiversion.enabled=true + +#spring.mvc.apiversion.use.header=X-API-Version +#spring.mvc.apiversion.use.query-parameter=version +#spring.mvc.apiversion.use.media-type-parameter[application/vnd.baeldung.product+json]=version +#spring.mvc.apiversion.use.path-segment=1 From 79e967d5e639c5e0c2dd6a19dbaaefe4e2a92015 Mon Sep 17 00:00:00 2001 From: saikat Date: Sun, 15 Feb 2026 14:45:41 +0530 Subject: [PATCH 19/38] refactor code --- .../apiversions/header/ProductController.java | 2 +- .../mediatype/ProductController.java | 2 +- .../pathsegment/ProductController.java | 2 +- .../queryparam/ProductController.java | 2 +- .../header/ProductControllerLiveTest.java | 33 ++++++---------- .../mediatype/ProductControllerLiveTest.java | 38 +++++++------------ .../ProductControllerLiveTest.java | 18 +++------ .../queryparam/ProductControllerLiveTest.java | 29 +++++--------- 8 files changed, 45 insertions(+), 81 deletions(-) diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ProductController.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ProductController.java index 490d8fb65dbc..1940bda0088f 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ProductController.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ProductController.java @@ -38,7 +38,7 @@ public ProductV2 getProductV2ById(@PathVariable String id) { @PostConstruct public void init(){ productsMap.put("1001", new Product("1001", "apple", - "apple_long_desc", 1.99)); + "apple_desc", 1.99)); productsV2Map.put("1001", new ProductV2("1001", "apple", 1.99)); } } diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/ProductController.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/ProductController.java index fd43855fe3e3..56e6c69b44f3 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/ProductController.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/ProductController.java @@ -42,7 +42,7 @@ public ProductV2 getProductV2ByIdCustomMedia(@PathVariable String id) { @PostConstruct public void init(){ productsMap.put("1001", new Product("1001", "apple", - "apple_long_desc", 1.99)); + "apple_desc", 1.99)); productsV2Map.put("1001", new ProductV2("1001", "apple", 1.99)); } } diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/ProductController.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/ProductController.java index 2dd4d2febfb5..fbb7ad47774a 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/ProductController.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/ProductController.java @@ -40,7 +40,7 @@ public ResponseEntity getProductV2ByIdPath(@PathVariable String id) { @PostConstruct public void init(){ productsMap.put("1001", new Product("1001", "apple", - "apple_long_desc", 1.99)); + "apple_desc", 1.99)); productsV2Map.put("1001", new ProductV2("1001", "apple", 1.99)); } } diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ProductController.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ProductController.java index 77628c136d57..da7a40c5c2f4 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ProductController.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ProductController.java @@ -38,7 +38,7 @@ public ProductV2 getProductV2ByIdPath(@PathVariable String id) { @PostConstruct public void init(){ productsMap.put("1001", new Product("1001", "apple", - "apple_long_desc", 1.99)); + "apple_desc", 1.99)); productsV2Map.put("1001", new ProductV2("1001", "apple", 1.99)); } } diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java index 9bf3c12b3813..de553008322e 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java @@ -23,15 +23,11 @@ void givenProductExists_WhenProductAPIIsCalled_WithHeaderVersion1_thenReturnVali .uri("/api/products/1001") .header("X-API-Version", "1") .exchange() - .expectStatus() - .isOk() + .expectStatus().isOk() .expectBody() - .jsonPath("$.name") - .isEqualTo("apple") - .jsonPath("$.desc") - .isEqualTo("apple_long_desc") - .jsonPath("$.price") - .isEqualTo(1.99); + .jsonPath("$.name").isEqualTo("apple") + .jsonPath("$.desc").isEqualTo("apple_desc") + .jsonPath("$.price").isEqualTo(1.99); } @Test @@ -40,15 +36,11 @@ void givenProductExists_WhenProductAPIIsCalled_WithHeaderVersion2_thenReturnVali .uri("/api/products/1001") .header("X-API-Version", "2") .exchange() - .expectStatus() - .isOk() + .expectStatus().isOk() .expectBody() - .jsonPath("$.name") - .isEqualTo("apple") - .jsonPath("$.desc") - .doesNotExist() - .jsonPath("$.price") - .isEqualTo(1.99); + .jsonPath("$.name").isEqualTo("apple") + .jsonPath("$.desc").doesNotExist() + .jsonPath("$.price").isEqualTo(1.99); } @Test @@ -60,11 +52,8 @@ void givenProductExists_WhenProductAPIIsCalled_WithInvalidHeaderVersion_thenRetu .expectStatus() .is4xxClientError() .expectBody() - .jsonPath("$.name") - .doesNotExist() - .jsonPath("$.desc") - .doesNotExist() - .jsonPath("$.price") - .doesNotExist(); + .jsonPath("$.name").doesNotExist() + .jsonPath("$.desc").doesNotExist() + .jsonPath("$.price").doesNotExist(); } } diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java index 8dbfd7cb8dba..5383b85133eb 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java @@ -26,13 +26,11 @@ void givenProductExists_WhenProductAPIIsCalled_WithValidMediaTypeVersion_thenRet .exchange() .expectStatus() .isOk() + .expectHeader().contentType("application/vnd.baeldung.product+json;version=1") .expectBody() - .jsonPath("$.name") - .isEqualTo("apple") - .jsonPath("$.desc") - .isEqualTo("apple_long_desc") - .jsonPath("$.price") - .isEqualTo(1.99); + .jsonPath("$.name").isEqualTo("apple") + .jsonPath("$.desc").isEqualTo("apple_desc") + .jsonPath("$.price").isEqualTo(1.99); } @Test @@ -43,13 +41,11 @@ void givenProductExists_WhenProductAPIIsCalled_WithValidMediaType_thenReturnVali .exchange() .expectStatus() .isOk() + .expectHeader().contentType("application/vnd.baeldung.product+json;version=2") .expectBody() - .jsonPath("$.name") - .isEqualTo("apple") - .jsonPath("$.desc") - .doesNotExist() - .jsonPath("$.price") - .isEqualTo(1.99); + .jsonPath("$.name").isEqualTo("apple") + .jsonPath("$.desc").doesNotExist() + .jsonPath("$.price").isEqualTo(1.99); } @Test @@ -61,12 +57,9 @@ void givenProductExists_WhenProductAPIIsCalled_WithInValidMediaTypeVersion_thenR .expectStatus() .is4xxClientError() .expectBody() - .jsonPath("$.name") - .doesNotExist() - .jsonPath("$.desc") - .doesNotExist() - .jsonPath("$.price") - .doesNotExist(); + .jsonPath("$.name").doesNotExist() + .jsonPath("$.desc").doesNotExist() + .jsonPath("$.price").doesNotExist(); } @Test @@ -78,11 +71,8 @@ void givenProductExists_WhenProductAPIIsCalled_WithInValidMediaType_thenReturnBa .expectStatus() .is4xxClientError() .expectBody() - .jsonPath("$.name") - .doesNotExist() - .jsonPath("$.desc") - .doesNotExist() - .jsonPath("$.price") - .doesNotExist(); + .jsonPath("$.name").doesNotExist() + .jsonPath("$.desc").doesNotExist() + .jsonPath("$.price").doesNotExist(); } } diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java index 5d9482990a9e..112f6ca75248 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java @@ -25,12 +25,9 @@ void givenProductExists_WhenGetProductAPIIsCalled_WithPathSegmentV1_thenReturnVa .expectStatus() .isOk() .expectBody() - .jsonPath("$.name") - .isEqualTo("apple") - .jsonPath("$.desc") - .isEqualTo("apple_long_desc") - .jsonPath("$.price") - .isEqualTo(1.99); + .jsonPath("$.name").isEqualTo("apple") + .jsonPath("$.desc").isEqualTo("apple_desc") + .jsonPath("$.price").isEqualTo(1.99); } @Test @@ -41,12 +38,9 @@ void givenProductExists_WhenProductAPIIsCalled_WithPathSegmentV2_thenReturnValid .expectStatus() .isOk() .expectBody() - .jsonPath("$.name") - .isEqualTo("apple") - .jsonPath("$.desc") - .doesNotExist() - .jsonPath("$.price") - .isEqualTo(1.99); + .jsonPath("$.name").isEqualTo("apple") + .jsonPath("$.desc").doesNotExist() + .jsonPath("$.price").isEqualTo(1.99); } @Test diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java index bdbb853dea9a..59fd15f955d9 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java @@ -25,28 +25,22 @@ void givenProductExists_WhenProductAPIIsCalled_WithQueryParamVersion1_thenReturn .expectStatus() .isOk() .expectBody() - .jsonPath("$.name") - .isEqualTo("apple") - .jsonPath("$.desc") - .isEqualTo("apple_long_desc") - .jsonPath("$.price") - .isEqualTo(1.99); + .jsonPath("$.name").isEqualTo("apple") + .jsonPath("$.desc").isEqualTo("apple_desc") + .jsonPath("$.price").isEqualTo(1.99); } @Test - void givenProductExists_WhenProductAPIIsCalled_WithQueryParamVersion2_thenReturnValidProduct() { + void givenProductExists_WhenProductAPIIsCalled_WithQueryParamVersion2_thenReturnValidProductV2() { restTestClient.get() .uri("/api/products/1001?version=2") .exchange() .expectStatus() .isOk() .expectBody() - .jsonPath("$.name") - .isEqualTo("apple") - .jsonPath("$.desc") - .doesNotExist() - .jsonPath("$.price") - .isEqualTo(1.99); + .jsonPath("$.name").isEqualTo("apple") + .jsonPath("$.desc").doesNotExist() + .jsonPath("$.price").isEqualTo(1.99); } @Test @@ -57,11 +51,8 @@ void givenProductExists_WhenProductAPIIsCalled_WithInvalidQueryParam_thenReturnB .expectStatus() .is4xxClientError() .expectBody() - .jsonPath("$.name") - .doesNotExist() - .jsonPath("$.desc") - .doesNotExist() - .jsonPath("$.price") - .doesNotExist(); + .jsonPath("$.name").doesNotExist() + .jsonPath("$.desc").doesNotExist() + .jsonPath("$.price").doesNotExist(); } } From 54db6b6574f11719f6624a7bfad1eaee5383f05e Mon Sep 17 00:00:00 2001 From: saikat Date: Sun, 15 Feb 2026 18:03:15 +0530 Subject: [PATCH 20/38] refactor code --- .../main/java/com/baeldung/apiversions/header/WebConfig.java | 2 +- .../baeldung/apiversions/header/ProductControllerLiveTest.java | 2 +- .../apiversions/mediatype/ProductControllerLiveTest.java | 2 +- .../apiversions/pathsegment/ProductControllerLiveTest.java | 2 +- .../apiversions/queryparam/ProductControllerLiveTest.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/WebConfig.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/WebConfig.java index f29e76439f0e..4dfa0dc4b434 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/WebConfig.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/WebConfig.java @@ -10,7 +10,7 @@ public class WebConfig implements WebMvcConfigurer { @Override public void configureApiVersioning(ApiVersionConfigurer configurer) { configurer.addSupportedVersions("1.0", "2.0") - .setDefaultVersion("2.0") + .setDefaultVersion("1.0") .useRequestHeader("X-API-Version"); } } diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java index de553008322e..4bd73345de0c 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java @@ -6,7 +6,7 @@ import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.web.context.WebApplicationContext; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = ExampleApplication.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class ProductControllerLiveTest { private RestTestClient restTestClient; diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java index 5383b85133eb..2a5803d714a4 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java @@ -7,7 +7,7 @@ import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.web.context.WebApplicationContext; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = ExampleApplication.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class ProductControllerLiveTest { private RestTestClient restTestClient; diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java index 112f6ca75248..48ccdc986791 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java @@ -6,7 +6,7 @@ import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.web.context.WebApplicationContext; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = ExampleApplication.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class ProductControllerLiveTest { private RestTestClient restTestClient; diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java index 59fd15f955d9..cade885608ef 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java @@ -6,7 +6,7 @@ import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.web.context.WebApplicationContext; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = ExampleApplication.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class ProductControllerLiveTest { private RestTestClient restTestClient; From a59ecab9614daa943f6669851d1a10a630b4fc5f Mon Sep 17 00:00:00 2001 From: saikat Date: Sun, 15 Feb 2026 22:50:09 +0530 Subject: [PATCH 21/38] refactor code --- .../apiversions/header/ProductController.java | 16 ++++++++-------- .../mediatype/ProductController.java | 18 ++++++++---------- .../baeldung/apiversions/model/Product.java | 5 ----- .../baeldung/apiversions/model/ProductDto.java | 5 +++++ .../apiversions/model/ProductDtoV2.java | 5 +++++ .../baeldung/apiversions/model/ProductV2.java | 5 ----- .../pathsegment/ProductController.java | 16 ++++++++-------- .../queryparam/ProductController.java | 16 ++++++++-------- 8 files changed, 42 insertions(+), 44 deletions(-) delete mode 100644 spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/Product.java create mode 100644 spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/ProductDto.java create mode 100644 spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/ProductDtoV2.java delete mode 100644 spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/ProductV2.java diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ProductController.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ProductController.java index 1940bda0088f..25e1bf065f87 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ProductController.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ProductController.java @@ -12,33 +12,33 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import com.baeldung.apiversions.model.Product; -import com.baeldung.apiversions.model.ProductV2; +import com.baeldung.apiversions.model.ProductDto; +import com.baeldung.apiversions.model.ProductDtoV2; @RestController @RequestMapping(path = "/api/products") public class ProductController { private static final Logger LOGGER = LoggerFactory.getLogger(ProductController.class); - private final Map productsMap = new HashMap<>(); - private final Map productsV2Map = new HashMap<>(); + private final Map productsMap = new HashMap<>(); + private final Map productsV2Map = new HashMap<>(); @GetMapping(value = "/{id}", version = "1.0") - public Product getProductV1ById(@PathVariable String id) { + public ProductDto getProductV1ById(@PathVariable String id) { LOGGER.info("Get Product version 1 for id {}", id); return productsMap.get(id); } @GetMapping(value = "/{id}", version = "2.0") - public ProductV2 getProductV2ById(@PathVariable String id) { + public ProductDtoV2 getProductV2ById(@PathVariable String id) { LOGGER.info("Get Product version 2 for id {}", id); return productsV2Map.get(id); } @PostConstruct public void init(){ - productsMap.put("1001", new Product("1001", "apple", + productsMap.put("1001", new ProductDto("1001", "apple", "apple_desc", 1.99)); - productsV2Map.put("1001", new ProductV2("1001", "apple", 1.99)); + productsV2Map.put("1001", new ProductDtoV2("1001", "apple", 1.99)); } } diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/ProductController.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/ProductController.java index 56e6c69b44f3..27800205a3ef 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/ProductController.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/ProductController.java @@ -7,42 +7,40 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import com.baeldung.apiversions.model.Product; -import com.baeldung.apiversions.model.ProductV2; +import com.baeldung.apiversions.model.ProductDto; +import com.baeldung.apiversions.model.ProductDtoV2; @RestController @RequestMapping(path = "/api/products") public class ProductController { private static final Logger LOGGER = LoggerFactory.getLogger(ProductController.class); - private final Map productsMap = new HashMap<>(); - private final Map productsV2Map = new HashMap<>(); + private final Map productsMap = new HashMap<>(); + private final Map productsV2Map = new HashMap<>(); @GetMapping(value = "/{id}", version = "1.0", produces = "application/vnd.baeldung.product+json") - public Product getProductByIdCustomMedia(@PathVariable String id) { + public ProductDto getProductByIdCustomMedia(@PathVariable String id) { LOGGER.info("Get Product with custom media version 1 for id {}", id); return productsMap.get(id); } @GetMapping(value = "/{id}", version = "2.0", produces = "application/vnd.baeldung.product+json") - public ProductV2 getProductV2ByIdCustomMedia(@PathVariable String id) { + public ProductDtoV2 getProductV2ByIdCustomMedia(@PathVariable String id) { LOGGER.info("Get Product with custom media version 2 for id {}", id); return productsV2Map.get(id); } @PostConstruct public void init(){ - productsMap.put("1001", new Product("1001", "apple", + productsMap.put("1001", new ProductDto("1001", "apple", "apple_desc", 1.99)); - productsV2Map.put("1001", new ProductV2("1001", "apple", 1.99)); + productsV2Map.put("1001", new ProductDtoV2("1001", "apple", 1.99)); } } diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/Product.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/Product.java deleted file mode 100644 index af70113e67fb..000000000000 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/Product.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.baeldung.apiversions.model; - -public record Product(String id, String name, String desc, double price) { - -} diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/ProductDto.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/ProductDto.java new file mode 100644 index 000000000000..d7586ee62a48 --- /dev/null +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/ProductDto.java @@ -0,0 +1,5 @@ +package com.baeldung.apiversions.model; + +public record ProductDto(String id, String name, String desc, double price) { + +} diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/ProductDtoV2.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/ProductDtoV2.java new file mode 100644 index 000000000000..66dc93b6939a --- /dev/null +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/ProductDtoV2.java @@ -0,0 +1,5 @@ +package com.baeldung.apiversions.model; + +public record ProductDtoV2(String id, String name, double price) { + +} diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/ProductV2.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/ProductV2.java deleted file mode 100644 index 6de10144022e..000000000000 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/model/ProductV2.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.baeldung.apiversions.model; - -public record ProductV2(String id, String name, double price) { - -} diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/ProductController.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/ProductController.java index fbb7ad47774a..6b6a250785d4 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/ProductController.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/ProductController.java @@ -14,33 +14,33 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import com.baeldung.apiversions.model.Product; -import com.baeldung.apiversions.model.ProductV2; +import com.baeldung.apiversions.model.ProductDto; +import com.baeldung.apiversions.model.ProductDtoV2; @RestController @RequestMapping(path = "/api/v{version}/products") public class ProductController { private static final Logger LOGGER = LoggerFactory.getLogger(ProductController.class); - private final Map productsMap = new HashMap<>(); - private final Map productsV2Map = new HashMap<>(); + private final Map productsMap = new HashMap<>(); + private final Map productsV2Map = new HashMap<>(); @GetMapping(value = "/{id}", version = "1.0") - public Product getProductV1ByIdPath(@PathVariable String id) { + public ProductDto getProductV1ByIdPath(@PathVariable String id) { LOGGER.info("Get Product with Path specific version 1 for id {}", id); return productsMap.get(id); } @GetMapping(value = "/{id}", version = "2.0") - public ResponseEntity getProductV2ByIdPath(@PathVariable String id) { + public ResponseEntity getProductV2ByIdPath(@PathVariable String id) { LOGGER.info("Get Product with Path specific version 2 for id {}", id); return new ResponseEntity<>(productsV2Map.get(id), HttpStatus.OK); } @PostConstruct public void init(){ - productsMap.put("1001", new Product("1001", "apple", + productsMap.put("1001", new ProductDto("1001", "apple", "apple_desc", 1.99)); - productsV2Map.put("1001", new ProductV2("1001", "apple", 1.99)); + productsV2Map.put("1001", new ProductDtoV2("1001", "apple", 1.99)); } } diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ProductController.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ProductController.java index da7a40c5c2f4..4e7fce4827c0 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ProductController.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ProductController.java @@ -12,33 +12,33 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import com.baeldung.apiversions.model.Product; -import com.baeldung.apiversions.model.ProductV2; +import com.baeldung.apiversions.model.ProductDto; +import com.baeldung.apiversions.model.ProductDtoV2; @RestController @RequestMapping(path = "/api/products") public class ProductController { private static final Logger LOGGER = LoggerFactory.getLogger(ProductController.class); - private final Map productsMap = new HashMap<>(); - private final Map productsV2Map = new HashMap<>(); + private final Map productsMap = new HashMap<>(); + private final Map productsV2Map = new HashMap<>(); @GetMapping(value = "/{id}", version = "1.0") - public Product getProductV1ByIdPath(@PathVariable String id) { + public ProductDto getProductV1ByIdPath(@PathVariable String id) { LOGGER.info("Get Product version 1 for id {}", id); return productsMap.get(id); } @GetMapping(value = "/{id}", version = "2.0") - public ProductV2 getProductV2ByIdPath(@PathVariable String id) { + public ProductDtoV2 getProductV2ByIdPath(@PathVariable String id) { LOGGER.info("Get Product version 2 for id {}", id); return productsV2Map.get(id); } @PostConstruct public void init(){ - productsMap.put("1001", new Product("1001", "apple", + productsMap.put("1001", new ProductDto("1001", "apple", "apple_desc", 1.99)); - productsV2Map.put("1001", new ProductV2("1001", "apple", 1.99)); + productsV2Map.put("1001", new ProductDtoV2("1001", "apple", 1.99)); } } From e84baaa09c1d40b01e5fbbe320489c1e962910bd Mon Sep 17 00:00:00 2001 From: saikat Date: Mon, 16 Feb 2026 11:03:33 +0530 Subject: [PATCH 22/38] refactor code --- .../apiversions/header/ProductController.java | 22 +++++++++---------- .../mediatype/ProductController.java | 16 ++++---------- .../pathsegment/ProductController.java | 16 ++++---------- .../queryparam/ProductController.java | 15 ++++--------- 4 files changed, 22 insertions(+), 47 deletions(-) diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ProductController.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ProductController.java index 25e1bf065f87..6179639a159f 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ProductController.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ProductController.java @@ -1,10 +1,7 @@ package com.baeldung.apiversions.header; -import java.util.HashMap; import java.util.Map; -import jakarta.annotation.PostConstruct; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; @@ -20,8 +17,10 @@ public class ProductController { private static final Logger LOGGER = LoggerFactory.getLogger(ProductController.class); - private final Map productsMap = new HashMap<>(); - private final Map productsV2Map = new HashMap<>(); + private final Map productsMap = + Map.of("1001", new ProductDto("1001", "apple", "apple_desc", 1.99)); + private final Map productsV2Map = + Map.of("1001", new ProductDtoV2("1001", "apple", 1.99)); @GetMapping(value = "/{id}", version = "1.0") public ProductDto getProductV1ById(@PathVariable String id) { @@ -29,16 +28,15 @@ public ProductDto getProductV1ById(@PathVariable String id) { return productsMap.get(id); } + @GetMapping(value = "/{id}", version = "1.1+") + public ProductDto getProductV1ById11(@PathVariable String id) { + LOGGER.info("Get Product version 1.1 for id {}", id); + return productsMap.get(id); + } + @GetMapping(value = "/{id}", version = "2.0") public ProductDtoV2 getProductV2ById(@PathVariable String id) { LOGGER.info("Get Product version 2 for id {}", id); return productsV2Map.get(id); } - - @PostConstruct - public void init(){ - productsMap.put("1001", new ProductDto("1001", "apple", - "apple_desc", 1.99)); - productsV2Map.put("1001", new ProductDtoV2("1001", "apple", 1.99)); - } } diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/ProductController.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/ProductController.java index 27800205a3ef..45ce46c1661d 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/ProductController.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/mediatype/ProductController.java @@ -1,10 +1,7 @@ package com.baeldung.apiversions.mediatype; -import java.util.HashMap; import java.util.Map; -import jakarta.annotation.PostConstruct; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; @@ -20,8 +17,10 @@ public class ProductController { private static final Logger LOGGER = LoggerFactory.getLogger(ProductController.class); - private final Map productsMap = new HashMap<>(); - private final Map productsV2Map = new HashMap<>(); + private final Map productsMap = + Map.of("1001", new ProductDto("1001", "apple", "apple_desc", 1.99)); + private final Map productsV2Map = + Map.of("1001", new ProductDtoV2("1001", "apple", 1.99)); @GetMapping(value = "/{id}", version = "1.0", produces = "application/vnd.baeldung.product+json") @@ -36,11 +35,4 @@ public ProductDtoV2 getProductV2ByIdCustomMedia(@PathVariable String id) { LOGGER.info("Get Product with custom media version 2 for id {}", id); return productsV2Map.get(id); } - - @PostConstruct - public void init(){ - productsMap.put("1001", new ProductDto("1001", "apple", - "apple_desc", 1.99)); - productsV2Map.put("1001", new ProductDtoV2("1001", "apple", 1.99)); - } } diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/ProductController.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/ProductController.java index 6b6a250785d4..710951300d7a 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/ProductController.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/pathsegment/ProductController.java @@ -1,10 +1,7 @@ package com.baeldung.apiversions.pathsegment; -import java.util.HashMap; import java.util.Map; -import jakarta.annotation.PostConstruct; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; @@ -22,8 +19,10 @@ public class ProductController { private static final Logger LOGGER = LoggerFactory.getLogger(ProductController.class); - private final Map productsMap = new HashMap<>(); - private final Map productsV2Map = new HashMap<>(); + private final Map productsMap = + Map.of("1001", new ProductDto("1001", "apple", "apple_desc", 1.99)); + private final Map productsV2Map = + Map.of("1001", new ProductDtoV2("1001", "apple", 1.99)); @GetMapping(value = "/{id}", version = "1.0") public ProductDto getProductV1ByIdPath(@PathVariable String id) { @@ -36,11 +35,4 @@ public ResponseEntity getProductV2ByIdPath(@PathVariable String id LOGGER.info("Get Product with Path specific version 2 for id {}", id); return new ResponseEntity<>(productsV2Map.get(id), HttpStatus.OK); } - - @PostConstruct - public void init(){ - productsMap.put("1001", new ProductDto("1001", "apple", - "apple_desc", 1.99)); - productsV2Map.put("1001", new ProductDtoV2("1001", "apple", 1.99)); - } } diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ProductController.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ProductController.java index 4e7fce4827c0..e0034e439ed6 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ProductController.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/queryparam/ProductController.java @@ -3,8 +3,6 @@ import java.util.HashMap; import java.util.Map; -import jakarta.annotation.PostConstruct; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; @@ -20,8 +18,10 @@ public class ProductController { private static final Logger LOGGER = LoggerFactory.getLogger(ProductController.class); - private final Map productsMap = new HashMap<>(); - private final Map productsV2Map = new HashMap<>(); + private final Map productsMap = + Map.of("1001", new ProductDto("1001", "apple", "apple_desc", 1.99)); + private final Map productsV2Map = + Map.of("1001", new ProductDtoV2("1001", "apple", 1.99)); @GetMapping(value = "/{id}", version = "1.0") public ProductDto getProductV1ByIdPath(@PathVariable String id) { @@ -34,11 +34,4 @@ public ProductDtoV2 getProductV2ByIdPath(@PathVariable String id) { LOGGER.info("Get Product version 2 for id {}", id); return productsV2Map.get(id); } - - @PostConstruct - public void init(){ - productsMap.put("1001", new ProductDto("1001", "apple", - "apple_desc", 1.99)); - productsV2Map.put("1001", new ProductDtoV2("1001", "apple", 1.99)); - } } From 3bea1372d733a326f0beef59fe0e7868fc17b716 Mon Sep 17 00:00:00 2001 From: saikat Date: Mon, 16 Feb 2026 11:46:34 +0530 Subject: [PATCH 23/38] refactor tests --- .../apiversions/header/ProductController.java | 6 ------ .../header/ProductControllerLiveTest.java | 14 ++++++++++---- .../mediatype/ProductControllerLiveTest.java | 16 +++++++++++----- .../pathsegment/ProductControllerLiveTest.java | 14 ++++++++++---- .../queryparam/ProductControllerLiveTest.java | 14 ++++++++++---- 5 files changed, 41 insertions(+), 23 deletions(-) diff --git a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ProductController.java b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ProductController.java index 6179639a159f..fac39a2a0495 100644 --- a/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ProductController.java +++ b/spring-boot-modules/spring-boot-5/src/main/java/com/baeldung/apiversions/header/ProductController.java @@ -28,12 +28,6 @@ public ProductDto getProductV1ById(@PathVariable String id) { return productsMap.get(id); } - @GetMapping(value = "/{id}", version = "1.1+") - public ProductDto getProductV1ById11(@PathVariable String id) { - LOGGER.info("Get Product version 1.1 for id {}", id); - return productsMap.get(id); - } - @GetMapping(value = "/{id}", version = "2.0") public ProductDtoV2 getProductV2ById(@PathVariable String id) { LOGGER.info("Get Product version 2 for id {}", id); diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java index 4bd73345de0c..0fd2046bebc2 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java @@ -3,6 +3,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.web.context.WebApplicationContext; @@ -11,14 +12,19 @@ class ProductControllerLiveTest { private RestTestClient restTestClient; + @LocalServerPort + private int port; + @BeforeEach void setUp(WebApplicationContext context) { - restTestClient = RestTestClient.bindToApplicationContext(context) + restTestClient = RestTestClient + .bindToServer() + .baseUrl("http://localhost:" + port) .build(); } @Test - void givenProductExists_WhenProductAPIIsCalled_WithHeaderVersion1_thenReturnValidProduct() { + void givenProductExists_WhenGetProductIsCalled_WithHeaderVersion1_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .header("X-API-Version", "1") @@ -31,7 +37,7 @@ void givenProductExists_WhenProductAPIIsCalled_WithHeaderVersion1_thenReturnVali } @Test - void givenProductExists_WhenProductAPIIsCalled_WithHeaderVersion2_thenReturnValidProduct() { + void givenProductExists_WhenGetProductIsCalled_WithHeaderVersion2_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .header("X-API-Version", "2") @@ -44,7 +50,7 @@ void givenProductExists_WhenProductAPIIsCalled_WithHeaderVersion2_thenReturnVali } @Test - void givenProductExists_WhenProductAPIIsCalled_WithInvalidHeaderVersion_thenReturnBadRequestError() { + void givenProductExists_WhenGetProductIsCalled_WithInvalidHeaderVersion_thenReturnBadRequestError() { restTestClient.get() .uri("/api/products/1001") .header("X-API-Version", "3") diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java index 2a5803d714a4..db760e4f06df 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java @@ -3,6 +3,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.web.context.WebApplicationContext; @@ -12,14 +13,19 @@ class ProductControllerLiveTest { private RestTestClient restTestClient; + @LocalServerPort + private int port; + @BeforeEach void setUp(WebApplicationContext context) { - restTestClient = RestTestClient.bindToApplicationContext(context) + restTestClient = RestTestClient + .bindToServer() + .baseUrl("http://localhost:" + port) .build(); } @Test - void givenProductExists_WhenProductAPIIsCalled_WithValidMediaTypeVersion_thenReturnValidProduct() { + void givenProductExists_WhenGetProductIsCalled_WithValidMediaTypeVersion_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .accept(MediaType.valueOf("application/vnd.baeldung.product+json;version=1")) @@ -34,7 +40,7 @@ void givenProductExists_WhenProductAPIIsCalled_WithValidMediaTypeVersion_thenRet } @Test - void givenProductExists_WhenProductAPIIsCalled_WithValidMediaType_thenReturnValidProduct() { + void givenProductExists_WhenGetProductIsCalled_WithValidMediaType_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .accept(MediaType.valueOf("application/vnd.baeldung.product+json;version=2")) @@ -49,7 +55,7 @@ void givenProductExists_WhenProductAPIIsCalled_WithValidMediaType_thenReturnVali } @Test - void givenProductExists_WhenProductAPIIsCalled_WithInValidMediaTypeVersion_thenReturnBadRequestError() { + void givenProductExists_WhenGetProductIsCalled_WithInValidMediaTypeVersion_thenReturnBadRequestError() { restTestClient.get() .uri("/api/products/1001") .accept(MediaType.valueOf("application/vnd.baeldung.product+json;version=3")) @@ -63,7 +69,7 @@ void givenProductExists_WhenProductAPIIsCalled_WithInValidMediaTypeVersion_thenR } @Test - void givenProductExists_WhenProductAPIIsCalled_WithInValidMediaType_thenReturnBadRequestError() { + void givenProductExists_WhenGetProductIsCalled_WithInValidMediaType_thenReturnBadRequestError() { restTestClient.get() .uri("/api/products/1001") .accept(MediaType.valueOf("application/invalid")) diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java index 48ccdc986791..fe724b612953 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java @@ -3,6 +3,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.web.context.WebApplicationContext; @@ -11,14 +12,19 @@ class ProductControllerLiveTest { private RestTestClient restTestClient; + @LocalServerPort + private int port; + @BeforeEach void setUp(WebApplicationContext context) { - restTestClient = RestTestClient.bindToApplicationContext(context) + restTestClient = RestTestClient + .bindToServer() + .baseUrl("http://localhost:" + port) .build(); } @Test - void givenProductExists_WhenGetProductAPIIsCalled_WithPathSegmentV1_thenReturnValidProduct() { + void givenProductExists_WhenGetProductIsCalled_WithPathSegmentV1_thenReturnValidProduct() { restTestClient.get() .uri("/api/v1/products/1001") .exchange() @@ -31,7 +37,7 @@ void givenProductExists_WhenGetProductAPIIsCalled_WithPathSegmentV1_thenReturnVa } @Test - void givenProductExists_WhenProductAPIIsCalled_WithPathSegmentV2_thenReturnValidProduct() { + void givenProductExists_WhenGetProductIsCalled_WithPathSegmentV2_thenReturnValidProduct() { restTestClient.get() .uri("/api/v2/products/1001") .exchange() @@ -44,7 +50,7 @@ void givenProductExists_WhenProductAPIIsCalled_WithPathSegmentV2_thenReturnValid } @Test - void givenProductExists_WhenProductAPIIsCalled_WithPathSegment2_thenThrowNotFoundError() { + void givenProductExists_WhenGetProductIsCalled_WithPathSegment2_thenThrowNotFoundError() { restTestClient.get() .uri("/api/2/products/1001") .exchange() diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java index cade885608ef..d148c5144988 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java @@ -3,6 +3,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.web.context.WebApplicationContext; @@ -11,14 +12,19 @@ class ProductControllerLiveTest { private RestTestClient restTestClient; + @LocalServerPort + private int port; + @BeforeEach void setUp(WebApplicationContext context) { - restTestClient = RestTestClient.bindToApplicationContext(context) + restTestClient = RestTestClient + .bindToServer() + .baseUrl("http://localhost:" + port) .build(); } @Test - void givenProductExists_WhenProductAPIIsCalled_WithQueryParamVersion1_thenReturnValidProduct() { + void givenProductExists_WhenGetProductIsCalled_WithQueryParamVersion1_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001?version=1") .exchange() @@ -31,7 +37,7 @@ void givenProductExists_WhenProductAPIIsCalled_WithQueryParamVersion1_thenReturn } @Test - void givenProductExists_WhenProductAPIIsCalled_WithQueryParamVersion2_thenReturnValidProductV2() { + void givenProductExists_WhenGetProductIsCalled_WithQueryParamVersion2_thenReturnValidProductV2() { restTestClient.get() .uri("/api/products/1001?version=2") .exchange() @@ -44,7 +50,7 @@ void givenProductExists_WhenProductAPIIsCalled_WithQueryParamVersion2_thenReturn } @Test - void givenProductExists_WhenProductAPIIsCalled_WithInvalidQueryParam_thenReturnBadRequestError() { + void givenProductExists_WhenGetProductIsCalled_WithInvalidQueryParam_thenReturnBadRequestError() { restTestClient.get() .uri("/api/products/1001?version=invalid") .exchange() From 50f25d979d6ece888e12ed5268a0f5cc2cd265a2 Mon Sep 17 00:00:00 2001 From: saikat Date: Mon, 16 Feb 2026 13:46:55 +0530 Subject: [PATCH 24/38] add more test case --- .../header/ProductControllerLiveTest.java | 21 +++++++++++++--- .../mediatype/ProductControllerLiveTest.java | 22 ++++++++++++++-- .../ProductControllerLiveTest.java | 25 ++++++++++++++++--- .../queryparam/ProductControllerLiveTest.java | 25 ++++++++++++++++--- 4 files changed, 82 insertions(+), 11 deletions(-) diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java index 0fd2046bebc2..78ff81842ff8 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java @@ -5,6 +5,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.web.client.ApiVersionInserter; import org.springframework.web.context.WebApplicationContext; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @@ -20,6 +21,7 @@ void setUp(WebApplicationContext context) { restTestClient = RestTestClient .bindToServer() .baseUrl("http://localhost:" + port) + .apiVersionInserter(ApiVersionInserter.useHeader("X-API-Version")) .build(); } @@ -27,7 +29,7 @@ void setUp(WebApplicationContext context) { void givenProductExists_WhenGetProductIsCalled_WithHeaderVersion1_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") - .header("X-API-Version", "1") + .apiVersion(1) .exchange() .expectStatus().isOk() .expectBody() @@ -40,7 +42,7 @@ void givenProductExists_WhenGetProductIsCalled_WithHeaderVersion1_thenReturnVali void givenProductExists_WhenGetProductIsCalled_WithHeaderVersion2_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") - .header("X-API-Version", "2") + .apiVersion(2) .exchange() .expectStatus().isOk() .expectBody() @@ -53,7 +55,7 @@ void givenProductExists_WhenGetProductIsCalled_WithHeaderVersion2_thenReturnVali void givenProductExists_WhenGetProductIsCalled_WithInvalidHeaderVersion_thenReturnBadRequestError() { restTestClient.get() .uri("/api/products/1001") - .header("X-API-Version", "3") + .apiVersion(3) .exchange() .expectStatus() .is4xxClientError() @@ -62,4 +64,17 @@ void givenProductExists_WhenGetProductIsCalled_WithInvalidHeaderVersion_thenRetu .jsonPath("$.desc").doesNotExist() .jsonPath("$.price").doesNotExist(); } + + @Test + void givenProductExists_WhenGetProductIsCalled_WithHeaderVersion1_0_thenReturnValidProduct() { + restTestClient.get() + .uri("/api/products/1001") + .apiVersion(1.0) + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("$.name").isEqualTo("apple") + .jsonPath("$.desc").isEqualTo("apple_desc") + .jsonPath("$.price").isEqualTo(1.99); + } } diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java index db760e4f06df..83f133dc5720 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java @@ -32,7 +32,8 @@ void givenProductExists_WhenGetProductIsCalled_WithValidMediaTypeVersion_thenRet .exchange() .expectStatus() .isOk() - .expectHeader().contentType("application/vnd.baeldung.product+json;version=1") + .expectHeader() + .contentType("application/vnd.baeldung.product+json;version=1") .expectBody() .jsonPath("$.name").isEqualTo("apple") .jsonPath("$.desc").isEqualTo("apple_desc") @@ -47,7 +48,8 @@ void givenProductExists_WhenGetProductIsCalled_WithValidMediaType_thenReturnVali .exchange() .expectStatus() .isOk() - .expectHeader().contentType("application/vnd.baeldung.product+json;version=2") + .expectHeader() + .contentType("application/vnd.baeldung.product+json;version=2") .expectBody() .jsonPath("$.name").isEqualTo("apple") .jsonPath("$.desc").doesNotExist() @@ -81,4 +83,20 @@ void givenProductExists_WhenGetProductIsCalled_WithInValidMediaType_thenReturnBa .jsonPath("$.desc").doesNotExist() .jsonPath("$.price").doesNotExist(); } + + @Test + void givenProductExists_WhenGetProductIsCalled_WithValidMediaTypeVersion1_0_thenReturnValidProduct() { + restTestClient.get() + .uri("/api/products/1001") + .accept(MediaType.valueOf("application/vnd.baeldung.product+json;version=1.0")) + .exchange() + .expectStatus() + .isOk() + .expectHeader() + .contentType("application/vnd.baeldung.product+json;version=1.0") + .expectBody() + .jsonPath("$.name").isEqualTo("apple") + .jsonPath("$.desc").isEqualTo("apple_desc") + .jsonPath("$.price").isEqualTo(1.99); + } } diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java index fe724b612953..1cf3ecb2cbb0 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java @@ -5,6 +5,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.web.client.ApiVersionInserter; import org.springframework.web.context.WebApplicationContext; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @@ -20,13 +21,15 @@ void setUp(WebApplicationContext context) { restTestClient = RestTestClient .bindToServer() .baseUrl("http://localhost:" + port) + .apiVersionInserter(ApiVersionInserter.usePathSegment(1)) .build(); } @Test void givenProductExists_WhenGetProductIsCalled_WithPathSegmentV1_thenReturnValidProduct() { restTestClient.get() - .uri("/api/v1/products/1001") + .uri("/api/products/1001") + .apiVersion("v1") .exchange() .expectStatus() .isOk() @@ -39,7 +42,8 @@ void givenProductExists_WhenGetProductIsCalled_WithPathSegmentV1_thenReturnValid @Test void givenProductExists_WhenGetProductIsCalled_WithPathSegmentV2_thenReturnValidProduct() { restTestClient.get() - .uri("/api/v2/products/1001") + .uri("/api/products/1001") + .apiVersion("v2") .exchange() .expectStatus() .isOk() @@ -52,10 +56,25 @@ void givenProductExists_WhenGetProductIsCalled_WithPathSegmentV2_thenReturnValid @Test void givenProductExists_WhenGetProductIsCalled_WithPathSegment2_thenThrowNotFoundError() { restTestClient.get() - .uri("/api/2/products/1001") + .uri("/api/3/products/1001") + .apiVersion(3) .exchange() .expectStatus() .isNotFound(); } + @Test + void givenProductExists_WhenGetProductIsCalled_WithPathSegmentV1_0_thenReturnValidProduct() { + restTestClient.get() + .uri("/api/products/1001") + .apiVersion("v1.0") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("$.name").isEqualTo("apple") + .jsonPath("$.desc").isEqualTo("apple_desc") + .jsonPath("$.price").isEqualTo(1.99); + } + } diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java index d148c5144988..4f49fe65943f 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java @@ -5,6 +5,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.web.client.ApiVersionInserter; import org.springframework.web.context.WebApplicationContext; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @@ -20,13 +21,15 @@ void setUp(WebApplicationContext context) { restTestClient = RestTestClient .bindToServer() .baseUrl("http://localhost:" + port) + .apiVersionInserter(ApiVersionInserter.useQueryParam("version")) .build(); } @Test void givenProductExists_WhenGetProductIsCalled_WithQueryParamVersion1_thenReturnValidProduct() { restTestClient.get() - .uri("/api/products/1001?version=1") + .uri("/api/products/1001") + .apiVersion(1) .exchange() .expectStatus() .isOk() @@ -39,7 +42,8 @@ void givenProductExists_WhenGetProductIsCalled_WithQueryParamVersion1_thenReturn @Test void givenProductExists_WhenGetProductIsCalled_WithQueryParamVersion2_thenReturnValidProductV2() { restTestClient.get() - .uri("/api/products/1001?version=2") + .uri("/api/products/1001") + .apiVersion(2) .exchange() .expectStatus() .isOk() @@ -52,7 +56,8 @@ void givenProductExists_WhenGetProductIsCalled_WithQueryParamVersion2_thenReturn @Test void givenProductExists_WhenGetProductIsCalled_WithInvalidQueryParam_thenReturnBadRequestError() { restTestClient.get() - .uri("/api/products/1001?version=invalid") + .uri("/api/products/1001") + .apiVersion(3) .exchange() .expectStatus() .is4xxClientError() @@ -61,4 +66,18 @@ void givenProductExists_WhenGetProductIsCalled_WithInvalidQueryParam_thenReturnB .jsonPath("$.desc").doesNotExist() .jsonPath("$.price").doesNotExist(); } + + @Test + void givenProductExists_WhenGetProductIsCalled_WithQueryParamVersion1_0_thenReturnValidProduct() { + restTestClient.get() + .uri("/api/products/1001") + .apiVersion(1.0) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("$.name").isEqualTo("apple") + .jsonPath("$.desc").isEqualTo("apple_desc") + .jsonPath("$.price").isEqualTo(1.99); + } } From f8aad087c375614493ac80b36c023a7d71c87fca Mon Sep 17 00:00:00 2001 From: saikat Date: Mon, 16 Feb 2026 18:52:09 +0530 Subject: [PATCH 25/38] update project to parent pom and test case refactor --- spring-boot-modules/pom.xml | 1 + .../baeldung/apiversions/header/ProductControllerLiveTest.java | 3 +-- .../apiversions/mediatype/ProductControllerLiveTest.java | 3 +-- .../apiversions/pathsegment/ProductControllerLiveTest.java | 3 +-- .../apiversions/queryparam/ProductControllerLiveTest.java | 3 +-- 5 files changed, 5 insertions(+), 8 deletions(-) diff --git a/spring-boot-modules/pom.xml b/spring-boot-modules/pom.xml index ef45502c6471..0b4d879e864c 100644 --- a/spring-boot-modules/pom.xml +++ b/spring-boot-modules/pom.xml @@ -116,6 +116,7 @@ spring-boot-3-4 spring-boot-4 + spring-boot-5 spring-boot-resilience4j spring-boot-retries spring-boot-properties diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java index 78ff81842ff8..d13744c31a0b 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java @@ -6,7 +6,6 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.web.client.ApiVersionInserter; -import org.springframework.web.context.WebApplicationContext; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class ProductControllerLiveTest { @@ -17,7 +16,7 @@ class ProductControllerLiveTest { private int port; @BeforeEach - void setUp(WebApplicationContext context) { + void setUp() { restTestClient = RestTestClient .bindToServer() .baseUrl("http://localhost:" + port) diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java index 83f133dc5720..f1922e76f0af 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java @@ -6,7 +6,6 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.client.RestTestClient; -import org.springframework.web.context.WebApplicationContext; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class ProductControllerLiveTest { @@ -17,7 +16,7 @@ class ProductControllerLiveTest { private int port; @BeforeEach - void setUp(WebApplicationContext context) { + void setUp() { restTestClient = RestTestClient .bindToServer() .baseUrl("http://localhost:" + port) diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java index 1cf3ecb2cbb0..af5df0ee237d 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java @@ -6,7 +6,6 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.web.client.ApiVersionInserter; -import org.springframework.web.context.WebApplicationContext; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class ProductControllerLiveTest { @@ -17,7 +16,7 @@ class ProductControllerLiveTest { private int port; @BeforeEach - void setUp(WebApplicationContext context) { + void setUp() { restTestClient = RestTestClient .bindToServer() .baseUrl("http://localhost:" + port) diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java index 4f49fe65943f..d2f4cd91c4e8 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java @@ -6,7 +6,6 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.web.client.ApiVersionInserter; -import org.springframework.web.context.WebApplicationContext; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class ProductControllerLiveTest { @@ -17,7 +16,7 @@ class ProductControllerLiveTest { private int port; @BeforeEach - void setUp(WebApplicationContext context) { + void setUp() { restTestClient = RestTestClient .bindToServer() .baseUrl("http://localhost:" + port) From d80e698065140c7eee14b60530e3584df34f0d82 Mon Sep 17 00:00:00 2001 From: saikatcse03 Date: Mon, 23 Feb 2026 10:52:03 +0530 Subject: [PATCH 26/38] update test names --- .../apiversions/header/ProductControllerLiveTest.java | 8 ++++---- .../mediatype/ProductControllerLiveTest.java | 10 +++++----- .../pathsegment/ProductControllerLiveTest.java | 8 ++++---- .../queryparam/ProductControllerLiveTest.java | 8 ++++---- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java index d13744c31a0b..916eeae53d0f 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java @@ -25,7 +25,7 @@ void setUp() { } @Test - void givenProductExists_WhenGetProductIsCalled_WithHeaderVersion1_thenReturnValidProduct() { + void givenProductExists_WhenGetProductIsCalledWithHeaderVersion1_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .apiVersion(1) @@ -38,7 +38,7 @@ void givenProductExists_WhenGetProductIsCalled_WithHeaderVersion1_thenReturnVali } @Test - void givenProductExists_WhenGetProductIsCalled_WithHeaderVersion2_thenReturnValidProduct() { + void givenProductExists_WhenGetProductIsCalledWithHeaderVersion2_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .apiVersion(2) @@ -51,7 +51,7 @@ void givenProductExists_WhenGetProductIsCalled_WithHeaderVersion2_thenReturnVali } @Test - void givenProductExists_WhenGetProductIsCalled_WithInvalidHeaderVersion_thenReturnBadRequestError() { + void givenProductExists_WhenGetProductIsCalledWithInvalidHeaderVersion_thenReturnBadRequestError() { restTestClient.get() .uri("/api/products/1001") .apiVersion(3) @@ -65,7 +65,7 @@ void givenProductExists_WhenGetProductIsCalled_WithInvalidHeaderVersion_thenRetu } @Test - void givenProductExists_WhenGetProductIsCalled_WithHeaderVersion1_0_thenReturnValidProduct() { + void givenProductExists_WhenGetProductIsCalledWithHeaderVersion1Dot0_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .apiVersion(1.0) diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java index f1922e76f0af..9564dfcdca72 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java @@ -24,7 +24,7 @@ void setUp() { } @Test - void givenProductExists_WhenGetProductIsCalled_WithValidMediaTypeVersion_thenReturnValidProduct() { + void givenProductExists_WhenGetProductIsCalledWithValidMediaTypeVersion_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .accept(MediaType.valueOf("application/vnd.baeldung.product+json;version=1")) @@ -40,7 +40,7 @@ void givenProductExists_WhenGetProductIsCalled_WithValidMediaTypeVersion_thenRet } @Test - void givenProductExists_WhenGetProductIsCalled_WithValidMediaType_thenReturnValidProduct() { + void givenProductExists_WhenGetProductIsCalledWithValidMediaType_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .accept(MediaType.valueOf("application/vnd.baeldung.product+json;version=2")) @@ -56,7 +56,7 @@ void givenProductExists_WhenGetProductIsCalled_WithValidMediaType_thenReturnVali } @Test - void givenProductExists_WhenGetProductIsCalled_WithInValidMediaTypeVersion_thenReturnBadRequestError() { + void givenProductExists_WhenGetProductIsCalledWithInValidMediaTypeVersion_thenReturnBadRequestError() { restTestClient.get() .uri("/api/products/1001") .accept(MediaType.valueOf("application/vnd.baeldung.product+json;version=3")) @@ -70,7 +70,7 @@ void givenProductExists_WhenGetProductIsCalled_WithInValidMediaTypeVersion_thenR } @Test - void givenProductExists_WhenGetProductIsCalled_WithInValidMediaType_thenReturnBadRequestError() { + void givenProductExists_WhenGetProductIsCalledWithInValidMediaType_thenReturnBadRequestError() { restTestClient.get() .uri("/api/products/1001") .accept(MediaType.valueOf("application/invalid")) @@ -84,7 +84,7 @@ void givenProductExists_WhenGetProductIsCalled_WithInValidMediaType_thenReturnBa } @Test - void givenProductExists_WhenGetProductIsCalled_WithValidMediaTypeVersion1_0_thenReturnValidProduct() { + void givenProductExists_WhenGetProductIsCalledWithValidMediaTypeVersion1Dot0_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .accept(MediaType.valueOf("application/vnd.baeldung.product+json;version=1.0")) diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java index af5df0ee237d..3b219df36164 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java @@ -25,7 +25,7 @@ void setUp() { } @Test - void givenProductExists_WhenGetProductIsCalled_WithPathSegmentV1_thenReturnValidProduct() { + void givenProductExists_WhenGetProductIsCalledWithPathSegmentV1_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .apiVersion("v1") @@ -39,7 +39,7 @@ void givenProductExists_WhenGetProductIsCalled_WithPathSegmentV1_thenReturnValid } @Test - void givenProductExists_WhenGetProductIsCalled_WithPathSegmentV2_thenReturnValidProduct() { + void givenProductExists_WhenGetProductIsCalledWithPathSegmentV2_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .apiVersion("v2") @@ -53,7 +53,7 @@ void givenProductExists_WhenGetProductIsCalled_WithPathSegmentV2_thenReturnValid } @Test - void givenProductExists_WhenGetProductIsCalled_WithPathSegment2_thenThrowNotFoundError() { + void givenProductExists_WhenGetProductIsCalledWithPathSegment2_thenThrowNotFoundError() { restTestClient.get() .uri("/api/3/products/1001") .apiVersion(3) @@ -63,7 +63,7 @@ void givenProductExists_WhenGetProductIsCalled_WithPathSegment2_thenThrowNotFoun } @Test - void givenProductExists_WhenGetProductIsCalled_WithPathSegmentV1_0_thenReturnValidProduct() { + void givenProductExists_WhenGetProductIsCalledWithPathSegmentV1Dot0_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .apiVersion("v1.0") diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java index d2f4cd91c4e8..549b8a455be8 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java @@ -25,7 +25,7 @@ void setUp() { } @Test - void givenProductExists_WhenGetProductIsCalled_WithQueryParamVersion1_thenReturnValidProduct() { + void givenProductExists_WhenGetProductIsCalledWithQueryParamVersion1_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .apiVersion(1) @@ -39,7 +39,7 @@ void givenProductExists_WhenGetProductIsCalled_WithQueryParamVersion1_thenReturn } @Test - void givenProductExists_WhenGetProductIsCalled_WithQueryParamVersion2_thenReturnValidProductV2() { + void givenProductExists_WhenGetProductIsCalledWithQueryParamVersion2_thenReturnValidProductV2() { restTestClient.get() .uri("/api/products/1001") .apiVersion(2) @@ -53,7 +53,7 @@ void givenProductExists_WhenGetProductIsCalled_WithQueryParamVersion2_thenReturn } @Test - void givenProductExists_WhenGetProductIsCalled_WithInvalidQueryParam_thenReturnBadRequestError() { + void givenProductExists_WhenGetProductIsCalledWithInvalidQueryParam_thenReturnBadRequestError() { restTestClient.get() .uri("/api/products/1001") .apiVersion(3) @@ -67,7 +67,7 @@ void givenProductExists_WhenGetProductIsCalled_WithInvalidQueryParam_thenReturnB } @Test - void givenProductExists_WhenGetProductIsCalled_WithQueryParamVersion1_0_thenReturnValidProduct() { + void givenProductExists_WhenGetProductIsCalledWithQueryParamVersion1Dot0_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .apiVersion(1.0) From cd5899fcd36ac9e1864bc9e28efd167a824224eb Mon Sep 17 00:00:00 2001 From: saikatcse03 Date: Mon, 23 Feb 2026 21:11:10 +0530 Subject: [PATCH 27/38] update test name and property for surefire --- spring-boot-modules/spring-boot-5/pom.xml | 3 ++- .../apiversions/header/ProductControllerLiveTest.java | 8 ++++---- .../mediatype/ProductControllerLiveTest.java | 10 +++++----- .../pathsegment/ProductControllerLiveTest.java | 8 ++++---- .../queryparam/ProductControllerLiveTest.java | 8 ++++---- 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/spring-boot-modules/spring-boot-5/pom.xml b/spring-boot-modules/spring-boot-5/pom.xml index 791c4fd86743..0f5fd41bf388 100644 --- a/spring-boot-modules/spring-boot-5/pom.xml +++ b/spring-boot-modules/spring-boot-5/pom.xml @@ -51,7 +51,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.2.5 + ${maven-surefire-plugin.version} @@ -60,5 +60,6 @@ 4.0.2 6.0.0 1.5.18 + 3.0.0-M7 \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java index 916eeae53d0f..080f77151305 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/header/ProductControllerLiveTest.java @@ -25,7 +25,7 @@ void setUp() { } @Test - void givenProductExists_WhenGetProductIsCalledWithHeaderVersion1_thenReturnValidProduct() { + void givenProductExists_whenGetProductIsCalledWithHeaderVersion1_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .apiVersion(1) @@ -38,7 +38,7 @@ void givenProductExists_WhenGetProductIsCalledWithHeaderVersion1_thenReturnValid } @Test - void givenProductExists_WhenGetProductIsCalledWithHeaderVersion2_thenReturnValidProduct() { + void givenProductExists_whenGetProductIsCalledWithHeaderVersion2_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .apiVersion(2) @@ -51,7 +51,7 @@ void givenProductExists_WhenGetProductIsCalledWithHeaderVersion2_thenReturnValid } @Test - void givenProductExists_WhenGetProductIsCalledWithInvalidHeaderVersion_thenReturnBadRequestError() { + void givenProductExists_whenGetProductIsCalledWithInvalidHeaderVersion_thenReturnBadRequestError() { restTestClient.get() .uri("/api/products/1001") .apiVersion(3) @@ -65,7 +65,7 @@ void givenProductExists_WhenGetProductIsCalledWithInvalidHeaderVersion_thenRetur } @Test - void givenProductExists_WhenGetProductIsCalledWithHeaderVersion1Dot0_thenReturnValidProduct() { + void givenProductExists_whenGetProductIsCalledWithHeaderVersion1Dot0_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .apiVersion(1.0) diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java index 9564dfcdca72..9c8499ed847c 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/mediatype/ProductControllerLiveTest.java @@ -24,7 +24,7 @@ void setUp() { } @Test - void givenProductExists_WhenGetProductIsCalledWithValidMediaTypeVersion_thenReturnValidProduct() { + void givenProductExists_whenGetProductIsCalledWithValidMediaTypeVersion_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .accept(MediaType.valueOf("application/vnd.baeldung.product+json;version=1")) @@ -40,7 +40,7 @@ void givenProductExists_WhenGetProductIsCalledWithValidMediaTypeVersion_thenRetu } @Test - void givenProductExists_WhenGetProductIsCalledWithValidMediaType_thenReturnValidProduct() { + void givenProductExists_whenGetProductIsCalledWithValidMediaType_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .accept(MediaType.valueOf("application/vnd.baeldung.product+json;version=2")) @@ -56,7 +56,7 @@ void givenProductExists_WhenGetProductIsCalledWithValidMediaType_thenReturnValid } @Test - void givenProductExists_WhenGetProductIsCalledWithInValidMediaTypeVersion_thenReturnBadRequestError() { + void givenProductExists_whenGetProductIsCalledWithInValidMediaTypeVersion_thenReturnBadRequestError() { restTestClient.get() .uri("/api/products/1001") .accept(MediaType.valueOf("application/vnd.baeldung.product+json;version=3")) @@ -70,7 +70,7 @@ void givenProductExists_WhenGetProductIsCalledWithInValidMediaTypeVersion_thenRe } @Test - void givenProductExists_WhenGetProductIsCalledWithInValidMediaType_thenReturnBadRequestError() { + void givenProductExists_whenGetProductIsCalledWithInValidMediaType_thenReturnBadRequestError() { restTestClient.get() .uri("/api/products/1001") .accept(MediaType.valueOf("application/invalid")) @@ -84,7 +84,7 @@ void givenProductExists_WhenGetProductIsCalledWithInValidMediaType_thenReturnBad } @Test - void givenProductExists_WhenGetProductIsCalledWithValidMediaTypeVersion1Dot0_thenReturnValidProduct() { + void givenProductExists_whenGetProductIsCalledWithValidMediaTypeVersion1Dot0_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .accept(MediaType.valueOf("application/vnd.baeldung.product+json;version=1.0")) diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java index 3b219df36164..5149a8fe0c2a 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/pathsegment/ProductControllerLiveTest.java @@ -25,7 +25,7 @@ void setUp() { } @Test - void givenProductExists_WhenGetProductIsCalledWithPathSegmentV1_thenReturnValidProduct() { + void givenProductExists_whenGetProductIsCalledWithPathSegmentV1_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .apiVersion("v1") @@ -39,7 +39,7 @@ void givenProductExists_WhenGetProductIsCalledWithPathSegmentV1_thenReturnValidP } @Test - void givenProductExists_WhenGetProductIsCalledWithPathSegmentV2_thenReturnValidProduct() { + void givenProductExists_whenGetProductIsCalledWithPathSegmentV2_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .apiVersion("v2") @@ -53,7 +53,7 @@ void givenProductExists_WhenGetProductIsCalledWithPathSegmentV2_thenReturnValidP } @Test - void givenProductExists_WhenGetProductIsCalledWithPathSegment2_thenThrowNotFoundError() { + void givenProductExists_whenGetProductIsCalledWithPathSegment2_thenThrowNotFoundError() { restTestClient.get() .uri("/api/3/products/1001") .apiVersion(3) @@ -63,7 +63,7 @@ void givenProductExists_WhenGetProductIsCalledWithPathSegment2_thenThrowNotFound } @Test - void givenProductExists_WhenGetProductIsCalledWithPathSegmentV1Dot0_thenReturnValidProduct() { + void givenProductExists_whenGetProductIsCalledWithPathSegmentV1Dot0_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .apiVersion("v1.0") diff --git a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java index 549b8a455be8..47265174a11c 100644 --- a/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java +++ b/spring-boot-modules/spring-boot-5/src/test/java/com/baeldung/apiversions/queryparam/ProductControllerLiveTest.java @@ -25,7 +25,7 @@ void setUp() { } @Test - void givenProductExists_WhenGetProductIsCalledWithQueryParamVersion1_thenReturnValidProduct() { + void givenProductExists_whenGetProductIsCalledWithQueryParamVersion1_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .apiVersion(1) @@ -39,7 +39,7 @@ void givenProductExists_WhenGetProductIsCalledWithQueryParamVersion1_thenReturnV } @Test - void givenProductExists_WhenGetProductIsCalledWithQueryParamVersion2_thenReturnValidProductV2() { + void givenProductExists_whenGetProductIsCalledWithQueryParamVersion2_thenReturnValidProductV2() { restTestClient.get() .uri("/api/products/1001") .apiVersion(2) @@ -53,7 +53,7 @@ void givenProductExists_WhenGetProductIsCalledWithQueryParamVersion2_thenReturnV } @Test - void givenProductExists_WhenGetProductIsCalledWithInvalidQueryParam_thenReturnBadRequestError() { + void givenProductExists_whenGetProductIsCalledWithInvalidQueryParam_thenReturnBadRequestError() { restTestClient.get() .uri("/api/products/1001") .apiVersion(3) @@ -67,7 +67,7 @@ void givenProductExists_WhenGetProductIsCalledWithInvalidQueryParam_thenReturnBa } @Test - void givenProductExists_WhenGetProductIsCalledWithQueryParamVersion1Dot0_thenReturnValidProduct() { + void givenProductExists_whenGetProductIsCalledWithQueryParamVersion1Dot0_thenReturnValidProduct() { restTestClient.get() .uri("/api/products/1001") .apiVersion(1.0) From e2d958e91387c796c4b8f12eb6217f96754613b8 Mon Sep 17 00:00:00 2001 From: saikatcse03 Date: Tue, 31 Mar 2026 13:05:12 +0530 Subject: [PATCH 28/38] Implement virtual thread pinning examples --- .../core-java-concurrency-advanced-7/pom.xml | 64 ++++++++++++++ .../virtualthread/classinit/HeavyClass.java | 29 +++++++ .../classloader/CustomClassLoader.java | 55 ++++++++++++ .../virtualthread/classloader/MyClass.java | 6 ++ .../foreignfunction/ForeignFunctionClass.java | 33 ++++++++ .../nativemethod/NativeDemo.java | 10 +++ ...ng_virtualthread_nativemethod_NativeDemo.h | 21 +++++ .../virtualthread/nativemethod/native-lib.c | 8 ++ .../synchronize/BenchmarkVirtualThread.java | 48 +++++++++++ .../synchronize/CartService.java | 43 ++++++++++ .../synchronize/fixed/CartService.java | 51 +++++++++++ .../classinit/HeavyClassTest.java | 51 +++++++++++ .../classloader/CustomClassLoaderTest.java | 67 +++++++++++++++ .../synchronize/CartServiceTest.java | 84 +++++++++++++++++++ .../synchronize/fixed/CartServiceTest.java | 80 ++++++++++++++++++ 15 files changed, 650 insertions(+) create mode 100644 core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/classinit/HeavyClass.java create mode 100644 core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/classloader/CustomClassLoader.java create mode 100644 core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/classloader/MyClass.java create mode 100644 core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/foreignfunction/ForeignFunctionClass.java create mode 100644 core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/nativemethod/NativeDemo.java create mode 100644 core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/nativemethod/com_baeldung_virtualthread_nativemethod_NativeDemo.h create mode 100644 core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/nativemethod/native-lib.c create mode 100644 core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/BenchmarkVirtualThread.java create mode 100644 core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/CartService.java create mode 100644 core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/fixed/CartService.java create mode 100644 core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classinit/HeavyClassTest.java create mode 100644 core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classloader/CustomClassLoaderTest.java create mode 100644 core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/synchronize/CartServiceTest.java create mode 100644 core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/synchronize/fixed/CartServiceTest.java diff --git a/core-java-modules/core-java-concurrency-advanced-7/pom.xml b/core-java-modules/core-java-concurrency-advanced-7/pom.xml index 32ac6448175d..db709df867c3 100644 --- a/core-java-modules/core-java-concurrency-advanced-7/pom.xml +++ b/core-java-modules/core-java-concurrency-advanced-7/pom.xml @@ -20,10 +20,74 @@ ${awaitility.version} test + + org.openjdk.jmh + jmh-core + ${jmh.core.version} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.core.version} + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source.version} + ${maven.compiler.target.version} + false + + --enable-preview + + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.core.version} + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + --enable-preview + + + + org.apache.maven.plugins + maven-shade-plugin + + + package + shade + + benchmarks + + + org.openjdk.jmh.Main + + + + + + + + + 1.7.0 + 21 + 21 + 3.5.0 + 1.37 diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/classinit/HeavyClass.java b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/classinit/HeavyClass.java new file mode 100644 index 000000000000..4ddf22ca3b8b --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/classinit/HeavyClass.java @@ -0,0 +1,29 @@ +package com.baeldung.virtualthread.classinit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class HeavyClass { + + private static final Logger LOGGER = LoggerFactory.getLogger(HeavyClass.class); + + static { + try { + Thread.sleep(100); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + + LOGGER.info("static initialization done"); + } + + { + try { + Thread.sleep(100); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + + LOGGER.info("initialization done"); + } +} \ No newline at end of file diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/classloader/CustomClassLoader.java b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/classloader/CustomClassLoader.java new file mode 100644 index 000000000000..27a7fcb15f58 --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/classloader/CustomClassLoader.java @@ -0,0 +1,55 @@ +package com.baeldung.virtualthread.classloader; + +import java.io.IOException; +import java.nio.file.Path; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class CustomClassLoader extends ClassLoader { + + private static final Logger LOGGER = LoggerFactory.getLogger(CustomClassLoader.class); + private final Path classDir; + + public CustomClassLoader(Path classDir) { + super(ClassLoader.getSystemClassLoader()); + this.classDir = classDir; + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + LOGGER.info("Load class for {}", name); + + Class clazz = findLoadedClass(name); + + if (clazz == null) { + try { + clazz = findClass(name); + } catch (ClassNotFoundException ex) { + clazz = super.loadClass(name, resolve); + } + } + + if (resolve) { + resolveClass(clazz); + } + + return clazz; + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + LOGGER.info("Finding class for {}", name); + + try { + Path file = classDir.resolve(name.replace('.', '/') + ".class"); + byte[] bytes = java.nio.file.Files.readAllBytes(file); + Thread.sleep(100); + + return defineClass(name, bytes, 0, bytes.length); + } catch (InterruptedException | IOException ex) { + LOGGER.error("Error while finding class file {}", ex.getMessage()); + throw new ClassNotFoundException(ex.getMessage(), ex); + } + } +} diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/classloader/MyClass.java b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/classloader/MyClass.java new file mode 100644 index 000000000000..10ef734d65bb --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/classloader/MyClass.java @@ -0,0 +1,6 @@ +package com.baeldung.virtualthread.classloader; + +public class MyClass { + public MyClass() { + } +} \ No newline at end of file diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/foreignfunction/ForeignFunctionClass.java b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/foreignfunction/ForeignFunctionClass.java new file mode 100644 index 000000000000..b13368c95b64 --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/foreignfunction/ForeignFunctionClass.java @@ -0,0 +1,33 @@ +package com.baeldung.virtualthread.foreignfunction; + +import static java.lang.foreign.ValueLayout.JAVA_INT; +import static java.lang.foreign.ValueLayout.JAVA_LONG; + +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.SymbolLookup; +import java.lang.invoke.MethodHandle; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ForeignFunctionClass { + + private static final Logger LOGGER = LoggerFactory.getLogger(ForeignFunctionClass.class); + + public void execute() { + LOGGER.info("Running foreign function sleep..."); + + Linker linker = Linker.nativeLinker(); + SymbolLookup stdlib = linker.defaultLookup(); + MethodHandle sleep = linker.downcallHandle(stdlib.find("sleep") + .orElseThrow(), FunctionDescriptor.of(JAVA_INT, JAVA_LONG)); + + try { + sleep.invoke(100); + } catch (Throwable ex) { + System.out.println("Error in native sleep..."); + throw new RuntimeException(ex); + } + } +} diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/nativemethod/NativeDemo.java b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/nativemethod/NativeDemo.java new file mode 100644 index 000000000000..122d97601ae0 --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/nativemethod/NativeDemo.java @@ -0,0 +1,10 @@ +package com.baeldung.virtualthread.nativemethod; + +public class NativeDemo { + + static { + System.loadLibrary("native-lib"); + } + + public native String nativeCall(); +} diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/nativemethod/com_baeldung_virtualthread_nativemethod_NativeDemo.h b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/nativemethod/com_baeldung_virtualthread_nativemethod_NativeDemo.h new file mode 100644 index 000000000000..5d5fab6b1910 --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/nativemethod/com_baeldung_virtualthread_nativemethod_NativeDemo.h @@ -0,0 +1,21 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class com_baeldung_virtualthread_nativemethod_NativeDemo */ + +#ifndef _Included_com_baeldung_virtualthread_nativemethod_NativeDemo +#define _Included_com_baeldung_virtualthread_nativemethod_NativeDemo +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: com_baeldung_virtualthread_nativemethod_NativeDemo + * Method: nativeCall + * Signature: ()Ljava/lang/String; + */ +JNIEXPORT jstring JNICALL Java_com_baeldung_virtualthread_nativemethod_NativeDemo_nativeCall + (JNIEnv *, jobject); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/nativemethod/native-lib.c b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/nativemethod/native-lib.c new file mode 100644 index 000000000000..48c81aac2aa3 --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/nativemethod/native-lib.c @@ -0,0 +1,8 @@ +#include +#include +#include "com_baeldung_virtualthread_nativemethod_NativeDemo.h" + +JNIEXPORT jstring JNICALL Java_com_baeldung_virtualthread_nativemethod_NativeDemo_nativeCall(JNIEnv *env, jobject obj) { + sleep(3); + return (*env)->NewStringUTF(env, "Return from native code!"); +} \ No newline at end of file diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/BenchmarkVirtualThread.java b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/BenchmarkVirtualThread.java new file mode 100644 index 000000000000..7a6d5d4391cb --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/BenchmarkVirtualThread.java @@ -0,0 +1,48 @@ +package com.baeldung.virtualthread.synchronize; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@BenchmarkMode({ Mode.AverageTime, Mode.Throughput }) +@OutputTimeUnit(TimeUnit.SECONDS) +@Warmup(iterations = 1, time = 5, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 3, time = 5, timeUnit = TimeUnit.SECONDS) +@Fork(value = 2) +@State(Scope.Benchmark) +public class BenchmarkVirtualThread { + + private final CartService cartService = new CartService(); + + @Param({ "100", "1000", "10000" }) + private int CONCURRENCY; + + @Benchmark + public void benchmark() throws InterruptedException, IOException { + List threads = new ArrayList<>(); + IntStream.range(0, CONCURRENCY).forEach(i -> threads.add(Thread.startVirtualThread(() -> cartService.update(UUID.randomUUID() + .toString(), 2)))); + + threads.forEach(th -> { + try { + th.join(); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + }); + } +} \ No newline at end of file diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/CartService.java b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/CartService.java new file mode 100644 index 000000000000..e3a0a0c772a4 --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/CartService.java @@ -0,0 +1,43 @@ +package com.baeldung.virtualthread.synchronize; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CartService { + + private static final Logger LOGGER = LoggerFactory.getLogger(CartService.class); + + private final Map products; + private final Map locks = new ConcurrentHashMap<>(); + + public CartService() { + this.products = new HashMap<>(); + } + + public void update(String productId, int quantity) { + Object lock = locks.computeIfAbsent(productId, k -> new Object()); + + synchronized (lock) { + simulateAPI(); + products.merge(productId, quantity, Integer::sum); + } + + LOGGER.info("Updated Cart for {} {}", productId, quantity); + } + + public Map getProducts() { + return Map.copyOf(products); + } + + private void simulateAPI() { + try { + Thread.sleep(50); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/fixed/CartService.java b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/fixed/CartService.java new file mode 100644 index 000000000000..754b0abd236d --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/fixed/CartService.java @@ -0,0 +1,51 @@ +package com.baeldung.virtualthread.synchronize.fixed; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CartService { + + private static final Logger LOGGER = LoggerFactory.getLogger(CartService.class); + + private final Map products; + private final Map locks = new ConcurrentHashMap<>(); + + public CartService() { + this.products = new HashMap<>(); + } + + public void update(String productId, int quantity) { + Lock lock = locks.computeIfAbsent(productId, k -> new ReentrantLock()); + + try { + if (lock.tryLock(500, TimeUnit.MILLISECONDS)) { + simulateAPI(); + products.merge(productId, quantity, Integer::sum); + + LOGGER.info("Updated Cart for {} {}", productId, quantity); + lock.unlock(); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private void simulateAPI() { + try { + Thread.sleep(50); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + public Map getProducts() { + return Map.copyOf(products); + } +} diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classinit/HeavyClassTest.java b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classinit/HeavyClassTest.java new file mode 100644 index 000000000000..ac8e0d283404 --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classinit/HeavyClassTest.java @@ -0,0 +1,51 @@ +package com.baeldung.virtualthread.classinit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordingFile; + +public class HeavyClassTest { + + @Test + void givenJFRIsEnabled_whenVThreadIsBlocked_thenDetectVThreadPinned() throws IOException, InterruptedException { + Path file = Path.of("pinning_1.jfr"); + + try (Recording recording = new Recording()) { + recording.enable("jdk.VirtualThreadPinned") + .withThreshold(Duration.ofMillis(1)); + recording.start(); + + Thread th1 = Thread.ofVirtual() + .start(HeavyClass::new); + th1.join(); + + recording.stop(); + recording.dump(file); + } + + try (RecordingFile rf = new RecordingFile(file)) { + assertTrue(rf.hasMoreEvents()); + + while (rf.hasMoreEvents()) { + RecordedEvent event = rf.readEvent(); + System.out.println(event); + assertEquals("jdk.VirtualThreadPinned", event.getEventType() + .getName()); + assertEquals("Virtual Thread Pinned", event.getEventType() + .getLabel()); + } + } + + Files.delete(file); + } +} diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classloader/CustomClassLoaderTest.java b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classloader/CustomClassLoaderTest.java new file mode 100644 index 000000000000..6fdc7bda9d52 --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classloader/CustomClassLoaderTest.java @@ -0,0 +1,67 @@ +package com.baeldung.virtualthread.classloader; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordingFile; + +public class CustomClassLoaderTest { + + @Test + void givenJFRRecIsEnabled_whenVThreadIsBlocked_thenDetectVThreadPinned() throws Exception { + Path classDir = Paths.get(CustomClassLoader.class.getProtectionDomain() + .getCodeSource() + .getLocation() + .toURI()); + + CustomClassLoader loader = new CustomClassLoader(classDir); + Path file = Path.of("pinning_3.jfr"); + + try (Recording recording = new Recording()) { + recording.enable("jdk.VirtualThreadPinned") + .withThreshold(Duration.ofMillis(1)); + recording.start(); + + Thread th1 = Thread.ofVirtual() + .start(() -> { + try { + Class clazz = Class.forName("com.baeldung.virtualthread.classloader.MyClass", + true, loader); + + System.out.println(Thread.currentThread() + " loaded class : " + clazz.getName()); + } catch (Exception ex) { + ex.printStackTrace(); + } + }); + + th1.join(); + + recording.stop(); + recording.dump(file); + } + + try (RecordingFile rf = new RecordingFile(file)) { + assertTrue(rf.hasMoreEvents()); + + while (rf.hasMoreEvents()) { + RecordedEvent event = rf.readEvent(); + + assertEquals("jdk.VirtualThreadPinned", event.getEventType() + .getName()); + assertEquals("Virtual Thread Pinned", event.getEventType() + .getLabel()); + } + } + + Files.delete(file); + } +} diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/synchronize/CartServiceTest.java b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/synchronize/CartServiceTest.java new file mode 100644 index 000000000000..554126c416df --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/synchronize/CartServiceTest.java @@ -0,0 +1,84 @@ +package com.baeldung.virtualthread.synchronize; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordingFile; + +public class CartServiceTest { + + private final CartService cartService = new CartService(); + + @Test + void givenJFRRecIsEnabled_whenVThreadIsBlocked_thenDetectVThreadPinned() throws Exception { + Path file = Path.of("pinning_4.jfr"); + + try (Recording recording = new Recording()) { + recording.enable("jdk.VirtualThreadPinned") + .withThreshold(Duration.ofMillis(1)); + recording.start(); + + Thread th = Thread.ofVirtual().start(() -> + cartService.update("test1", 2)); + + th.join(); + + recording.stop(); + recording.dump(file); + } + + try (RecordingFile rf = new RecordingFile(file)) { + assertTrue(rf.hasMoreEvents()); + + while (rf.hasMoreEvents()) { + RecordedEvent event = rf.readEvent(); + + System.out.println(event); + assertEquals("jdk.VirtualThreadPinned", event.getEventType().getName()); + assertEquals("Virtual Thread Pinned", event.getEventType().getLabel()); + } + } + + Files.delete(file); + } + + @Test + void givenProductsIsPresent_whenProductIsAdded_thenUpdate() throws InterruptedException { + String productId = "test2"; + Thread th1 = Thread.ofVirtual().start(() -> + cartService.update(productId, 2)); + + Thread th2 = Thread.ofVirtual().start(() -> + cartService.update(productId, 3)); + + th1.join(); + th2.join(); + + Map products = cartService.getProducts(); + + assertTrue(products.containsKey(productId)); + assertEquals(5, products.get(productId)); + } + + @Test + void givenProductIsNotPresent_whenProductIsAdded_thenUpdate() throws InterruptedException { + String productId = "test3"; + Thread th = Thread.ofVirtual().start(() -> + cartService.update(productId, 2)); + th.join(); + + Map products = cartService.getProducts(); + + assertTrue(products.containsKey(productId)); + assertEquals(2, products.get(productId)); + } +} diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/synchronize/fixed/CartServiceTest.java b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/synchronize/fixed/CartServiceTest.java new file mode 100644 index 000000000000..8aa410601b58 --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/synchronize/fixed/CartServiceTest.java @@ -0,0 +1,80 @@ +package com.baeldung.virtualthread.synchronize.fixed; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordingFile; + +public class CartServiceTest { + + private final CartService cartService = new CartService(); + + @Test + void givenJFRIsEnabled_whenVThreadIsBlocked_thenDetectVirtualThreadPinned() throws Exception { + Path file = Path.of("no-pinning.jfr"); + + try (Recording recording = new Recording()) { + recording.enable("jdk.VirtualThreadPinned") + .withThreshold(Duration.ofMillis(1)); + recording.start(); + + Thread th1 = Thread.ofVirtual().start(() -> + cartService.update("test1", 2)); + + Thread th2 = Thread.ofVirtual().start(() -> + cartService.update("test1", 3)); + + th1.join(); + th2.join(); + + recording.stop(); + recording.dump(file); + } + + try (RecordingFile rf = new RecordingFile(file)) { + assertFalse(rf.hasMoreEvents()); + } + + Files.delete(file); + } + + @Test + void givenProductsIsPresent_whenProductIsAdded_thenUpdate() throws InterruptedException { + String productId = "test4"; + Thread th1 = Thread.ofVirtual().start(() -> + cartService.update(productId, 2)); + + Thread th2 = Thread.ofVirtual().start(() -> + cartService.update(productId, 3)); + + th1.join(); + th2.join(); + + Map products = cartService.getProducts(); + + assertTrue(products.containsKey(productId)); + assertEquals(5, products.get(productId)); + } + + @Test + void givenProductIsNotPresent_whenProductIsAdded_thenUpdate() throws InterruptedException { + String productId = "test5"; + Thread th = Thread.ofVirtual().start(() -> + cartService.update(productId, 2)); + th.join(); + + Map products = cartService.getProducts(); + + assertTrue(products.containsKey(productId)); + assertEquals(2, products.get(productId)); + } +} From ea2b2f6e14b64b2e3dd3250cdda9f323e686585e Mon Sep 17 00:00:00 2001 From: saikatcse03 Date: Tue, 31 Mar 2026 14:06:53 +0530 Subject: [PATCH 29/38] remove c related files and rename tests --- ...ng_virtualthread_nativemethod_NativeDemo.h | 21 ------------------- .../virtualthread/nativemethod/native-lib.c | 8 ------- .../synchronize/CartServiceTest.java | 4 ++-- .../synchronize/fixed/CartServiceTest.java | 4 ++-- 4 files changed, 4 insertions(+), 33 deletions(-) delete mode 100644 core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/nativemethod/com_baeldung_virtualthread_nativemethod_NativeDemo.h delete mode 100644 core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/nativemethod/native-lib.c diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/nativemethod/com_baeldung_virtualthread_nativemethod_NativeDemo.h b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/nativemethod/com_baeldung_virtualthread_nativemethod_NativeDemo.h deleted file mode 100644 index 5d5fab6b1910..000000000000 --- a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/nativemethod/com_baeldung_virtualthread_nativemethod_NativeDemo.h +++ /dev/null @@ -1,21 +0,0 @@ -/* DO NOT EDIT THIS FILE - it is machine generated */ -#include -/* Header for class com_baeldung_virtualthread_nativemethod_NativeDemo */ - -#ifndef _Included_com_baeldung_virtualthread_nativemethod_NativeDemo -#define _Included_com_baeldung_virtualthread_nativemethod_NativeDemo -#ifdef __cplusplus -extern "C" { -#endif -/* - * Class: com_baeldung_virtualthread_nativemethod_NativeDemo - * Method: nativeCall - * Signature: ()Ljava/lang/String; - */ -JNIEXPORT jstring JNICALL Java_com_baeldung_virtualthread_nativemethod_NativeDemo_nativeCall - (JNIEnv *, jobject); - -#ifdef __cplusplus -} -#endif -#endif diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/nativemethod/native-lib.c b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/nativemethod/native-lib.c deleted file mode 100644 index 48c81aac2aa3..000000000000 --- a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/nativemethod/native-lib.c +++ /dev/null @@ -1,8 +0,0 @@ -#include -#include -#include "com_baeldung_virtualthread_nativemethod_NativeDemo.h" - -JNIEXPORT jstring JNICALL Java_com_baeldung_virtualthread_nativemethod_NativeDemo_nativeCall(JNIEnv *env, jobject obj) { - sleep(3); - return (*env)->NewStringUTF(env, "Return from native code!"); -} \ No newline at end of file diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/synchronize/CartServiceTest.java b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/synchronize/CartServiceTest.java index 554126c416df..84e1d29568f4 100644 --- a/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/synchronize/CartServiceTest.java +++ b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/synchronize/CartServiceTest.java @@ -52,7 +52,7 @@ void givenJFRRecIsEnabled_whenVThreadIsBlocked_thenDetectVThreadPinned() throws } @Test - void givenProductsIsPresent_whenProductIsAdded_thenUpdate() throws InterruptedException { + void givenProductsIsPresent_whenProductIsAdded_thenProductIsUpdated() throws InterruptedException { String productId = "test2"; Thread th1 = Thread.ofVirtual().start(() -> cartService.update(productId, 2)); @@ -70,7 +70,7 @@ void givenProductsIsPresent_whenProductIsAdded_thenUpdate() throws InterruptedEx } @Test - void givenProductIsNotPresent_whenProductIsAdded_thenUpdate() throws InterruptedException { + void givenProductIsNotPresent_whenProductIsAdded_thenProductIsUpdated() throws InterruptedException { String productId = "test3"; Thread th = Thread.ofVirtual().start(() -> cartService.update(productId, 2)); diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/synchronize/fixed/CartServiceTest.java b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/synchronize/fixed/CartServiceTest.java index 8aa410601b58..dfc64fd0ad93 100644 --- a/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/synchronize/fixed/CartServiceTest.java +++ b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/synchronize/fixed/CartServiceTest.java @@ -48,7 +48,7 @@ void givenJFRIsEnabled_whenVThreadIsBlocked_thenDetectVirtualThreadPinned() thro } @Test - void givenProductsIsPresent_whenProductIsAdded_thenUpdate() throws InterruptedException { + void givenProductsIsPresent_whenProductIsAdded_thenProductIsUpdated() throws InterruptedException { String productId = "test4"; Thread th1 = Thread.ofVirtual().start(() -> cartService.update(productId, 2)); @@ -66,7 +66,7 @@ void givenProductsIsPresent_whenProductIsAdded_thenUpdate() throws InterruptedEx } @Test - void givenProductIsNotPresent_whenProductIsAdded_thenUpdate() throws InterruptedException { + void givenProductIsNotPresent_whenProductIsAdded_thenProductIsUpdate() throws InterruptedException { String productId = "test5"; Thread th = Thread.ofVirtual().start(() -> cartService.update(productId, 2)); From 67a3a2bc45cb9c77eb63e474873aa4b220339199 Mon Sep 17 00:00:00 2001 From: saikatcse03 Date: Tue, 31 Mar 2026 15:56:45 +0530 Subject: [PATCH 30/38] remove non required dependency --- .../core-java-concurrency-advanced-7/pom.xml | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/core-java-modules/core-java-concurrency-advanced-7/pom.xml b/core-java-modules/core-java-concurrency-advanced-7/pom.xml index db709df867c3..5ef47a3a1792 100644 --- a/core-java-modules/core-java-concurrency-advanced-7/pom.xml +++ b/core-java-modules/core-java-concurrency-advanced-7/pom.xml @@ -61,24 +61,6 @@ --enable-preview - - org.apache.maven.plugins - maven-shade-plugin - - - package - shade - - benchmarks - - - org.openjdk.jmh.Main - - - - - - From 9dcaba590b24948daa183ec60acad08ec2a0912f Mon Sep 17 00:00:00 2001 From: saikatcse03 Date: Tue, 31 Mar 2026 17:19:45 +0530 Subject: [PATCH 31/38] rename variable --- .../com/baeldung/virtualthread/classinit/HeavyClassTest.java | 4 ++-- .../virtualthread/classloader/CustomClassLoaderTest.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classinit/HeavyClassTest.java b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classinit/HeavyClassTest.java index ac8e0d283404..539b2c4eeee9 100644 --- a/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classinit/HeavyClassTest.java +++ b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classinit/HeavyClassTest.java @@ -25,9 +25,9 @@ void givenJFRIsEnabled_whenVThreadIsBlocked_thenDetectVThreadPinned() throws IOE .withThreshold(Duration.ofMillis(1)); recording.start(); - Thread th1 = Thread.ofVirtual() + Thread th = Thread.ofVirtual() .start(HeavyClass::new); - th1.join(); + th.join(); recording.stop(); recording.dump(file); diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classloader/CustomClassLoaderTest.java b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classloader/CustomClassLoaderTest.java index 6fdc7bda9d52..60b72f0cc230 100644 --- a/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classloader/CustomClassLoaderTest.java +++ b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classloader/CustomClassLoaderTest.java @@ -31,7 +31,7 @@ void givenJFRRecIsEnabled_whenVThreadIsBlocked_thenDetectVThreadPinned() throws .withThreshold(Duration.ofMillis(1)); recording.start(); - Thread th1 = Thread.ofVirtual() + Thread th = Thread.ofVirtual() .start(() -> { try { Class clazz = Class.forName("com.baeldung.virtualthread.classloader.MyClass", @@ -43,7 +43,7 @@ void givenJFRRecIsEnabled_whenVThreadIsBlocked_thenDetectVThreadPinned() throws } }); - th1.join(); + th.join(); recording.stop(); recording.dump(file); From ebb64305f51bb353ea06580028ddae406c022785 Mon Sep 17 00:00:00 2001 From: saikatcse03 Date: Tue, 31 Mar 2026 17:21:05 +0530 Subject: [PATCH 32/38] rename variable --- .../virtualthread/classloader/CustomClassLoaderTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classloader/CustomClassLoaderTest.java b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classloader/CustomClassLoaderTest.java index 60b72f0cc230..a023d6f55051 100644 --- a/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classloader/CustomClassLoaderTest.java +++ b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classloader/CustomClassLoaderTest.java @@ -38,8 +38,8 @@ void givenJFRRecIsEnabled_whenVThreadIsBlocked_thenDetectVThreadPinned() throws true, loader); System.out.println(Thread.currentThread() + " loaded class : " + clazz.getName()); - } catch (Exception ex) { - ex.printStackTrace(); + } catch (ClassNotFoundException ex) { + throw new RuntimeException(ex); } }); From 8a436e762272a7f6b8c765a6e4fe88a42bdd6d1a Mon Sep 17 00:00:00 2001 From: saikatcse03 Date: Tue, 31 Mar 2026 17:27:05 +0530 Subject: [PATCH 33/38] include maven-shade-plugin --- .../core-java-concurrency-advanced-7/pom.xml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core-java-modules/core-java-concurrency-advanced-7/pom.xml b/core-java-modules/core-java-concurrency-advanced-7/pom.xml index 5ef47a3a1792..db709df867c3 100644 --- a/core-java-modules/core-java-concurrency-advanced-7/pom.xml +++ b/core-java-modules/core-java-concurrency-advanced-7/pom.xml @@ -61,6 +61,24 @@ --enable-preview + + org.apache.maven.plugins + maven-shade-plugin + + + package + shade + + benchmarks + + + org.openjdk.jmh.Main + + + + + + From 046055dfd79628266556c0ce44b56cade00db96c Mon Sep 17 00:00:00 2001 From: saikatcse03 Date: Tue, 31 Mar 2026 17:31:47 +0530 Subject: [PATCH 34/38] remove unused property --- core-java-modules/core-java-concurrency-advanced-7/pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/core-java-modules/core-java-concurrency-advanced-7/pom.xml b/core-java-modules/core-java-concurrency-advanced-7/pom.xml index db709df867c3..3a178f33ece0 100644 --- a/core-java-modules/core-java-concurrency-advanced-7/pom.xml +++ b/core-java-modules/core-java-concurrency-advanced-7/pom.xml @@ -86,7 +86,6 @@ 1.7.0 21 21 - 3.5.0 1.37 From e8f13da326930ec29d7dba01dbe1f1a470acc432 Mon Sep 17 00:00:00 2001 From: saikatcse03 Date: Tue, 31 Mar 2026 17:34:32 +0530 Subject: [PATCH 35/38] update pom file with unused var removal --- core-java-modules/core-java-concurrency-advanced-7/pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/core-java-modules/core-java-concurrency-advanced-7/pom.xml b/core-java-modules/core-java-concurrency-advanced-7/pom.xml index 3a178f33ece0..0d8557a69133 100644 --- a/core-java-modules/core-java-concurrency-advanced-7/pom.xml +++ b/core-java-modules/core-java-concurrency-advanced-7/pom.xml @@ -56,7 +56,6 @@ org.apache.maven.plugins maven-surefire-plugin - ${maven-surefire-plugin.version} --enable-preview From deb50df8e37cef5e8e5db52577aa035289b15e23 Mon Sep 17 00:00:00 2001 From: saikatcse03 Date: Tue, 21 Apr 2026 12:27:17 +0530 Subject: [PATCH 36/38] safe unlock handling --- .../virtualthread/synchronize/fixed/CartService.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/fixed/CartService.java b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/fixed/CartService.java index 754b0abd236d..7bf68808c61b 100644 --- a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/fixed/CartService.java +++ b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/fixed/CartService.java @@ -26,11 +26,13 @@ public void update(String productId, int quantity) { try { if (lock.tryLock(500, TimeUnit.MILLISECONDS)) { - simulateAPI(); - products.merge(productId, quantity, Integer::sum); - - LOGGER.info("Updated Cart for {} {}", productId, quantity); - lock.unlock(); + try { + simulateAPI(); + products.merge(productId, quantity, Integer::sum); + LOGGER.info("Updated Cart for {} {}", productId, quantity); + } finally{ + lock.unlock(); + } } } catch (InterruptedException e) { throw new RuntimeException(e); From fbcc2c5be2938bd416e5f05e91c136732dbba53b Mon Sep 17 00:00:00 2001 From: saikatcse03 Date: Tue, 21 Apr 2026 14:22:57 +0530 Subject: [PATCH 37/38] rename variable --- .../virtualthread/synchronize/fixed/CartService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/fixed/CartService.java b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/fixed/CartService.java index 7bf68808c61b..e0c3dad9616f 100644 --- a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/fixed/CartService.java +++ b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/fixed/CartService.java @@ -34,16 +34,16 @@ public void update(String productId, int quantity) { lock.unlock(); } } - } catch (InterruptedException e) { - throw new RuntimeException(e); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); } } private void simulateAPI() { try { Thread.sleep(50); - } catch (InterruptedException e) { - throw new RuntimeException(e); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); } } From 706bf908d94c0784a56ddff724d34849631b40ca Mon Sep 17 00:00:00 2001 From: saikatcse03 Date: Tue, 21 Apr 2026 15:55:22 +0530 Subject: [PATCH 38/38] refactor method --- .../baeldung/virtualthread/synchronize/fixed/CartService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/fixed/CartService.java b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/fixed/CartService.java index e0c3dad9616f..a252e8ca9fc1 100644 --- a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/fixed/CartService.java +++ b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/fixed/CartService.java @@ -29,10 +29,10 @@ public void update(String productId, int quantity) { try { simulateAPI(); products.merge(productId, quantity, Integer::sum); - LOGGER.info("Updated Cart for {} {}", productId, quantity); - } finally{ + } finally { lock.unlock(); } + LOGGER.info("Updated Cart for {} {}", productId, quantity); } } catch (InterruptedException ex) { throw new RuntimeException(ex);