Backwards compatibility when changing Spring Boot externalized configuration

1.7k Views Asked by At

Is there a recommended way to introduce restructurings/renamings into an externalized configuration while keeping backwards compatibility for consumers still relying on the old configuration structure?

For example, given a library used the following configuration structure defined via @ConfigurationProperties in the past:

old-properties:
  an:
    old-property: true
  another:
    custom-property: 1234

A new version of that library redefines the configuration to something like this:

my-library:
  a-property: true
  another-property: 1234

Is there a good way to deprecate the old structure while keeping compatibility for existing consumers for some time? Consumers using the new version of the library should still be able to use old-properties.an.old-property and have that automatically mapped to my-library.a-property.

I'm aware of the functionality to use additional configuration metadata to mark a property as deprecated, but I'm explicitly looking for a way to support both versions to ease migration.

2

There are 2 best solutions below

0
On BEST ANSWER

I looked into how Spring Boot handled the deprecation phase for logging.file (which was replaced by logging.file.name) and as they implemented the fallback directly in code I decided to try something similar by creating the new @ConfigurationProperties in a @Bean method which handles setting the values from old property names if available.

Given the new configuration structure looks like this (using Lombok for brevity):

import lombok.Data;

@Data
public class MyLibraryConfigurationProperties {

    private String aProperty;
    private String anotherProperty;

}

The @Bean method now takes care of reading the old value and applying it to the properties:

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

@Configuration
public class MyLibraryConfiguration {

    @Bean
    @ConfigurationProperties(prefix = "my-library")
    public MyLibraryConfigurationProperties myLibraryConfigurationProperties(Environment environment) {
        MyLibraryConfigurationProperties config = new MyLibraryConfigurationProperties();

        // fallback to old property if available
        if (environment.containsProperty("old-properties.an.old-property")) {
            // here we could also log warnings regarding the deprecation
            config.setAProperty(environment.getProperty("old-properties.an.old-property"));
        }

        return config;
    }
}

If the new value is also set via config, it will override the value set from the old property.

1
On

Spring Boot Configuration Processor provides a @DeprecatedConfigurationProperty annotation for this purpose. The generated metadata file will include any reason/replacement notes, which causes appropriate deprecation warnings to be logged if an annotated property is used.

See here for a basic example. The following snippet from CassandraProperties.java shows a real-world use case in which spring.data.cassandra.cluster-name was deprecated in favor of spring.data.cassandra.session-name. Backwards compatibility is handled by simply calling the getter/setter for the replacement property in the getter/setter for the deprecated property:

public String getSessionName() {
    return this.sessionName;
}

public void setSessionName(String sessionName) {
    this.sessionName = sessionName;
}

@Deprecated
@DeprecatedConfigurationProperty(replacement = "spring.data.cassandra.session-name")
public String getClusterName() {
    return getSessionName();
}

@Deprecated
public void setClusterName(String clusterName) {
    setSessionName(clusterName);
}

To achieve the same behavior for properties that aren't mapped to a @ConfigurationProperties bean you can manually specify them in META-INF/additional-spring-configuration-metadata.json and add a runtime dependency on org.springframework.boot:spring-boot-properties-migrator. See Spring Boot docs for reference.

The following snippet from spring-boot-autoconfigure shows a real-world use case in which server.servlet.path was deprecated in favor of spring.mvc.servlet.path. Backwards compatibility is handled by the PropertiesMigrationListener, which "Automatically renames the keys that have a matching replacement and log[s] a report of what was discovered.":

    {
      "name": "server.servlet.path",
      "type": "java.lang.String",
      "description": "Path of the main dispatcher servlet.",
      "defaultValue": "/",
      "deprecation": {
        "replacement": "spring.mvc.servlet.path",
        "level": "error"
      }
    },

If you set the deprecated property server.servlet.path=/foo, the replacement property @Value("${spring.mvc.servlet.path}") will evaluate to /foo, and the deprecation notice will be logged on startup.