Appearance
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:
- inject both and choose per request, from the client's input;
- choose from configuration, with a value in
application.propertiesread 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 initializedCall it:
| Request | Response |
|---|---|
POST /api/createOrder?isOnlineOrder=true | created online order |
POST /api/createOrder?isOnlineOrder=false | created 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=falseUser 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 initializedPOST /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 initializedPOST /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 both | Solution 2: @Bean + @Value | |
|---|---|---|
| Decided by | each request's input | configuration |
| Changes when | on every request | on restart, after changing the property |
| Beans created | both implementations | only 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:
| Source | Example |
|---|---|
| a property file | @Value("${isOnlineOrder}") with isOnlineOrder=false in application.properties |
| an environment variable | ISONLINEORDER=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 initializedSpring 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 nulltext
Autowired annotation is not supported on static fields: static java.lang.String ...staticNameSpring 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
- Why does
@Qualifier("onlineOrderObject")on the injected field not solve the problem dynamically? - In Solution 1, how is the implementation chosen, and how many
Orderbeans exist? - In Solution 2, why must
OnlineOrderandOfflineOrderlose their@Component? - Walk through what Spring does at startup to inject the
OrderintoUserin Solution 2. - You change
isOnlineOrderinapplication.propertieswhile the application runs. When does the change take effect? - Name three sources
@Valuecan read from. application.propertiessaysisOnlineOrder=falseand the environment hasISONLINEORDER=true. Which is used?- What happens with
@Value("${app.timeout:30}")whenapp.timeoutis not defined? And with@Value("${app.timeout}")? - Why does
@Valueon astaticfield staynull?
Answers
- It always injects the same implementation; the choice is fixed in code, so nobody can get the other one without a code change.
- Both implementations are injected (one field each, each with its
@Qualifier), and the method picks one from the request's input. TwoOrderbeans exist. - The
@Beanmethod creates the chosen implementation itself. With@Componentstill on them, Spring would also create both as beans, giving two candidates again. - Creating the singleton
Userneeds anOrder. Spring finds the@Beanmethod returningOrder, resolves its@Value("${isOnlineOrder}")parameter from the configuration, calls the method, and injects the returned object. - After a restart: the
@Beanmethod runs once, at startup. - Property files, environment variables, inline literals.
- The environment variable (
true). - The value is 30. Without a default, startup fails: "Could not resolve placeholder 'app.timeout'".
- Spring injects into bean instances; a static field belongs to the class (Spring logs that it is not supported and skips it).