coderatchet
coderatchet

Reputation: 8420

Spring Boot extend existing properties class with new prefix

I wish to setup some custom datasource properties ("app.datasource.alternate") in addition to the standard DataSourceProperties ("spring.datasource").

These 2 datasources are not competing (i.e. the app.datasource.alternate properties are not a replacement for the spring.datasource properties) and should live alongside each other in the default application properties.

I was hoping something like this would work:

@ConfigurationProperties("app.datasource.alternate")
public class AlternateDataSourceProperties extends DataSourceProperties {}

However when I define the property in my yaml:

app:
  datasource:
    alternate: 
      schema-username: TEST

And run my test:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = NONE)
@EnableConfigurationProperties({AlternateDataSourceProperties.class})
public class AlternateDataSourcePropertiesTest {

    @Autowired
    // @Qualifier("alternateDataSourceProperties")
    private AlternateDataSourceProperties props;

    @Test
    public void propertiesAreInjected() {
        assertThat(props.getSchemaUsername()).isEqualTo("TEST");
    }
}

The program fails with an NoUniqueBeanDefinitionException when attempting to create the datasource bean in the DataSourceConfiguration.Tomcat:

Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'dataSource' defined in class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Tomcat.class]: Unsatisfied dependency expressed through method 'dataSource' parameter 0; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.boot.autoconfigure.jdbc.DataSourceProperties' available: expected single matching bean but found 2: app.datasource.alternate-com.example.config.AlternateDataSourceProperties,spring.datasource-org.springframework.boot.autoconfigure.jdbc.DataSourceProperties
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:749)
    at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:467)
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.boot.autoconfigure.jdbc.DataSourceProperties' available: \
  expected single matching bean but found 2: \
  app.datasource.alternate-com.example.config.AlternateDataSourceProperties \
  ,spring.datasource-org.springframework.boot.autoconfigure.jdbc.DataSourceProperties
    at org.springframework.beans.factory.config.DependencyDescriptor.resolveNotUnique(DependencyDescriptor.java:173)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1116)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1066)
    at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:835)
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:741)
    ... 66 more

I do not wish to re-write an entire properties class that already exists, I just need an additional set of properties under a different prefix. Is this possible in Spring Boot?

I am using Spring Boot 1.5.X

UPDATE

with inspiration from @Prashant's answer I have tried creating an additional configuration file and injecting them with the following: @PropertySource("classpath:config/alternate-ds.yml")

# alternate-ds.yml
app:
  datasource:
    alternate:
      schema-username: TEST

# AlternateDataSourceProperties.java
@ConfigurationProperties("app.datasource.alternate")
@PropertySource("classpath:config/alternate-ds.yml")
public class AlternateDataSourceProperties extends DataSourceProperties {}

I still get the NoUniqueBeanDefinitionException as now there still exists a competing bean resource. Is there a way to specify a bean should not be autowired?

Upvotes: 4

Views: 11083

Answers (2)

coderatchet
coderatchet

Reputation: 8420

Based on inspiration from @Prashant's answer I have come up with a work around. The solution is somewhat intrusive as it redefines an existing properties bean so could be dangerous, however the @SpringBootTest seem to be ok with this.

An optimal solution for me would be to somehow declare that the custom bean should not be picked up by an autowiring, I have tried adding the autowire = Autowire.NO parameter to the @Bean annotation for the custom property set, but this still gives the NoUniqueBeanDefinitionException.

The problem is that spring doesn't know which DataSourceProperties to inject when it is referenced in library code due to me adding an additional instance of the DataSourceProperties class to the application context. The stop-gap measure I have come up with is recreating the original properties definition as a custom @Bean annotated with @Primary so there is no ambiguity with my custom DataSourceProperties Bean.

When I require the special datasource properties, I use @Qualifier("alternateDataSourceProperties") to reference the correct set.

The Configuration class looks like this:

@Configuration
public class AlternateDataSourcePropertiesConfiguration {
   @Bean
    @ConfigurationProperties("app.datasource.alternate")
    public DataSourceProperties alternateDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    @Primary
    @ConfigurationProperties("spring.datasource")
    public DataSourceProperties dataSourceProperties() {
        return new DataSourceProperties();
    }
}

And I have tested them as follows:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = NONE, properties={"app.datasource.alternate.schema-username=TEST",
"app.datasource.alternate.username=SOMETHING"})
@ActiveProfiles("test") // there is a default test datasource configured in application-test.properties so I can test that primary configuration is unaffected. i.e. I have a H2 datasource configured here with username 'sa'
public class AlternateDataSourcePropertiesConfigurationTest {

    @Autowired
    @Qualifier("alternateDataSourceProperties")
    private DataSourceProperties alternateDataSourceProperties;

    @Autowired
    private DataSourceProperties primaryDataSourceProperties;

    @Test
    public void propertiesAreInjected() {
        assertThat(alternateDataSourceProperties.getSchemaUsername()).isEqualTo("TEST");
    }

    @Test
    public void propertiesDontOverridePrimary() {
        assertThat(alternateDataSourceProperties.getUsername()).isEqualTo("SOMETHING");
        assertThat(primaryDataSourceProperties.getUsername()).isEqualTo("sa");
    }
}

Upvotes: 3

Prashant
Prashant

Reputation: 5383

The way I do it is:

    public class WebApplication { 
       private static final String CONFIGURATION_FILE_NAME_KEY = "spring.config.name";
       private static final String CONFIGURATION_FILE_NAME_VALUE = new StringJoiner(",")
            .add("application")
            .add("aurora_web")
            .toString();
       private static final String CONFIGURATION_FILE_NAME_PROPERTY = MessageFormat.format("{0}:{1}", CONFIGURATION_FILE_NAME_KEY, CONFIGURATION_FILE_NAME_VALUE);

       public static void main(String[] args) {
        new SpringApplicationBuilder(WebApplication.class)
                .properties(CONFIGURATION_FILE_NAME_PROPERTY)
                .build()
                .run(args);
    }

Spring allows configuring property file names via spring.config.name. This could be used to allow a custom file name to be used in place of the default application.[property|yml]. You can create your own property file with overridden values for the data source configuration and Spring will pick this file for you. You can access the properties using @Value() or if you have overridden the default key values from spring's properties, the new ones would be picked from this file. You can refer the following official documentation.

Upvotes: 0

Related Questions