I2obiN
I2obiN

Reputation: 309

Springboot Actuator Refresh does not reload @Value properties in @ConfigurationProperties

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

Upvotes: 7

Views: 20137

Answers (3)

Roe hit
Roe hit

Reputation: 467

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=*

Upvotes: 2

Terry Sposato
Terry Sposato

Reputation: 642

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.

Upvotes: 2

Chetan
Chetan

Reputation: 21

@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

Upvotes: 2

Related Questions