Appearance
Conditional Beans with @ConditionalOnProperty
Chapter 9 used a property to set a value in a bean. A property can also decide whether a bean is created at all. That is @ConditionalOnProperty.
Why does it matter? A large application has thousands of beans, and the singletons among them are all created at startup, needed or not. They clutter the application context (the IoC container), cost memory and slow startup. @ConditionalOnProperty lets configuration switch beans off.
Step 1: the starting point
Two connection classes, and a class that uses both:
java
@Component
public class MySQLConnection {
MySQLConnection() {
System.out.println("initialization of MySQLConnection bean");
}
}
@Component
public class NoSQLConnection {
NoSQLConnection() {
System.out.println("initialization of NoSQLConnection bean");
}
}
@Component
public class DBConnection {
@Autowired
MySQLConnection mySQLConnection;
@Autowired
NoSQLConnection noSQLConnection;
@PostConstruct
public void init() {
System.out.println("DBConnection bean created with dependencies below:");
System.out.println("is MySQLConnection object null: " + (mySQLConnection == null));
System.out.println("is NoSQLConnection object null: " + (noSQLConnection == null));
}
}All three are singletons, so all three are created at startup, and both connections are injected:
text
initialization of MySQLConnection bean
initialization of NoSQLConnection bean
DBConnection bean created with dependencies below:
is MySQLConnection object null: false
is NoSQLConnection object null: falseStep 2: why you would want only one
Two situations that come up in real companies:
- Migration. The company moves from MySQL to NoSQL. Once the move is done, the
MySQLConnectionbean should no longer be created. While migrating, you want to flip between them without changing code. - Two applications, one shared codebase. Application 1 and application 2 both depend (in their
pom.xml) on a common library that contains both connection classes. Application 1 needs onlyNoSQLConnection; application 2 onlyMySQLConnection. Each application should create only the bean it needs.
In both cases, the same code must create only one of the beans, chosen by configuration.
Step 3: add @ConditionalOnProperty
java
@Component
@ConditionalOnProperty(prefix = "sqlconnection", value = "enabled",
havingValue = "true", matchIfMissing = false)
public class MySQLConnection { ... }
@Component
@ConditionalOnProperty(prefix = "nosqlconnection", value = "enabled",
havingValue = "true", matchIfMissing = false)
public class NoSQLConnection { ... }properties
# application.properties
sqlconnection.enabled=trueThe four attributes:
| Attribute | Meaning |
|---|---|
prefix + value | together they form the property key: prefix.value, here sqlconnection.enabled (name is an alias of value) |
havingValue | the text the property's value must equal for the bean to be created. Just a string: "true" here, but "sql" works too if the property says sqlconnection.enabled=sql |
matchIfMissing | what to do when the key is not in the properties at all: false = do not create the bean, true = create it anyway |
The rule: if the property's value equals havingValue, the bean is created; otherwise it is not.
Start the application:
text
initialization of MySQLConnection bean
Description:
Field noSQLConnection in com.example.demo.DBConnection required a bean of type 'com.example.demo.NoSQLConnection' that could not be found.MySQLConnection was created (its key exists and equals "true"). NoSQLConnection was not (its key is missing, and matchIfMissing = false). So far so good. But DBConnection still requires a NoSQLConnection, and startup fails.
Step 4: let the injection point accept a missing bean
@Autowired means @Autowired(required = true): this dependency must be found, otherwise fail. A conditional bean may legitimately not exist, so wherever you inject one, say that it is optional:
java
@Component
public class DBConnection {
@Autowired(required = false)
MySQLConnection mySQLConnection;
@Autowired(required = false)
NoSQLConnection noSQLConnection;
@PostConstruct
public void init() { ... as before ... }
}With required = false, a missing bean is not an error: the field is simply left null. Every use of @ConditionalOnProperty has these two parts: the condition on the class, and required = false (or another way of handling absence) where it is injected.
Start again, with sqlconnection.enabled=true:
text
initialization of MySQLConnection bean
DBConnection bean created with dependencies below:
is MySQLConnection object null: false
is NoSQLConnection object null: trueWhat Spring did:
MySQLConnection: singleton, so eligible for creation at startup. It has a condition: build the keysqlconnection.enabled, look it up, findtrue, compare withhavingValue"true": equal, so create it.NoSQLConnection: keynosqlconnection.enabledis missing,matchIfMissing = false: not created.DBConnection: created;MySQLConnectionis injected; there is noNoSQLConnectionbean, whichrequired = falseallows, so that field staysnull.
Step 5: switch with configuration only
Finished migrating to NoSQL? Change the properties and restart:
properties
sqlconnection.enabled=false
nosqlconnection.enabled=truetext
initialization of NoSQLConnection bean
DBConnection bean created with dependencies below:
is MySQLConnection object null: true
is NoSQLConnection object null: falseNo code changed. For the two-applications case, application 1 puts nosqlconnection.enabled=true in its application.properties, application 2 puts sqlconnection.enabled=true in its own.
havingValue is an exact string match. With sqlconnection.enabled=sql (and havingValue = "true"), the values differ, so neither connection is created:
text
DBConnection bean created with dependencies below:
is MySQLConnection object null: true
is NoSQLConnection object null: true(If you leave havingValue out, the bean is created whenever the property exists and is not false.)
Advantages and disadvantages
Advantages
- Feature toggling. Switch a feature or an implementation on or off from
application.properties, as in the migration. - An uncluttered application context. Beans that are not needed are never created.
- Less memory. Every bean created at startup is kept in memory; fewer beans, less memory.
- Faster startup. Creating thousands of beans takes time; skipping unneeded ones shortens startup.
Disadvantages
- Misconfiguration. A value that does not match (
sqlin the property,"true"inhavingValue) silently switches a bean off, as the last example showed. - Complexity when overused. With the annotation on hundreds of classes, you must keep checking the property files to know which beans exist.
- One key controlling several beans is confusing. Using
sqlconnection.enabledfor both the MySQL and the NoSQL bean makes it hard to reason about which exists. Give each its own key. - More to manage. Property files and annotation values have to be kept in sync.
Next: The most common reason configuration differs is the environment: dev, QA and production need different databases, URLs and limits. Chapter 11 covers profiles, Spring's built-in way to switch a whole set of properties, and beans, at once.
Check yourself
- What does
@ConditionalOnPropertydecide, and why does it matter in a large application? - Give the two use cases from this chapter.
- With
prefix = "sqlconnection"andvalue = "enabled", which property key is checked? - The property is
sqlconnection.enabled=sqlandhavingValue = "true". Is the bean created? - The key is missing and
matchIfMissing = false. Is the bean created? And withmatchIfMissing = true? - In step 3, the condition worked but startup still failed. Why, and what is the fix?
- What does
required = falsedo when the bean does not exist? - How do you switch from MySQL to NoSQL, and what has to change in the code?
- Name two advantages and two disadvantages.
Answers
- Whether a bean is created at all, based on a property. Unneeded beans then do not clutter the context, use memory or slow startup.
- Migrating between implementations (MySQL to NoSQL), and two applications sharing a codebase where each needs a different bean.
sqlconnection.enabled.- No:
"sql"does not equal"true". - No. Yes.
- The injection point (
@Autowired,required = trueby default) demanded aNoSQLConnection, which was not created. Use@Autowired(required = false). - Startup continues and the field stays
null. - Change the properties (
sqlconnection.enabled=false,nosqlconnection.enabled=true) and restart. Nothing in the code. - Advantages: feature toggling, uncluttered context, less memory, faster startup. Disadvantages: silent misconfiguration, complexity when overused, confusion when one key controls several beans, property files and annotations to keep in sync.