5

I have a springboot application that ingests an application.properties via @ConfigurationProperties and @PropertySource("classpath:application.properties"). My desire is to reload these properties on the fly for the purposes of support. When I POST to http://localhost:8080/actuator/refresh I get a 200 OK response but the body is empty, which I think implies no @RefreshScopes have been refreshed but I'm not sure. I have read and viewed most docs and SO while trying various things but haven't managed to reload a new value.

Here is my app:

application.properties:

# suppress inspection "UnusedProperty" for whole file
# the name of Camel
camel.springboot.name = IntegrationsCamel

# how often to trigger the timer (millis)
myPeriod = 2000

spring.main.allow-bean-definition-overriding=true

# to turn off Camel info in (/actuator/info)
management.info.camel.enabled=false

# to configure logging levels
logging.level.org.springframework = INFO
logging.level.org.apache.camel.spring.boot = INFO
logging.level.org.apache.camel.impl = DEBUG
logging.level.sample.camel = DEBUG

# Environment
integration.env = DEV
spring.application.name = integration
spring.config.import=aws-parameterstore:

# Client API Auth -- Set by Instance
integration.client.auth.key = tempclientkey
integration.client.auth.secret = tempclientsecret

# Client API Credentials -- Set by Instance
integration.client.endpoint = https://127.0.0.2
integration.client.port = 6060

# AWS Paramstore Config
aws.paramstore.enabled=true
aws.paramstore.prefix=/integration
aws.paramstore.defaultContext=application
aws.paramstore.profile-separator=_

# AWS OAuth
cloud.aws.credentials.instance-profile=true
cloud.aws.credentials.profile-name=default
cloud.aws.credentials.access-key=${AWS_ACCESS_KEY_ID}
cloud.aws.credentials.secret-key=${AWS_SECRET_ACCESS_KEY}

# Non EC2 vars (remove when running live)
cloud.aws.stack.auto=false
cloud.aws.region.static=eu-west-1
AWS_EC2_METADATA_DISABLED=true
logging.level.com.amazonaws.util.EC2MetadataUtils=error
logging.level.com.amazonaws.internal.InstanceMetadataServiceResourceFetcher=error

# DB Vars
spring.datasource.driver-class-name=com.microsoft.sqlserver.jdbc.SQLServerDriver
spring.datasource.url=jdbc:sqlserver://${RDS_HOSTNAME:my-server-domain.com}:${RDS_PORT:1337};databaseName=${RDS_DB_NAME:MYDB}
spring.datasource.username=${RDS_USERNAME:readuser}
spring.datasource.password=${RDS_PASSWORD:${read_pass}}
spring.jpa.properties.hibernate.default_schema=dbo

# Actuator Management
management.endpoints.enabled-by-default=false
management.endpoint.refresh.enabled=true
management.endpoints.web.exposure.include=refresh

All of these are successfully pulled in to the bean

package com.Integration.Configuration;

import lombok.Getter;
import lombok.Setter;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.event.EventListener;

@Getter
@Setter

@RefreshScope
@Configuration
public class ApplicationProperties {

    @Value("${integration.env}")
    private String env;

    @Value("${spring.datasource.url}")
    private String DB_URL;

    @Value("${rds-username}")
    private String DB_USERNAME;

    @Value("${rds-password}")
    private String DB_ENCRYPTED_PASSWORD;

    @Value("${oauth2.key}")
    private String OAuth2Key;

    @Value("${oauth2.secret}")
    private String OAuth2Secret;

    @Value("${integration.client.endpoint}")
    private String ClientAPIEndpoint;

    @Value("${integration.client.port}")
    private String ClientAPIPort;

    @Value("${integration.client.auth.key}")
    private String ClientAPIKey;

    @Value("${integration.client.auth.secret}")
    private String ClientAPISecret;

    public ApplicationProperties() {}

    @SuppressWarnings("unused")
    @EventListener(RefreshScopeRefreshedEvent.class)
    public void onRefresh(RefreshScopeRefreshedEvent event) {
        // todo: Bug-#01 @RefreshScope actuator/refresh not updating @Values
        System.out.println(this.ClientAPIEndpoint);
    }

}

The event listener fires without issue too and it will successfully print the value from application.properties. I have debugged main below and all values are populated without issue. I suspect the issue is here but I'm not sure what I'm doing wrong in main()

package com.Integration;

import com.Integration.AutoLoader.RouteBuilderAutoLoader;
import com.Integration.Configuration.ApplicationProperties;

import com.Integration.Scheduler.TasksScheduler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

import org.springframework.context.ConfigurableApplicationContext;

import org.apache.camel.CamelContext;
import org.apache.camel.impl.DefaultCamelContext;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

