diff --git a/spring-boot-modules/spring-boot-3-3/pom.xml b/spring-boot-modules/spring-boot-3-3/pom.xml index c6f43a93c522..5ec0caf9b35b 100644 --- a/spring-boot-modules/spring-boot-3-3/pom.xml +++ b/spring-boot-modules/spring-boot-3-3/pom.xml @@ -41,6 +41,25 @@ langchain4j-ollama ${langchain4j.version} + + org.springframework.cloud + spring-cloud-starter + ${spring-cloud-starter.version} + + + org.springframework.cloud + spring-cloud-starter-config + ${spring-cloud-starter.version} + + + org.springframework.boot + spring-boot-starter-actuator + + + org.awaitility + awaitility + test + @@ -65,5 +84,6 @@ 3.2.4 4.7.0 0.33.0 + 4.1.3 diff --git a/spring-boot-modules/spring-boot-3-3/src/main/java/com/baeldung/dynamically/updating/properties/UpdatingPropertiesApplication.java b/spring-boot-modules/spring-boot-3-3/src/main/java/com/baeldung/dynamically/updating/properties/UpdatingPropertiesApplication.java new file mode 100644 index 000000000000..7a5abb5d3426 --- /dev/null +++ b/spring-boot-modules/spring-boot-3-3/src/main/java/com/baeldung/dynamically/updating/properties/UpdatingPropertiesApplication.java @@ -0,0 +1,13 @@ +package com.baeldung.dynamically.updating.properties; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class UpdatingPropertiesApplication { + + public static void main(String[] args) { + SpringApplication.run(UpdatingPropertiesApplication.class, args); + } + +} diff --git a/spring-boot-modules/spring-boot-3-3/src/main/java/com/baeldung/dynamically/updating/properties/controllers/PropertyController.java b/spring-boot-modules/spring-boot-3-3/src/main/java/com/baeldung/dynamically/updating/properties/controllers/PropertyController.java new file mode 100644 index 000000000000..a3f0a24fb6ac --- /dev/null +++ b/spring-boot-modules/spring-boot-3-3/src/main/java/com/baeldung/dynamically/updating/properties/controllers/PropertyController.java @@ -0,0 +1,29 @@ +package com.baeldung.dynamically.updating.properties.controllers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import com.baeldung.dynamically.updating.properties.services.ExampleBean; +import com.baeldung.dynamically.updating.properties.services.PropertyUpdaterService; + +@RestController +@RequestMapping("/properties") +public class PropertyController { + + @Autowired + private PropertyUpdaterService propertyUpdaterService; + + @Autowired + private ExampleBean exampleBean; + + @PostMapping("/update") + public String updateProperty(@RequestParam String key, @RequestParam String value) { + propertyUpdaterService.updateProperty(key, value); + return "Property updated. Remember to call the actuator /actuator/refresh"; + } + + @GetMapping("/customProperty") + public String getCustomProperty() { + return exampleBean.getCustomProperty(); + } +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-3-3/src/main/java/com/baeldung/dynamically/updating/properties/services/ExampleBean.java b/spring-boot-modules/spring-boot-3-3/src/main/java/com/baeldung/dynamically/updating/properties/services/ExampleBean.java new file mode 100644 index 000000000000..06a0347f5b39 --- /dev/null +++ b/spring-boot-modules/spring-boot-3-3/src/main/java/com/baeldung/dynamically/updating/properties/services/ExampleBean.java @@ -0,0 +1,17 @@ +package com.baeldung.dynamically.updating.properties.services; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.stereotype.Component; + +@RefreshScope +@Component +public class ExampleBean { + + @Value("${my.custom.property}") + private String customProperty; + + public String getCustomProperty() { + return customProperty; + } +} diff --git a/spring-boot-modules/spring-boot-3-3/src/main/java/com/baeldung/dynamically/updating/properties/services/PropertyUpdaterService.java b/spring-boot-modules/spring-boot-3-3/src/main/java/com/baeldung/dynamically/updating/properties/services/PropertyUpdaterService.java new file mode 100644 index 000000000000..4157ec726598 --- /dev/null +++ b/spring-boot-modules/spring-boot-3-3/src/main/java/com/baeldung/dynamically/updating/properties/services/PropertyUpdaterService.java @@ -0,0 +1,31 @@ +package com.baeldung.dynamically.updating.properties.services; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.stereotype.Service; + +@Service +public class PropertyUpdaterService { + + private static final String DYNAMIC_PROPERTIES_SOURCE_NAME = "dynamicProperties"; + + @Autowired + private ConfigurableEnvironment environment; + + public void updateProperty(String key, String value) { + MutablePropertySources propertySources = environment.getPropertySources(); + if (!propertySources.contains(DYNAMIC_PROPERTIES_SOURCE_NAME)) { + Map dynamicProperties = new HashMap<>(); + dynamicProperties.put(key, value); + propertySources.addFirst(new MapPropertySource(DYNAMIC_PROPERTIES_SOURCE_NAME, dynamicProperties)); + } else { + MapPropertySource propertySource = (MapPropertySource) propertySources.get(DYNAMIC_PROPERTIES_SOURCE_NAME); + propertySource.getSource().put(key, value); + } + } +} diff --git a/spring-boot-modules/spring-boot-3-3/src/main/java/com/baeldung/dynamically/updating/properties/utility/CustomConfig.java b/spring-boot-modules/spring-boot-3-3/src/main/java/com/baeldung/dynamically/updating/properties/utility/CustomConfig.java new file mode 100644 index 000000000000..1f5ea52bd96d --- /dev/null +++ b/spring-boot-modules/spring-boot-3-3/src/main/java/com/baeldung/dynamically/updating/properties/utility/CustomConfig.java @@ -0,0 +1,16 @@ +package com.baeldung.dynamically.updating.properties.utility; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; + +@Configuration +public class CustomConfig { + + @Bean + @Scope("prototype") + public MyService myService(@Value("${custom.property:default}") String property) { + return new MyService(property); + } +} diff --git a/spring-boot-modules/spring-boot-3-3/src/main/java/com/baeldung/dynamically/updating/properties/utility/MyService.java b/spring-boot-modules/spring-boot-3-3/src/main/java/com/baeldung/dynamically/updating/properties/utility/MyService.java new file mode 100644 index 000000000000..9cd6c04bedc9 --- /dev/null +++ b/spring-boot-modules/spring-boot-3-3/src/main/java/com/baeldung/dynamically/updating/properties/utility/MyService.java @@ -0,0 +1,14 @@ +package com.baeldung.dynamically.updating.properties.utility; + +public class MyService { + + private final String property; + + public MyService(String property) { + this.property = property; + } + + public String getProperty() { + return property; + } +} diff --git a/spring-boot-modules/spring-boot-3-3/src/main/resources/application.properties b/spring-boot-modules/spring-boot-3-3/src/main/resources/application.properties index a2c75b9bcc65..2b310bb6a26a 100644 --- a/spring-boot-modules/spring-boot-3-3/src/main/resources/application.properties +++ b/spring-boot-modules/spring-boot-3-3/src/main/resources/application.properties @@ -12,3 +12,15 @@ ollama.max_response_length=1000 # Logging configuration logging.level.root=INFO logging.level.com.baeldung.chatbot=DEBUG +logging.level.com.baeldung.dynamically.updating.properties=DEBUG +logging.level.org.springframework.boot.actuate=DEBUG + +# We can disable Spring Cloud Config to prevent the application from trying to connect to a configuration server +spring.cloud.config.enabled=false +# Enable the Spring Boot Actuator endpoints to trigger a refresh +management.endpoint.refresh.enabled=true +management.endpoints.web.exposure.include=refresh + +# Example property to be changed at runtime +my.custom.property=defaultValue + diff --git a/spring-boot-modules/spring-boot-3-3/src/test/java/com/baeldung/dynamically/updating/properties/CustomConfigUnitTest.java b/spring-boot-modules/spring-boot-3-3/src/test/java/com/baeldung/dynamically/updating/properties/CustomConfigUnitTest.java new file mode 100644 index 000000000000..278d96e4399a --- /dev/null +++ b/spring-boot-modules/spring-boot-3-3/src/test/java/com/baeldung/dynamically/updating/properties/CustomConfigUnitTest.java @@ -0,0 +1,34 @@ +package com.baeldung.dynamically.updating.properties; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import com.baeldung.dynamically.updating.properties.utility.MyService; + +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@ExtendWith(SpringExtension.class) +@SpringBootTest(classes = UpdatingPropertiesApplication.class) +public class CustomConfigUnitTest { + + @Autowired + private ApplicationContext context; + + @Test + void whenPropertyInjected_thenServiceUsesCustomProperty() { + MyService service = context.getBean(MyService.class); + assertEquals("default", service.getProperty()); + } + + @Test + void whenPropertyChanged_thenServiceUsesUpdatedProperty() { + System.setProperty("custom.property", "updated"); + MyService service = context.getBean(MyService.class); + assertEquals("updated", service.getProperty()); + } +} diff --git a/spring-boot-modules/spring-boot-3-3/src/test/java/com/baeldung/dynamically/updating/properties/PropertyUpdaterServiceUnitTest.java b/spring-boot-modules/spring-boot-3-3/src/test/java/com/baeldung/dynamically/updating/properties/PropertyUpdaterServiceUnitTest.java new file mode 100644 index 000000000000..ec94040aa6e8 --- /dev/null +++ b/spring-boot-modules/spring-boot-3-3/src/test/java/com/baeldung/dynamically/updating/properties/PropertyUpdaterServiceUnitTest.java @@ -0,0 +1,48 @@ +package com.baeldung.dynamically.updating.properties; + +import static org.awaitility.Awaitility.await; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.web.client.RestTemplate; + +import com.baeldung.dynamically.updating.properties.services.ExampleBean; +import com.baeldung.dynamically.updating.properties.services.PropertyUpdaterService; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class PropertyUpdaterServiceUnitTest { + + @Autowired + private PropertyUpdaterService propertyUpdaterService; + + @Autowired + private ExampleBean exampleBean; + + @LocalServerPort + private int port; + + @Test + @Timeout(5) + public void whenUpdatingProperty_thenPropertyIsUpdatedAndRefreshed() throws InterruptedException { + // Injects a new property into the test context + propertyUpdaterService.updateProperty("my.custom.property", "newValue"); + + // Trigger the refresh by calling the actuator endpoint + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity<>(null, headers); + RestTemplate restTemplate = new RestTemplate(); + restTemplate.postForEntity("http://localhost:" + port + "/actuator/refresh", entity, String.class); + + // Awaitility to wait until the property is updated + await().atMost(5, TimeUnit.SECONDS).until(() -> "newValue".equals(exampleBean.getCustomProperty())); + } +}