diff --git a/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/handlerinterceptor/DeliveryChargeInterceptor.java b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/handlerinterceptor/DeliveryChargeInterceptor.java new file mode 100644 index 000000000000..7978ac2aa941 --- /dev/null +++ b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/handlerinterceptor/DeliveryChargeInterceptor.java @@ -0,0 +1,37 @@ +package com.baeldung.handlerinterceptor; + +import java.security.SecureRandom; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import org.springframework.lang.NonNull; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class DeliveryChargeInterceptor implements HandlerInterceptor { + + static final String USE_V2_ATTRIBUTE = "useV2"; + + private final FeatureFlagService featureFlagService; + private final SecureRandom random = new SecureRandom(); + private final Logger logger = LoggerFactory.getLogger(DeliveryChargeInterceptor.class); + + public DeliveryChargeInterceptor(FeatureFlagService featureFlagService) { + this.featureFlagService = featureFlagService; + } + + @Override + public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) { + if (handler instanceof HandlerMethod) { + int rollout = featureFlagService.rolloutPercentage(); + boolean useV2 = rollout > 0 && random.nextInt(100) < rollout; + request.setAttribute(USE_V2_ATTRIBUTE, useV2); + logger.info("Delivery charge feature: rollout={}%, useV2={}", rollout, useV2); + } + return true; + } +} diff --git a/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/handlerinterceptor/DeliveryChargeService.java b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/handlerinterceptor/DeliveryChargeService.java new file mode 100644 index 000000000000..04e8afea42ee --- /dev/null +++ b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/handlerinterceptor/DeliveryChargeService.java @@ -0,0 +1,9 @@ +package com.baeldung.handlerinterceptor; + +public interface DeliveryChargeService { + + double calculateV1(String postCode); + + double calculateV2(String postCode); + +} diff --git a/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/handlerinterceptor/DeliveryChargesController.java b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/handlerinterceptor/DeliveryChargesController.java new file mode 100644 index 000000000000..437306534058 --- /dev/null +++ b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/handlerinterceptor/DeliveryChargesController.java @@ -0,0 +1,26 @@ +package com.baeldung.handlerinterceptor; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.servlet.http.HttpServletRequest; + +@RestController +public class DeliveryChargesController { + + private final DeliveryChargeService deliveryChargeService; + + public DeliveryChargesController(DeliveryChargeService deliveryChargeService) { + this.deliveryChargeService = deliveryChargeService; + } + + @PostMapping("/delivery-charges/calculate") + public double calculate(@RequestParam String postcode, HttpServletRequest request) { + Boolean useV2 = (Boolean) request.getAttribute(DeliveryChargeInterceptor.USE_V2_ATTRIBUTE); + if (Boolean.TRUE.equals(useV2)) { + return deliveryChargeService.calculateV2(postcode); + } + return deliveryChargeService.calculateV1(postcode); + } +} diff --git a/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/handlerinterceptor/FeatureFlagService.java b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/handlerinterceptor/FeatureFlagService.java new file mode 100644 index 000000000000..54656521aead --- /dev/null +++ b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/handlerinterceptor/FeatureFlagService.java @@ -0,0 +1,5 @@ +package com.baeldung.handlerinterceptor; + +public interface FeatureFlagService { + int rolloutPercentage(); +} diff --git a/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/handlerinterceptor/HandlerInterceptorApp.java b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/handlerinterceptor/HandlerInterceptorApp.java new file mode 100644 index 000000000000..5d254e0c393d --- /dev/null +++ b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/handlerinterceptor/HandlerInterceptorApp.java @@ -0,0 +1,11 @@ +package com.baeldung.handlerinterceptor; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication(scanBasePackages = "com.baeldung.handlerinterceptor") +public class HandlerInterceptorApp { + public static void main(String[] args) { + SpringApplication.run(HandlerInterceptorApp.class, args); + } +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/handlerinterceptor/WebMvcConfig.java b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/handlerinterceptor/WebMvcConfig.java new file mode 100644 index 000000000000..06709cda097b --- /dev/null +++ b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/handlerinterceptor/WebMvcConfig.java @@ -0,0 +1,21 @@ +package com.baeldung.handlerinterceptor; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + private final FeatureFlagService featureFlagService; + + public WebMvcConfig(FeatureFlagService featureFlagService) { + this.featureFlagService = featureFlagService; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new DeliveryChargeInterceptor(featureFlagService)) + .addPathPatterns("/delivery-charges/**"); + } +} diff --git a/spring-boot-modules/spring-boot-mvc-5/src/test/java/com/baeldung/handlerinterceptor/DeliveryChargeInterceptorUnitTest.java b/spring-boot-modules/spring-boot-mvc-5/src/test/java/com/baeldung/handlerinterceptor/DeliveryChargeInterceptorUnitTest.java new file mode 100644 index 000000000000..f0f2fd69a48d --- /dev/null +++ b/spring-boot-modules/spring-boot-mvc-5/src/test/java/com/baeldung/handlerinterceptor/DeliveryChargeInterceptorUnitTest.java @@ -0,0 +1,72 @@ +package com.baeldung.handlerinterceptor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(controllers = DeliveryChargesController.class) +class DeliveryChargeInterceptorUnitTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private DeliveryChargeService deliveryChargeService; + + @MockBean + private FeatureFlagService featureFlagService; + + @Test + void givenZeroRollout_whenCalculateDeliveryCharge_thenV1IsUsed() throws Exception { + when(featureFlagService.rolloutPercentage()).thenReturn(0); + when(deliveryChargeService.calculateV1("SW1A")).thenReturn(5.0); + + mockMvc.perform(post("/delivery-charges/calculate").param("postcode", "SW1A")) + .andExpect(status().isOk()) + .andExpect(content().string("5.0")); + } + + @Test + void givenFullRollout_whenCalculateDeliveryCharge_thenV2IsUsed() throws Exception { + when(featureFlagService.rolloutPercentage()).thenReturn(100); + when(deliveryChargeService.calculateV2("SW1A")).thenReturn(3.5); + + mockMvc.perform(post("/delivery-charges/calculate").param("postcode", "SW1A")) + .andExpect(status().isOk()) + .andExpect(content().string("3.5")); + } + + @Test + void givenPartialRollout_whenCalculateDeliveryCharge_thenBothVersionsAreUsed() throws Exception { + when(featureFlagService.rolloutPercentage()).thenReturn(50); + when(deliveryChargeService.calculateV1("SW1A")).thenReturn(5.0); + when(deliveryChargeService.calculateV2("SW1A")).thenReturn(3.5); + + int v1Count = 0; + int v2Count = 0; + + for (int i = 0; i < 20; i++) { + String response = mockMvc.perform(post("/delivery-charges/calculate").param("postcode", "SW1A")) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + if ("5.0".equals(response)) { + v1Count++; + } else if ("3.5".equals(response)) { + v2Count++; + } + } + + assertThat(v1Count).isGreaterThan(0); + assertThat(v2Count).isGreaterThan(0); + } +}