Value Injection

jim stafford

Introduction

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");
}

Goals

The student will learn:

  • how to configure an application using properties

  • how to use different forms of injection

Objectives

At the conclusion of this lecture and related exercises, the student will be able to:

  1. implement value injection into a Spring Bean attribute using

    • field injection

    • constructor injection

  2. inject a specific value at runtime using a command line parameter

  3. define a default value for the attribute

  4. define property values for attributes of different type

@Value Annotation

  • 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);
    }
}
1defining target of value as a FIELD
2using FIELD injection to directly inject into the field

Property Name

@Value("${property_name}")
  • No specific requirements for property names

  • Common convention

    • (scope prefix).(property)

    • app.audience

    • logging.file.name

    • spring.application.name

Value Not Found

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}"

Value Property Provided by Command Line

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
1use double dash (--) and property name to supply property value

Default 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;
1use :value to express a default value for injection


Property Default
$ java -jar target/appconfig-valueinject-example-*-SNAPSHOT-bootexec.jar
...
Application @Bean says Hey Default World
Property Defined
$ java -jar target/appconfig-valueinject-example-*-SNAPSHOT-bootexec.jar \
    --app.audience="Command line World"
...
Application @Bean says Hey Command line World

Constructor Injection

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

@Value("${app.audience}")
private String audience;

public AppCommand(Hello greeter) {
    this.greeter = greeter;
    greeter.sayHello(audience); //X-no (1)
}
1audience value will be null when used in the constructor — when using FIELD injection

Constructor Injection Solution

@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)
    }
1audience value will be known when used in the constructor
2audience value can be optionally made final

@PostConstruct

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;
    }
1constructor injection occurs first and in-time to declare attribute as final
2field and property-injection occurs next and can involve many properties
3Container calls @PostConstruct when all injection complete

Property Types

non-String Property Types

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

Collection Property Types

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)
1parsed sequence with duplicates injected into List maintained duplicates
2parsed sequence with duplicates injected into Set retained only unique values
3parsed sequence with duplicates injected into Array maintained duplicates

Custom Delimiters (using Spring SpEL)

We can get a bit more elaborate and define a custom delimiter for the values

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]
...
1sequence is expressed on command line using two different delimiters
2val.intListDelimiter String is read in from raw property value and segmented at the custom ! character

Map Property Types

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}
1parsed map injected into Map of specific type using Spring Expression Language (`#{}') operator

Map Element

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)
...
1Spring EL declared to use Map element with key 3 and default to a Map of 2 elements with key 0 and 3
2With no arguments provided, the default 3:'' value was injected
3With a map provided, the value 3:'buz' was injected

System Properties

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
1Complete Map of system properties is injected
2Single element is accessed and printed

Property Conversion Errors

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"

Summary

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.