Skip to content
Chapter 09 · Spring Boot

Choosing Beans at Runtime with @Value ​

Chapter 7 ended with an interface, Order, with two implementations, and solved the "which one?" problem with @Qualifier. That fix has a flaw. This chapter shows the flaw, then two ways to choose the implementation dynamically:

  1. inject both and choose per request, from the client's input;
  2. choose from configuration, with a value in application.properties read by @Value.

@Value is also the start of the configuration module (chapters 9 to 11): changing what the application does without changing code.

Recap: the unsatisfied dependency ​

java
public interface Order {
    String createOrder();
}

@Component
public class OnlineOrder implements Order {
    public String createOrder() { return "created online order"; }
}

@Component
public class OfflineOrder implements Order {
    public String createOrder() { return "created offline order"; }
}

@RestController
@RequestMapping("/api")
public class User {

    @Autowired
    Order order;          // OnlineOrder or OfflineOrder?

    @PostMapping("/createOrder")
    public String createOrder() {
        return order.createOrder();
    }
}

User is a singleton, created at startup. Spring must inject an Order, finds two candidates, does not guess, and startup fails ("required a single bean, but 2 were found").

Chapter 7 fixed it with @Qualifier:

java
@Component
@Qualifier("onlineOrderObject")
public class OnlineOrder implements Order { ... }

@Component
@Qualifier("offlineOrderObject")
public class OfflineOrder implements Order { ... }

@RestController
@RequestMapping("/api")
public class User {

    @Autowired
    @Qualifier("onlineOrderObject")      // always the online order
    Order order;
    ...
}

The flaw: the choice is hard-coded ​

User now always gets OnlineOrder. A customer who wants an offline order cannot get one, and switching means editing code and redeploying. User declares the Order interface, but @Qualifier("onlineOrderObject") ties it to one concrete implementation, so you lose the flexibility the interface was supposed to give (the point of the Dependency Inversion Principle from chapter 7).

What we want is to decide at run time. There are two common ways.

Solution 1: inject both, choose per request ​

Keep the qualifiers on the two implementations. In User, inject both, each into its own field, and pick one in the method based on what the client asks for:

java
@RestController
@RequestMapping("/api")
public class User {

    @Autowired
    @Qualifier("onlineOrderObject")
    Order onlineOrderObject;

    @Autowired
    @Qualifier("offlineOrderObject")
    Order offlineOrderObject;

    @PostMapping("/createOrder")
    public String createOrder(@RequestParam boolean isOnlineOrder) {
        if (isOnlineOrder) {
            return onlineOrderObject.createOrder();
        } else {
            return offlineOrderObject.createOrder();
        }
    }
}

Start the application: both implementations are singletons, so both are created.

text
online order initialized
offline order initialized

Call it:

RequestResponse
POST /api/createOrder?isOnlineOrder=truecreated online order
POST /api/createOrder?isOnlineOrder=falsecreated offline order

The choice now depends on each request's input. This is a very common pattern in real code: inject every implementation you may need and let the business logic pick.

Solution 2: let configuration choose, with @Value ​

Sometimes the choice is not per request but per deployment: this installation takes online orders, that one offline orders. Then the choice belongs in configuration.

Step 1: remove @Component from the implementations ​

java
public class OnlineOrder implements Order {

    public OnlineOrder() {
        System.out.println("online order initialized");
    }

    public String createOrder() { return "created online order"; }
}

public class OfflineOrder implements Order {

    public OfflineOrder() {
        System.out.println("offline order initialized");
    }

    public String createOrder() { return "created offline order"; }
}

Why? We are about to create the Order bean ourselves, in an @Bean method, choosing which class to instantiate. If the classes still had @Component, Spring would also create both as beans, and we would be back to "2 candidates".

Step 2: an @Bean method that reads a property ​

java
@Configuration
public class AppConfig {

    @Bean
    public Order createOrderBean(@Value("${isOnlineOrder}") boolean isOnlineOrder) {
        if (isOnlineOrder) {
            return new OnlineOrder();
        } else {
            return new OfflineOrder();
        }
    }
}

@Bean (chapter 6) says: the object this method returns is a bean. The new part is the parameter: @Value("${isOnlineOrder}") tells Spring to fill it with the value of the property isOnlineOrder. ${...} is a placeholder: "look up this key".

Step 3: set the property ​

properties
# src/main/resources/application.properties
isOnlineOrder=false

User goes back to a plain Order field, no qualifier:

java
@RestController
@RequestMapping("/api")
public class User {

    @Autowired
    Order order;

    @PostMapping("/createOrder")
    public String createOrder() {
        return order.createOrder();
    }
}

