public void run(String... args) throws Exception {
greeter.sayHello("World");
}
jim stafford
One of the things you may have noticed was the hard-coded string in the AppCommand class in the previous example.
public void run(String... args) throws Exception {
greeter.sayHello("World");
}
Let’s say we don’t want the value hard-coded or passed in as a command-line argument
Let’s go down a path that uses standard Spring value injection to inject a value from a property file
The student will learn:
how to configure an application using properties
how to use different forms of injection
At the conclusion of this lecture and related exercises, the student will be able to:
implement value injection into a Spring Bean attribute using
field injection
constructor injection
inject a specific value at runtime using a command line parameter
define a default value for the attribute
define property values for attributes of different type
To inject a value from a property source
add the Spring @Value
annotation to the component property
package info.ejava.examples.app.config.valueinject;
import org.springframework.beans.factory.annotation.Value;
...
@Component
public class AppCommand implements CommandLineRunner {
private final Hello greeter;
@Value("${app.audience}") (2)
private String audience; (1)
public AppCommand(Hello greeter) {
this.greeter = greeter;
}
public void run(String... args) throws Exception {
greeter.sayHello(audience);
}
}
1 | defining target of value as a FIELD |
2 | using FIELD injection to directly inject into the field |
@Value("${property_name}")
No specific requirements for property names
Common convention
(scope prefix).(property)
app.audience
logging.file.name
spring.application.name
However, if the property is not defined anywhere the following ugly error will appear.
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'appCommand' defined in file [.../app/app-config/appconfig-valueinject-example/target/classes/info/ejava/examples/app/config/valueinject/AppCommand.class]:
Unexpected exception during bean creation
...
Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder
'app.audience' in value "${app.audience}"
We can try to fix the problem by defining the property value on the command line
$ java -jar target/appconfig-valueinject-example-*-SNAPSHOT-bootexec.jar \
--app.audience="Command line World" (1)
...
Application @Bean says Hey Command line World
1 | use double dash (-- ) and property name to supply property value |
We can defend against the value not being provided by assigning a default value where we declared the injection
@Value("${app.audience:Default World}") (1)
private String audience;
1 | use :value to express a default value for injection |
$ java -jar target/appconfig-valueinject-example-*-SNAPSHOT-bootexec.jar
...
Application @Bean says Hey Default World
$ java -jar target/appconfig-valueinject-example-*-SNAPSHOT-bootexec.jar \
--app.audience="Command line World"
...
Application @Bean says Hey Command line World
In the above version of the example, we injected the Hello
bean through the constructor
and the audience
property using FIELD injection. This means
the value for audience
attribute will not be known during the constructor
the value for audience
attribute cannot be made final
@Component
public class AppCommand implements CommandLineRunner {
private final Hello greeter;
@Value("${app.audience}")
private String audience; //<== injection is after ctor
public AppCommand(Hello greeter) {
this.greeter = greeter;
greeter.sayHello(audience); //X-no (1)
}
1 | audience value will be null when used in the constructor — when using FIELD injection |
@Component
public class AppCommand implements CommandLineRunner {
private final Hello greeter;
private final String audience; (2)
public AppCommand(Hello greeter,
@Value("${app.audience:Default World}") String audience) {
this.greeter = greeter;
this.audience = audience; (1)
}
1 | audience value will be known when used in the constructor |
2 | audience value can be optionally made final |
Alternatively …
account for late-arriving field-injection using @PostConstruct
Spring container will call method annotated with @PostConstruct
after instantiation and properties fully injected.
import jakarta.annotation.PostConstruct;
...
@Component
public class AppCommand implements CommandLineRunner {
private final Hello greeter; (1)
@Value("${app.audience}")
private String audience; (2)
@PostConstruct
void init() { (3)
greeter.sayHello(audience); //yes-greeter and audience initialized
}
public AppCommand(Hello greeter) {
this.greeter = greeter;
}
1 | constructor injection occurs first and in-time to declare attribute as final |
2 | field and property-injection occurs next and can involve many properties |
3 | Container calls @PostConstruct when all injection complete |
Properties can also express non-String types
@Component
public class PropertyExample implements CommandLineRunner {
private final String strVal;
private final int intVal;
private final boolean booleanVal;
private final float floatVal;
public PropertyExample(
@Value("${val.str:}") String strVal,
@Value("${val.int:0}") int intVal,
@Value("${val.boolean:false}") boolean booleanVal,
@Value("${val.float:0.0}") float floatVal) {
...
Non-string values syntactically converted to type of target variable
$ java -jar target/appconfig-valueinject-example-*-SNAPSHOT-bootexec.jar \
--app.audience="Command line option" \
--val.str=aString \
--val.int=123 \
--val.boolean=true \
--val.float=123.45
...
Application @Bean says Hey Command line option
strVal=aString
intVal=123
booleanVal=true
floatVal=123.45
We can also express properties as a sequence of values and inject the parsed string into Arrays and Collections.
...
private final List<Integer> intList;
private final int[] intArray;
private final Set<Integer> intSet;
public PropertyExample(...
@Value("${val.intList:}") List<Integer> intList,
@Value("${val.intList:}") Set<Integer> intSet,
@Value("${val.intList:}") int[] intArray) {
...
--val.intList=1,2,3,3,3
...
intList=[1, 2, 3, 3, 3] (1)
intSet=[1, 2, 3] (2)
intArray=[1, 2, 3, 3, 3] (3)
1 | parsed sequence with duplicates injected into List maintained duplicates |
2 | parsed sequence with duplicates injected into Set retained only unique values |
3 | parsed sequence with duplicates injected into Array maintained duplicates |
We can get a bit more elaborate and define a custom delimiter for the values
using Spring Expression Language (EL; SpEL) #{}
operator
(Ref: A Quick Guide to Spring @Value)
private final List<Integer> intList;
private final List<Integer> intListDelimiter;
public PropertyExample(
...
@Value("${val.intList:}") List<Integer> intList,
@Value("#{'${val.intListDelimiter:}'.split('!')}") List<Integer> intListDelimiter, (2)
...
--val.intList=1,2,3,3,3 --val.intListDelimiter='1!2!3!3!3' (1)
...
intList=[1, 2, 3, 3, 3]
intListDelimeter=[1, 2, 3, 3, 3]
...
1 | sequence is expressed on command line using two different delimiters |
2 | val.intListDelimiter String is read in from raw property value and segmented at the custom ! character |
We can also leverage Spring EL to inject property values directly into a Map.
private final Map<Integer,String> map;
public PropertyExample( ...
@Value("#{${val.map:{}}}") Map<Integer,String> map) { (1)
...
--val.map="{0:'a', 1:'b,c,d', 2:'x'}"
...
map={0=a, 1=b,c,d, 2=x}
1 | parsed map injected into Map of specific type using Spring Expression Language (`#{}') operator |
We can also use Spring EL to obtain a specific element from a Map.
private final Map<String, String> systemProperties;
public PropertyExample(
...
@Value("#{${val.map:{0:'',3:''}}[3]}") String mapValue, (1)
...
(no args)
...
mapValue= (2)
--val.map={0:'foo', 2:'bar, baz', 3:'buz'}
...
mapValue=buz (3)
...
1 | Spring EL declared to use Map element with key 3 and default to a Map of 2 elements with key 0 and 3 |
2 | With no arguments provided, the default 3:'' value was injected |
3 | With a map provided, the value 3:'buz' was injected |
We can also simply inject Java System Properties into a Map using Spring EL.
private final Map<String, String> systemProperties;
public PropertyExample(
...
@Value("#{systemProperties}") Map<String, String> systemProperties) { (1)
...
System.out.println("systemProperties[user.timezone]=" + systemProperties.get("user.timezone")); (2)
...
systemProperties[user.timezone]=America/New_York
1 | Complete Map of system properties is injected |
2 | Single element is accessed and printed |
An error will be reported and the program will not start if the value provided cannot be syntactically converted to the target variable type.
$ java -jar target/appconfig-valueinject-example-*-SNAPSHOT-bootexec.jar \ --val.int=abc ... TypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'int'; nested exception is java.lang.NumberFormatException: For input string: "abc"
In this section we
defined a value injection for an attribute within a Spring Bean using
field injection
constructor injection
defined a default value to use in the event a value is not provided
defined a specific value to inject at runtime using a command line parameter
implemented property injection for attributes of different types
Built-in types (String, int, boolean, etc)
Collection types
Maps
Defined custom parsing techniques using Spring Expression Language (EL)
In future sections we will look to specify properties using aggregate property sources like file(s) rather than specifying each property individually.