Relentless Coding

A Developer’s Blog

Spring Basics Dynamically Inject Values With Springs Value

If we do not want to hard-code values into our source code, we can use properties files. With the @Value annotation, Spring gives us an easy means to get properties from properties files and inject them into our code.

Dependencies

This post was written with Spring 5.0.5.RELEASE and Java 1.8.0_181.

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.0.5.RELEASE</version>
</dependency>

Inject Scalars

Let’s say we have the following key-value pairs in a file app.properties:

app.string.property=hello
app.integer.property=987
app.floating.point.property=3.14159
app.boolean.property=false

To get access to these properties, we declare an application configuration in Java:

@Configuration
@ComponentScan
@PropertySource("app.properties")
public class AppConfig {}

The @PropertySource adds a property source to Spring’s Environment. Here, the properties file is placed in the root of the classpath (and since I am using Maven default paths, that would be src/main/resources.

package com.relentlesscoding.wirebeans;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = AppConfig.class)
public class HabitTest {

    @Value("${app.string.property}")
    private String stringProperty;

    @Value("${app.integer.property}")
    private int integerProperty;

    @Value("${app.floating.point.property}")
    private float floatingPointProperty;

    @Value("${app.boolean.property}")
    private boolean booleanProperty;

    @Test
    public void stringProperty() {
        assertThat(stringProperty, is("hello"));
    }

    @Test
    public void integerProperty() {
        assertThat(integerProperty, is(987));
    }

    @Test
    public void floatingPointProperty() {
        assertThat(floatingPointProperty, is(3.14159F));
    }

    @Test
    public void booleanProperty(){
        assertThat(booleanProperty, is(false));
    }
}

Injecting Complex Values

Let’s say we also have collections of values in our properties file:

app.collection.strings.property=one, two, three
app.collection.floats.property=1.2, 3.4, 5.6
app.collection.dates.property=2018-09-30T10:00:00, 2018-09-29T11:00:00, 2018-09-28T12:00:00

By default, Spring won’t be able to interpret Lists of values. The best we can do, by default, is getting an array of Strings:

@Value("${app.collection.strings.property}")
private String[] stringArrayProperty;

@Test
public void stringsArrayProperty() {
    assertArrayEquals(new String[]{"one", "two", "three"}, stringArrayProperty);
}

Declare a ConversionService to Inject Collections And Other Complex Types

To convert properties to other types than strings, we need to enable Spring’s ConversionService.

@Bean
public static ConversionService conversionService() {
    return new DefaultFormattingConversionService();
}

We instantiate DefaultFormattingConversionService, which is “configured by default with converters and formatters appropriate for most applications”. That means that it can convert comma-separated strings to common, generic collection types, and can convert strings to dates, currencies and TimeZones, for example.

Notice that the method is declared static. ConversionService is of type BeanFactoryPostProcessor and must be instantiated very early in the Spring container life cycle, so that its services are available when processing of annotations such as @Autowired and @Value are done. By declaring the method static, the ConversionService can be invoked without instantiating the enclosing @Configuration class. Therefore, we can use @Value (and other annotations) that make use of this converter in the same class, without running into the trouble that ConversionService is not yet instantiated.

To learn more about bean instantiation, read the part under BeanFactoryPostProcessor-returning @Bean methods. Also, this answer on StackOverflow is very clarifying.

@Test
public void floatArrayProperty() {
    assertArrayEquals(new float[]{1.2F, 3.4F, 5.6F}, floatArrayProperty, 0.01F);
}

To convert dates from a properties string to actual date objects we need an extra step: @DateTimeFormat. This annotation indicates the format of the date to Spring. In this case, we format our property strings to conform to ISO-8601 or DateTimeFormat.ISO.DATE_TIME:

@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
@Value("${app.collection.dates.property}")
private List<LocalDate> listOfDatesProperty;

@Test
public void listOfDatesProperty() {
    assertThat(listOfDatesProperty.size(), is(3));
    assertEquals(LocalDate.parse("2018-09-30T10:00:00", DateTimeFormatter.ISO_DATE_TIME), listOfDatesProperty.get(0));
}

Thanks to our ConversionService, we can now convert not only to arrays, but also to collections.

Inject Maps With Spring’s @Value

If we throw in a Spring Expression Language (SpEL) expression, we can even have dictionaries in our properties and convert them to Maps:

app.collection.map.string.to.integer={one:"1", two:"2", three:"3"}
@Value("#{${app.collection.map.string.to.integer}}")
private Map<String, Integer> mapStringToInteger;

@Test
public void mapProperty() {
    assertThat(mapStringToInteger.size(), is(3));
    assertEquals(new Integer(1), mapStringToInteger.get("one"));
    assertEquals(new Integer(2), mapStringToInteger.get("two"));
    assertEquals(new Integer(3), mapStringToInteger.get("three"));
}

Notice the #{...} that delimits the SpEL expression. It evaluates the string that comes from the properties files and parses it to a Map. To understand how this works, let’s have a look at what a literal map in a SpEL expression would look like:

@Value("#{{1: 'Catch-22', 2: '1984', 3: 'Pride and Prejudice'}}")
private Map<Integer, String> books;

A literal map in a SpEL expression is delimited by braces {key: 'value', ...}. This is exactly what we had in our properties file.