import java.util.Objects;

@SpringBootApplication
@RefreshScope
@EnableScheduling
public class Integration {

    public static String SERVER_ADDR;

    protected static CamelContext context;

    public static void main(String[] args) throws Exception {
        // Spring Boot App Entry
        ConfigurableApplicationContext appContext = SpringApplication.run(Integration.class, args);
        // Get ApplicationProperties
        ApplicationProperties properties = appContext.getBean(ApplicationProperties.class);
        SERVER_ADDR = (Objects.equals(properties.getEnv(), "DEV")) ? "0.0.0.0" : "localhost";
        // Start Camel Context Engine
        context = new DefaultCamelContext();
        // Feed Camel our defined routes & start
        RouteBuilderAutoLoader.loadRoutes(context);
        context.start();
    }
}

The last thing I'm unsure of is if I have the necessary dependencies or if I'm missing something here.

<?xml version="1.0" encoding="UTF-8"?>
<!--suppress MavenPackageUpdate -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.Integration</groupId>
    <artifactId>Integration</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>Integration</name>
    <description>Generic API Integration App</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
            <version>2.6.5</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
            <version>2.6.5</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>5.2.4.Final</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.21</version>
        </dependency>

        <dependency>
            <groupId>com.microsoft.sqlserver</groupId>
            <artifactId>mssql-jdbc</artifactId>
            <version>9.4.1.jre8</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>2021.0.1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <!-- AWS/Cloud -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-aws</artifactId>
            <version>2.2.6.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>io.awspring.cloud</groupId>
            <artifactId>spring-cloud-aws-context</artifactId>
            <version>2.4.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
            <version>3.1.1</version>
        </dependency>

        <dependency>
            <groupId>io.awspring.cloud</groupId>
            <artifactId>spring-cloud-starter-aws-parameter-store-config</artifactId>
            <version>2.4.0</version>
        </dependency>

        <!-- Camel 3.14.1 Latest LTS version -->
        <dependency>
            <groupId>org.apache.camel.springboot</groupId>
            <artifactId>camel-spring-boot-bom</artifactId>
            <version>3.14.1</version>
            <type>pom</type>
        </dependency>
        <dependency>
            <groupId>org.apache.camel</groupId>
            <artifactId>camel-core</artifactId>
            <version>3.14.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.camel</groupId>
            <artifactId>camel-core-languages</artifactId>
            <version>3.14.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.camel</groupId>
            <artifactId>camel-bean</artifactId>
            <version>3.14.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.camel</groupId>
            <artifactId>camel-rest</artifactId>
            <version>3.14.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.camel</groupId>
            <artifactId>camel-direct</artifactId>
            <version>3.14.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.camel</groupId>
            <artifactId>camel-netty-http</artifactId>
            <version>3.14.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.camel</groupId>
            <artifactId>camel-platform-http</artifactId>
            <version>3.14.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.camel</groupId>
            <artifactId>camel-management</artifactId>
            <version>3.14.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.camel</groupId>
            <artifactId>camel-endpointdsl</artifactId>
            <version>3.14.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.activemq</groupId>
            <artifactId>activemq-spring</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jetbrains</groupId>
            <artifactId>annotations</artifactId>
            <version>RELEASE</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-inline</artifactId>
            <version>4.3.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.1.Final</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>


</project>

Edit 12/04/2022

Completely redid my classes with @ConfigurationProperties

So when I post to refresh the values in propertySource for AWS automatically update. So eg I change cobaltdlt to cobaltdlttest below, hit refresh, and then I can see the change when I query GET http://localhost:8080/actuator/env

So the refresh is working but it's not reflective in the @Component beans even though the propertySource is updating. In fact the only beans it is working with are the AWS beans.

Postman

I2obiN
  • 181
  • 3
  • 18
  • Why have you mixed `@ConfigurationProperties` and `@Value`? The two are separate mechanisms for binding properties to a class. You also don't need to declare `application.properties` as a property source. It's a Spring Boot default so it'll work out of the box. – Andy Wilkinson Apr 08 '22 at 20:13
  • https://www.baeldung.com/spring-value-annotation "Naturally, we'll need a properties file to define the values we want to inject with the ```@Value``` annotation. And so, we'll first need to define a ```@PropertySource``` in our configuration class — with the properties file name." that gave me the impression they were to be used together. So I don't need to use ```@Value```s? – I2obiN Apr 08 '22 at 22:41
  • 1
    You should use either `@Value` or `@ConfigurationProperties`, not both. In either case, as you are using Spring Boot, you don’t need `@PropertySource` for `application.properties`. If you decide to use `@ConfiigurationProperties` you should read https://docs.spring.io/spring-boot/docs/2.6.x/reference/htmlsingle/#features.external-config.typesafe-configuration-properties first and modify your code accordingly – Andy Wilkinson Apr 09 '22 at 05:46
  • Okay thank you. I have modified and retested in both question and code. Unfortunately I still get an empty body when I post to the actuator/refresh and the value remains unchanged after modification on the fly when printed from either main or the refresh listener – I2obiN Apr 09 '22 at 17:25
  • Have tried this both way and neither have worked. Completely stuck at this point – I2obiN Apr 11 '22 at 10:46
  • After rewriting this with just use of the @ConfigurationProperties I have this successfully refreshing the AWS parameter store values, but bizarrely the file properties remain unchanged. I fear I will never solve this and my solution will simply have to be redesigned to use the AWS parameter store entirely. So for anyone looking for a solution and happen to be using AWS, just use the parameter store. :/ – I2obiN Apr 13 '22 at 09:27