Step 4: run it ​

text
offline order initialized

POST /api/createOrder → created offline order.

Follow the lifecycle to see why. At startup Spring creates the singleton User, which needs an Order. No class is annotated as an Order component, but the @Configuration class has an @Bean method returning Order, so Spring calls it. To call it, Spring resolves the parameter: it finds @Value("${isOnlineOrder}"), looks up isOnlineOrder in the configuration, finds false, converts the text "false" to the boolean false, and passes it. The method returns an OfflineOrder, which becomes the Order bean and is injected into User.

Change the property to isOnlineOrder=true and restart:

text
online order initialized

POST /api/createOrder → created online order. No code changed. A restart is needed because the @Bean method runs once, when the bean is created at startup.

Solution 1: inject bothSolution 2: @Bean + @Value
Decided byeach request's inputconfiguration
Changes whenon every requeston restart, after changing the property
Beans createdboth implementationsonly the chosen one

More about @Value ​

@Value injects a value into a field, a constructor or method parameter, or a setter. The value can come from:

SourceExample
a property file@Value("${isOnlineOrder}") with isOnlineOrder=false in application.properties
an environment variableISONLINEORDER=true
an inline literal@Value("false")

Spring converts the text to the target type (String, int, boolean, ...).

java
@Component
public class AppSettings {

    @Value("${app.name}")             // from application.properties
    String name;

    @Value("${app.timeout:30}")       // 30 if app.timeout is not defined
    int timeout;

    @Value("false")                   // an inline literal
    boolean debug;
}

With app.name=demo in the properties and no app.timeout: name=demo timeout=30 debug=false.

Environment variables override the property file. Run the Solution 2 application with the environment variable ISONLINEORDER=true while application.properties still says isOnlineOrder=false:

text
online order initialized

Spring Boot reads both, and the environment variable wins. (It matches the upper-case environment name ISONLINEORDER to the property isOnlineOrder.) This is how the same build gets different settings on different servers.

A missing property without a default stops startup. Remove isOnlineOrder from the properties:

text
Could not resolve placeholder 'isOnlineOrder' in value "${isOnlineOrder}"

Failing at startup is good for required settings. For optional ones, give a default after a colon: ${isOnlineOrder:false}.

@Value on a static field is ignored:

java
@Value("${app.name}")
static String staticName;          // stays null
text
Autowired annotation is not supported on static fields: static java.lang.String ...staticName

Spring injects into bean instances; a static field belongs to the class, not to any instance. For the same reason, @Value does nothing in a class Spring does not manage (created with new, no @Component): the field stays null.

With constructor injection (the style chapter 7 recommends), put @Value on the parameters:

java
@Component
public class DatabaseClient {

    private final String host;
    private final int port;

    public DatabaseClient(@Value("${db.host:localhost}") String host,
                          @Value("${db.port:5432}") int port) {
        this.host = host;
        this.port = port;
    }
}

Next: @Value injects a value into a bean. A property can also decide whether a bean is created at all. Chapter 10 covers @ConditionalOnProperty.

Check yourself ​

  1. Why does @Qualifier("onlineOrderObject") on the injected field not solve the problem dynamically?
  2. In Solution 1, how is the implementation chosen, and how many Order beans exist?
  3. In Solution 2, why must OnlineOrder and OfflineOrder lose their @Component?
  4. Walk through what Spring does at startup to inject the Order into User in Solution 2.
  5. You change isOnlineOrder in application.properties while the application runs. When does the change take effect?
  6. Name three sources @Value can read from.
  7. application.properties says isOnlineOrder=false and the environment has ISONLINEORDER=true. Which is used?
  8. What happens with @Value("${app.timeout:30}") when app.timeout is not defined? And with @Value("${app.timeout}")?
  9. Why does @Value on a static field stay null?
Answers
  1. It always injects the same implementation; the choice is fixed in code, so nobody can get the other one without a code change.
  2. Both implementations are injected (one field each, each with its @Qualifier), and the method picks one from the request's input. Two Order beans exist.
  3. The @Bean method creates the chosen implementation itself. With @Component still on them, Spring would also create both as beans, giving two candidates again.
  4. Creating the singleton User needs an Order. Spring finds the @Bean method returning Order, resolves its @Value("${isOnlineOrder}") parameter from the configuration, calls the method, and injects the returned object.
  5. After a restart: the @Bean method runs once, at startup.
  6. Property files, environment variables, inline literals.
  7. The environment variable (true).
  8. The value is 30. Without a default, startup fails: "Could not resolve placeholder 'app.timeout'".
  9. Spring injects into bean instances; a static field belongs to the class (Spring logs that it is not supported and skips it).