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 List
s of values. The
best we can do, by default, is getting an array of String
s:
@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 TimeZone
s, 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 Map
s:
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.