3 Answers3

2

Have you tried a PropertiesConfiguration bean? See here: https://www.baeldung.com/spring-reloading-properties.

Once configured correctly, it should automatically read changes to your properties file and reload them. Also, there are some limitations that are mentioned in that article that you should be aware of.

Terry Sposato
  • 572
  • 2
  • 7
  • Trying this but doesn't seem to be working if I declare it as a @Bean, do i need to instantiate in main maybe? Will experiment further – I2obiN Apr 15 '22 at 22:41
1

@I2obiN. As you have mentioned that ApplicationProperties is getting refreshed by Refresh Scopes, therefore actuator refresh is working properly.

But I noticed that you are using RefreshScope on main application class. IMO this actuator management/refresh will not restart application. You can try to create an extra method that reinitializes Camel Context. Hope this helps!!

https://www.baeldung.com/java-restart-spring-boot-app#actuators-restart-endpoint

Chetan
  • 11
  • 2
  • Thanks but it is only the AWS parameter store that is getting refreshed. The values in my config file are still unchanged. – I2obiN Apr 13 '22 at 09:24
1

You can invoke the refresh Actuator endpoint by sending an empty HTTP POST to the client's refresh endpoint: http://localhost:8080/actuator/refresh . Then you can confirm it worked by visiting the http://localhost:8080/message endpoint.

And once you refresh, try to rebind immediately by calling the POST endpoint http://localhost:8080/actuator/env This worked on a spring boot I tried and it reloads the values and you can see it in the stack trace.

I tried doing a field in spring data source with the given payload to Rebind and it reloads Example as the value.

Payload:

{"name":"spring.datasource.userName", "value":"Example"}

In the application.properties, add the following

management.endpoint.env.post.enabled=true
management.endpoint.restart.enabled=true
endpoints.sensitive=true
endpoints.actuator.enabled=true
management.security.enabled=false 
management.endpoints.web.exposure.include=*
Roe hit
  • 457
  • 1
  • 7
  • 23
  • So message 404s for me. Not sure if I have to enable that endpoint or if it should be there by default. What do you mean by payload? payload to what endpoint? – I2obiN Apr 17 '22 at 00:24
  • 1
    Okay. Can you add the following as well to the application.properties so that spring boot can detect the management endpoints like refresh and rebind and try? (Edited my answer). By Payload I mean the payload for the rebind endpoint which you can hit your spring boot by calling http://localhost:8080/actuator/env using POST which requires a body right.. – Roe hit Apr 17 '22 at 01:09
  • Okay so the rebind works. However the issue is I need to detect the change to the application.properties file. In other words. In testing I can POST that payload to the env but how would i detect that change normally? – I2obiN Apr 17 '22 at 02:26
  • The change to the application.properties needs to be through the rebind. You can have thatother application that's triggering a change to the application.properties of the application we're talking about, send a rebind and you can monitor the stack trace of this application you're talking about and you can see that it will being to restart the configuration load and based on the new application.properties change. Try to rebind a user name or something using a rebind while having your application running, and you can see that once you do the rebind, the application reloads the configuration – Roe hit Apr 17 '22 at 06:11
  • Okay. So using the PropertiesConfiguration bean mentioned in above answer I will need to reload the properties file, detect the change from that as a json string and then post it to the env? – I2obiN Apr 19 '22 at 09:27
  • 1
    yes. That's right. You can confirm that the reload happened through the stack trace – Roe hit Apr 20 '22 at 11:37
  • 1
    https://stackoverflow.com/questions/71150090/is-there-a-way-to-increase-the-token-expiry-time-for-aurorapostgres-rds-database This is another question where a partial solution was to refresh and rebind the token which I put in. But there's the issue of token that remains. For your case, the refresh and rebind will solve the issue. – Roe hit Apr 20 '22 at 11:40