1. Introduction

Thus far we have focused on how to configure an application within the primary application module, under fairly static conditions, and applied directly to a single application.

However, our application configuration will likely be required to be:

  • dynamically determined - Application configurations commonly need to be dynamic based on libraries present, properties defined, resources found, etc. at startup. For example, what database will be used when in development, integration, or production? What security should be enabled in development versus production areas?

  • modularized and not repeated - Breaking the application down into separate components and making these components reusable in multiple applications by physically breaking them into separate modules is a good practice. However, that leaves us with the repeated responsibility to configure the components reused. Many times there could be dozens of choices to make within a component configuration, and the application can be significantly simplified if an opinionated configuration can be supplied based on the runtime environment of the module.

If you find yourself needing configurations determined dynamically at runtime or find yourself solving a repeated problem and bundling that into a library shared by multiple applications, then you are going to want to master the concepts within Spring Boot’s Auto-configuration capability that will be discussed here. Some of these Auto-configuration capabilities mentioned can be placed directly into the application while others are meant to be placed into separate Auto-configuration modules called "starter" modules that can come with an opinionated, default way to configure the component for use with as little work as possible.

1.1. Goals

The student will learn to:

  • Enable/disable bean creation based on condition(s) at startup

  • Create Auto-configuration/Starter module(s) that establish necessary dependencies and conditionally supplies beans

  • Resolve conflicts between alternate configurations

  • Locate environment and condition details to debug Auto-configuration issues

1.2. Objectives

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

  1. Enable a @Component, @Configuration class, or @Bean factory method based on the result of a condition at startup

  2. Create Spring Boot Auto-configuration/Starter module(s)

  3. Bootstrap Auto-configuration classes into applications using a Spring Boot 3 org.springframework.boot.autoconfigure.AutoConfiguration.imports metadata file

  4. Create a conditional component based on the presence of a property value

  5. Create a conditional component based on a missing component

  6. Create a conditional component based on the presence of a class

  7. Define a processing dependency order for Auto-configuration classes

  8. Access textual debug information relative to conditions using the debug property

  9. Access web-based debug information relative to conditionals and properties using the Spring Boot Actuator

2. Review: Configuration Class

As we have seen earlier, @Configuration classes are how we bootstrap an application using Java classes. They are the modern alternative to the legacy XML definitions that basically do the same thing — define and configure beans.

@Configuration classes can be the @SpringBootApplication class itself. This would be appropriate for a small application.

Configuration supplied within @SpringBootApplication Class
@SpringBootApplication
//==> wraps @EnableAutoConfiguration
//==> wraps @SpringBootConfiguration
//            ==> wraps @Configuration
public class SelfConfiguredApp {
    public static void main(String...args) {
        SpringApplication.run(SelfConfiguredApp.class, args);
    }

    @Bean
    public Hello hello() {
        return new StdOutHello("Application @Bean says Hey");
    }
}

2.1. Separate @Configuration Class

@Configuration classes can be broken out into separate classes. This would be appropriate for larger applications with distinct areas to be configured.

@Configuration(proxyBeanMethods = false) (2)
public class AConfigurationClass {
    @Bean (1)
    public Hello hello() {
        return new StdOutHello("...");
    }
}
1 bean scope defaults to "singleton"
2 nothing directly calling the @Bean factory method; establishing a CGLIB proxy is unnecessary
@Configuration classes are commonly annotated with the proxyMethods=false attribute that tells Spring not to create extra proxy code to enforce normal, singleton return of the created instance to be shared by all callers since @Configuration class instances are only called by Spring. The javadoc for the annotation attribute describes the extra and unnecessary work saved.

3. Conditional Configuration

We can make @Bean factory methods (or the @Component annotated class) and entire @Configuration classes dependent on conditions found at startup. The following example uses the @ConditionalOnProperty annotation to define a Hello bean based on the presence of the hello.quiet property having the value true.

Property Condition Example
...
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class StarterConfiguredApp {
    public static void main(String...args) {
        SpringApplication.run(StarterConfiguredApp.class, args);
    }

    @Bean
    @ConditionalOnProperty(prefix="hello", name="quiet", havingValue="true") (1)
    public Hello quietHello() {
        return new StdOutHello("(hello.quiet property condition set, Application @Bean says hi)");
    }
}
1 @ConditionalOnProperty annotation used to define a Hello bean based on the presence of the hello.quiet property having the value true

3.1. Property Value Condition Satisfied

The following is an example of the property being defined with the targeted value.

Property Value Condition Satisfied Result
$ java -jar target/appconfig-autoconfig-*-SNAPSHOT-bootexec.jar --hello.quiet=true (1)
...
(hello.quiet property condition set, Application @Bean says hi) World (2)
1 matching property supplied using command line
2 satisfies property condition in @SpringBootApplication
The (parentheses) is trying to indicate a whisper. hello.quiet=true property turns on this behavior.

3.2. Property Value Condition Not Satisfied

The following is an example of when the property is missing. Since there is no Hello bean factory, we encounter an error that we will look to solve using a separate Auto-configuration module.

Property Value Condition Not Satisfied
$ java -jar target/appconfig-autoconfig-*-SNAPSHOT-bootexec.jar (1)
...
***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in info.ejava.examples.app.config.auto.AppCommand required a bean of type 'info.ejava.examples.app.hello.Hello' that could not be found. (2)


Action:

Consider defining a bean of type 'info.ejava.examples.app.hello.Hello' in your configuration.
1 property either not specified or not specified with targeted value
2 property condition within @SpringBootApplication not satisfied

4. Two Primary Configuration Phases

Configuration processing within Spring Boot is separated into two primary phases:

  1. User-defined configuration classes

    • processed first

    • part of the application module

    • located through the use of a @ComponentScan (wrapped by @SpringBootApplication)

    • establish the base configuration for the application

    • fill in any fine-tuning details.

  2. Auto-configuration classes

    • parsed second

    • outside the scope of the @ComponentScan

    • placed in separate modules, identified by metadata within those modules

    • enabled by application using @EnableAutoConfiguration (also wrapped by @SpringBootApplication)

    • provide defaults to fill in the reusable parts of the application

    • use User-defined configuration for details

5. Auto-Configuration

An Auto-configuration class is technically no different from any other @Configuration class except that it is inspected after the User-defined @Configuration class(es) processing is complete and based on being named in a descriptor file within META-INF/. This alternate identification and second pass processing allows the core application to make key directional and detailed decisions and control conditions for the Auto-configuration class(es).

The following Auto-configuration class example defines an unconditional Hello bean factory configured using a @ConfigurationProperties class.

Example Auto-Configuration Class
package info.ejava.examples.app.hello; (2)
...

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(HelloProperties.class)
public class HelloAutoConfiguration {
    @Bean (1)
    public Hello hello(HelloProperties helloProperties) {
        return new StdOutHello(helloProperties.getGreeting());
    }
}
1 Example Auto-configuration class provides unconditional @Bean factory for Hello
2 this @Configuration package is outside the default scanning scope of @SpringBootApplication
Auto-Configuration Packages are Separate from Application

Auto-Configuration classes are designed to be outside the scope of the @SpringBootApplication package scanning. Otherwise, it would end up being a normal @Configuration class and processed within the main application JAR pre-processing.

package info.ejava.examples.app.config.auto;
@SpringBootApplication
package info.ejava.examples.app.hello; (1)

@Configuration(proxyBeanMethods = false)
public class HelloAutoConfiguration {
1 app.hello is not under app.config.auto

5.1. @AutoConfiguration Annotation

Spring Boot 2.7 added the @AutoConfiguration annotation, which

  • extends @Configuration

  • permanently sets proxyBeanMethods to false

  • contains aliases for before/after configuration processing order

import org.springframework.context.annotation.Configuration;
import org.springframework.boot.autoconfigure.AutoConfiguration;

//@Configuration(proxyBeanMethods = false)
@AutoConfiguration
public class HelloAutoConfiguration {

It helps document the purpose of the @Configuration class and provides some ease of use features, but the @Configuration annotation can still be used.

5.2. Supporting @ConfigurationProperties

This particular @Bean factory defines the @ConfigurationProperties class to encapsulate the details of configuring Hello. It supplies a default greeting making it optional for the User-defined configuration to do anything.

Example Auto-Configuration Properties Class
@ConfigurationProperties("hello")
@Data
@Validated
public class HelloProperties {
    @NotBlank
    private String greeting = "HelloProperties default greeting says Hola!"; (1)
}
1 Value used if user-configuration does not specify a property value

5.3. Locating Auto Configuration Classes

A dependency JAR makes the Auto-configuration class(es) known to the application by supplying a metadata file (META-INF/spring/ org.springframework.boot.autoconfigure.AutoConfiguration.imports) and listing the Auto-configuration classes within that file.

The example below shows the metadata file ("META-INF/…​AutoConfiguration.imports") and an Auto-configuration class ("HelloAutoConfiguration") that will be named within that metadata file.

Auto-configuration Module JAR
$ jar tf target/hello-starter-*-SNAPSHOT.jar | egrep -v '/$|maven|MANIFEST.MF'

META-INF/spring-configuration-metadata.json (2)
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (1)
info/ejava/examples/app/hello/HelloProperties.class
info/ejava/examples/app/hello/HelloAutoConfiguration.class
1 "auto-configuration" dependency JAR supplies …​ AutoConfiguration.imports
2 @ConfigurationProperties class metadata generated by maven plugin for use by IDEs
It is common best-practice to host Auto-configuration classes in a separate module than the beans it configures. The Hello interface and Hello implementation(s) comply with this convention and are housed in separate modules.

5.4. META-INF Auto-configuration Metadata File

Auto-configuration classes are registered in the …​ AutoConfiguration.imports file by listing the class' fully qualified name, one per line.

Spring Boot 3 AutoConfiguration Metadata File
# src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

info.ejava.examples.app.hello.HelloAutoConfiguration (1)
info.ejava.examples.app.hello.HelloResourceAutoConfiguration (2)
1 Auto-configuration class registration
2 this class is part of a later example; multiple classes are listed one-per-line

5.5. Spring Boot 2 META-INF/spring.factories

Prior to Spring Boot 2.7, the general purpose META-INF/spring.factories file was used to bootstrap auto-configuration classes. This approach was deprecated in 2.7 and eliminated in Spring Boot 3. If you are ever working with a legacy version of Spring Boot, you will have to use this approach.

Auto-configuration classes were registered using the property name equaling the fully qualified classname of the @EnableAutoConfiguration annotation and the value equaling the fully qualified classname of the Auto-configuration class(es). Multiple classes can be specified separated by commas. The last entry on a line cannot end with a comma.

Spring Boot 2 Auto-Configuration Metadata Entry
# src/main/resources/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  info.ejava.examples.app.hello.HelloAutoConfiguration, \ (1)
  info.ejava.examples.app.hello.HelloResourceAutoConfiguration
1 Auto-configuration class registration
The last line of the property cannot end with a comma or Spring Boot 2 will interpret entry as an empty class name

5.6. Example Auto-Configuration Module Source Tree

Our configuration and properties class — along with the org.springframework.boot.autoconfigure.AutoConfiguration.imports file get placed in a separate module source tree.

Example Auto-Configuration Module Structure
pom.xml
src
`-- main
    |-- java
    |   `-- info
    |       `-- ejava
    |           `-- examples
    |               `-- app
    |                   `-- hello
    |                       |-- HelloAutoConfiguration.java
    |                       `-- HelloProperties.java
    `-- resources
        `-- META-INF
            `-- spring
                `-- org.springframework.boot.autoconfigure.AutoConfiguration.imports

5.7. Auto-Configuration / Starter Roles/Relationships

Modules designed as starters can have varying designs with the following roles carried out:

  • Auto-configuration classes that conditionally wire the application. These lay dormant when for conditions to trigger them at startup. They typically lack implementation code, sticking to choices, configuration, and reasonable defaults.

  • An opinionated starter with dependencies that trigger the Auto-configuration rules

autoconfig modules

5.8. Example Starter Module pom.xml

The module commonly termed a starter and will have dependencies on

Example Auto-Configuration pom.xml Snippet
    <groupId>info.ejava.examples.app</groupId>
    <artifactId>hello-starter</artifactId>

    <dependencies>
        <dependency> (1)
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- commonly declares dependency on interface module -->
        <dependency> (2)
            <groupId>${project.groupId}</groupId>
            <artifactId>hello-service-api</artifactId>
            <version>${project.version}</version>
        </dependency> (2)
        <!-- hello implementation dependency -->
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>hello-service-stdout</artifactId>
            <version>${project.version}</version>
        </dependency>
1 dependency on spring-boot-starter define classes pertinent to Auto-configuration
2 starter modules commonly define dependencies on interface and implementation modules

The rest of the dependencies have nothing specific to do with Auto-configuration or starter modules, and are there to support the module implementation.

5.9. Application Starter Dependency

The application module declares dependency on the starter module containing or having a dependency on the Auto-configuration artifacts.

Application Module Dependency on Starter Module
<!-- takes care of initializing Hello Service for us to inject -->
<dependency>
    <groupId>${project.groupId}</groupId> (1)
    <artifactId>hello-starter</artifactId>
    <version>${project.version}</version> (1)
</dependency>
1 For this example, the application and starter modules share the same groupId and version and leverage a ${project} variable to simplify the expression. That will likely not be the case with most starter module dependencies and will need to be spelled out.

5.10. Starter Brings in Pertinent Dependencies

The starter dependency brings in the Hello Service interface, targeted implementation(s), and some implementation dependencies.

Application Module Transitive Dependencies from Starter
$ mvn dependency:tree
...
[INFO] +- info.ejava.examples.app:hello-starter:jar:6.1.0-SNAPSHOT:compile
[INFO] |  +- info.ejava.examples.app:hello-service-api:jar:6.1.0-SNAPSHOT:compile
[INFO] |  +- info.ejava.examples.app:hello-service-stdout:jar:6.1.0-SNAPSHOT:compile
[INFO] |  +- org.projectlombok:lombok:jar:1.18.10:provided
[INFO] |  \- org.springframework.boot:spring-boot-starter-validation:jar:3.3.2:compile
...

6. Configured Application

The example application contains a component that requests the greeter implementation to say hello to "World".

Injection Point for Auto-configuration Bean
import lombok.RequiredArgsConstructor;
...
@Component
@RequiredArgsConstructor (1)
public class AppCommand implements CommandLineRunner {
    private final Hello greeter; //<== component in App requires Hello injected

    public void run(String... args) throws Exception {
        greeter.sayHello("World");
    }
}
1 lombok is being used to provide the constructor injection

6.1. Review: Unconditional Auto-Configuration Class

This starter dependency is bringing in a @Bean factory to construct an implementation of Hello, that can satisfy the injection dependency.

Example Auto-Configuration Class
package info.ejava.examples.app.hello;
...

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(HelloProperties.class)
public class HelloAutoConfiguration {
    @Bean
    public Hello hello(HelloProperties helloProperties) { (1)
        return new StdOutHello(helloProperties.getGreeting());
    }
}
1 Example Auto-configuration configured by HelloProperties

This bean will be unconditionally instantiated the way it is currently defined.

6.2. Review: Starter Module Default

The starter dependency brings in an Auto-configuration class that instantiates a StdOutHello implementation configured by a HelloProperties class.

Review: Auto-configuration class` Configuration Properties
@ConfigurationProperties("hello")
@Data
@Validated
public class HelloProperties {
    @NotBlank
    private String greeting = "HelloProperties default greeting says Hola!"; (1)
}
1 hello.greeting default defined in @ConfigurationProperties class of starter/autoconfigure module

6.3. Produced Default Starter Greeting

This produces the default greeting

Example Application Execution without Satisfying Property Condition
$ java -jar target/appconfig-autoconfig-*-SNAPSHOT-bootexec.jar
...
HelloProperties default greeting says Hola! World

The HelloAutoConfiguration.hello @Bean was instantiated with the HelloProperties greeting of "HelloProperties default greeting says Hola!". This Hello instance was injected into the AppCommand, which added "World" to the result.

Example of Reasonable Default
This is an example of a component being Auto-configured with a reasonable default. It did not simply crash, demanding a greeting be supplied.

6.4. User-Application Supplies Property Details

Since the Auto-configuration class is using a properties class, we can define properties (aka "the details") in the main application for the dependency module to use.

application.properties
#appconfig-autoconfig-example application.properties
#uncomment to use this greeting
hello.greeting: application.properties Says - Hey
Runtime Output with hello.greeting Property Defined
$ java -jar target/appconfig-autoconfig-*-SNAPSHOT-bootexec.jar
...
application.properties Says - Hey World (1)
1 auto-configured implementation using user-defined property

The same scenario as before is occurring except this time the instantiation of the HelloProperties finds a hello.greeting property to override the Java default.

Example of Configuring Details
This is an example of customizing the behavior of an Auto-configured component.

7. Auto-Configuration Conflict

7.1. Review: Conditional @Bean Factory

We saw how we could make a @Bean factory in the User-defined application module conditional (on the value of a property).

Conditional @Bean Factory
@SpringBootApplication
public class StarterConfiguredApp {
...
    @Bean
    @ConditionalOnProperty(prefix = "hello", name = "quiet", havingValue = "true")
    public Hello quietHello() {
        return new StdOutHello("(hello.quiet property condition set, Application @Bean says hi)");
    }
}

7.2. Potential Conflict

We also saw how to define a @Bean factory in an Auto-configuration class brought in by starter module. We now have a condition where the two can cause an ambiguity error that we need to account for.

Example Output with Bean Factory Ambiguity
$ java -jar target/appconfig-autoconfig-*-SNAPSHOT-bootexec.jar --hello.quiet=true (1)
...
***************************
APPLICATION FAILED TO START
***************************
Description:

Parameter 0 of constructor in info.ejava.examples.app.config.auto.AppCommand
required a single bean, but 2 were found:
        - quietHello: defined by method 'quietHello' in info.ejava.examples.app.config.auto.StarterConfiguredApp
        - hello: defined by method 'hello' in class path resource [info/ejava/examples/app/hello/HelloAutoConfiguration.class]

This may be due to missing parameter name information

Action:

Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
1 Supplying the hello.quiet=true property value causes two @Bean factories to choose from

7.3. @ConditionalOnMissingBean

One way to solve the ambiguity is by using the @ConditionalOnMissingBean annotation — which defines a condition based on the absence of a bean. Most conditional annotations can be used in both the application and autoconfigure modules. However, the @ConditionalOnMissingBean and its sibling @ConditionalOnBean are special and meant to be used with Auto-configuration classes in the autoconfigure modules.

Since the Auto-configuration classes are processed after the User-defined classes — there is a clear point to determine whether a User-defined bean does or does not exist. Any other use of these two annotations requires careful ordering and is not recommended.

@ConditionOnMissingBean Auto-Configuration Example
...
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(HelloProperties.class)
public class HelloAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean (1)
    public Hello hello(HelloProperties helloProperties) {
        return new StdOutHello(helloProperties.getGreeting());
    }
}
1 @ConditionOnMissingBean causes Auto-configured @Bean method to be inactive when Hello bean already exists

7.4. Bean Conditional Example Output

With the @ConditionalOnMissingBean defined on the Auto-configuration class and the property condition satisfied, we get the bean injected from the User-defined @Bean factory.

Runtime with Property Condition Satisfied
$ java -jar target/appconfig-autoconfig-*-SNAPSHOT-bootexec.jar --hello.quiet=true
...
(hello.quiet property condition set, Application @Bean says hi) World

With the property condition not satisfied, we get the bean injected from the Auto-configuration @Bean factory. Wahoo!

Runtime with Property Condition Not Satisfied
$ java -jar target/appconfig-autoconfig-*-SNAPSHOT-bootexec.jar
...
application.properties Says - Hey World

8. Resource Conditional and Ordering

We can also define a condition based on the presence of a resource on the filesystem or classpath using the @ConditionOnResource. The following example satisfies the condition if the file hello.properties exists in the current directory. We are also going to order our Auto-configured classes with the help of the @AutoConfigureBefore annotation. There is a sibling @AutoConfigureAfter annotation as well as a AutoConfigureOrder we could have used.

Example Condition on File Present and Evaluation Ordering
...
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnResource;

@Configuration(proxyBeanMethods = false)
@ConditionalOnResource(resources = "file:./hello.properties") (1)
@AutoConfigureBefore(HelloAutoConfiguration.class) (2)
public class HelloResourceAutoConfiguration {
    @Bean
    public Hello resourceHello() {
        return new StdOutHello("hello.properties exists says hello");
    }
}
1 Auto-configured class satisfied only when file hello.properties present
2 This Auto-configuration class is processed prior to HelloAutoConfiguration

8.1. @AutoConfiguration Alternative

We can use the @AutoConfiguration annotation, which wraps some of our desired settings.

Example @AutoConfiguration Use
import org.springframework.boot.autoconfigure.AutoConfiguration;

@AutoConfiguration(before = HelloAutoConfiguration.class)
//==> wraps @Configuration(proxyBeanMethods = false)
//==> wraps @AutoConfigureBefore(HelloAutoConfiguration.class)
@ConditionalOnClass(StdOutHello.class)
@ConditionalOnResource(resources = "file:./hello.properties")
public class HelloResourceAutoConfiguration {

8.2. Registering Second Auto-Configuration Class

This second Auto-configuration class is being provided in the same, hello-starter module, so we need to update the '…​ AutoConfiguration.imports` file. We do this by listing the second class within the same file.

hello-starter AutoConfiguration.imports
# src/main/resources/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

info.ejava.examples.app.hello.HelloAutoConfiguration
info.ejava.examples.app.hello.HelloResourceAutoConfiguration

8.3. Resource Conditional Example Output

The following execution with hello.properties present in the current directory satisfies the condition, causes the @Bean factory from HelloAutoConfiguration to be skipped because the bean already exists.

Resource Condition Satisfied
$ touch hello.properties

$ java -jar target/appconfig-autoconfig-*-SNAPSHOT-bootexec.jar
...
hello.properties exists says hello World
  • when property file is not present:

    • @Bean factory from HelloAutoConfiguration used since neither property nor resource-based conditions satisfied

Resource Condition Not Satisfied
$ rm hello.properties
$ java -jar target/appconfig-autoconfig-*-SNAPSHOT-bootexec.jar
...
application.properties Says - Hey World

9. @Primary

In the previous example, I purposely put ourselves in a familiar situation to demonstrate an alternative solution if appropriate. We will re-enter the ambiguous match state if we supply a hello.properties file and the hello.quiet=true property value.

Example Ambiguous Conditional Match
$ touch hello.properties
$ java -jar target/appconfig-autoconfig-*-SNAPSHOT-bootexec.jar --hello.quiet=true
...
***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in info.ejava.examples.app.config.auto.AppCommand required a single bean,
  but 2 were found:
        - quietHello: defined by method 'quietHello' in info.ejava.examples.app.config.auto.StarterConfiguredApp
        - resourceHello: defined by method 'resourceHello' in class path resource
      [info/ejava/examples/app/hello/HelloResourceAutoConfiguration.class]


Action:

Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans,
or using @Qualifier to identify the bean that should be consumed

This time — to correct — we want the resource-based @Bean factory to take priority so we add the @Primary annotation to our highest priority @Bean factory. If there is a conflict — this one will be used.

...
import org.springframework.context.annotation.Primary;

@AutoConfiguration(before = HelloAutoConfiguration.class)
@ConditionalOnResource(resources = "file:./hello.properties")
public class HelloResourceAutoConfiguration {
    @Bean
    @Primary //chosen when there is a conflict
    public Hello resourceHello() {
        return new StdOutHello("hello.properties exists says hello");
    }
}

9.1. @Primary Example Output

This time we avoid the error with the same conditions met and one of the @Bean factories listed as @Primary to resolve the conflict.

Ambiguous Choice Resolved through @Primary
$ touch hello.properties

$ java -jar target/appconfig-autoconfig-*-SNAPSHOT-bootexec.jar --hello.quiet=true (1)
...
hello.properties exists says hello World
1 @Primary condition satisfied overrides application @Bean condition

10. Class Conditions

There are many conditions we can add to our @Configuration class or methods. However, there is an important difference between the two.

  • class conditional annotations prevent the entire class from loading when not satisfied

  • @Bean factory conditional annotations allow the class to load but prevent the method from being called when not satisfied

This works for missing classes too! Spring Boot parses the conditional class using ASM to detect and then evaluate conditions before allowing the class to be loaded into the JVM. Otherwise, we would get a ClassNotFoundException for the import of a class we are trying to base our condition on.

10.1. Class Conditional Example

In the following example, I am adding @ConditionalOnClass annotation to prevent the class from being loaded if the implementation class does not exist on the classpath.

...
import info.ejava.examples.app.hello.stdout.StdOutHello; (2)
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(StdOutHello.class) (2)
@EnableConfigurationProperties(HelloProperties.class)
public class HelloAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public Hello hello(HelloProperties helloProperties) {
        return new StdOutHello(helloProperties.getGreeting()); (1)
    }
}
1 StdOutHello is the implementation instantiated by the @Bean factory method
2 HelloAutoConfiguration.class will not get loaded if StdOutHello.class does not exist

The @ConditionOnClass accepts either a class or string expression of the fully qualified classname. The sibling @ConditionalOnMissingClass accepts only the string form of the classname.

Spring Boot Autoconfigure module contains many examples of real Auto-configuration classes

11. Excluding Auto Configurations

We can turn off certain Auto-configured classes using the

@SpringBootApplication(exclude = {})
// ==> wraps @EnableAutoConfiguration(exclude={})
public class StarterConfiguredApp {
    ...
}

12. Debugging Auto Configurations

With all these conditional User-defined and Auto-configurations going on, it is easy to get lost or make a mistake. There are two primary tools that can be used to expose the details of the conditional configuration decisions.

12.1. Conditions Evaluation Report

It is easy to get a simplistic textual report of positive and negative condition evaluation matches by adding a debug property to the configuration. This can be done by adding --debug or -Ddebug to the command line.

The following output shows only the positive and negative matching conditions relevant to our example. There is plently more in the full output.

12.2. Conditions Evaluation Report Example

Conditions Evaluation Report Snippet
$ java -jar target/appconfig-autoconfig-*-SNAPSHOT-bootexec.jar --debug | less
...
============================
CONDITIONS EVALUATION REPORT
============================

Positive matches: (1)
-----------------
   HelloAutoConfiguration matched:
      - @ConditionalOnClass found required class 'info.ejava.examples.app.hello.stdout.StdOutHello' (OnClassCondition)

   HelloAutoConfiguration#hello matched:
      - @ConditionalOnMissingBean (types: info.ejava.examples.app.hello.Hello; SearchStrategy: all) did not find any beans (OnBeanCondition)

Negative matches: (2)
-----------------
   HelloResourceAutoConfiguration:
      Did not match:
         - @ConditionalOnResource did not find resource 'file:./hello.properties' (OnResourceCondition)
      Matched:
         - @ConditionalOnClass found required class 'info.ejava.examples.app.hello.stdout.StdOutHello' (OnClassCondition)

   StarterConfiguredApp#quietHello:
      Did not match:
         - @ConditionalOnProperty (hello.quiet=true) did not find property 'quiet' (OnPropertyCondition)
1 Positive matches show which conditionals are activated and why
2 Negative matches show which conditionals are not activated and why

12.3. Condition Evaluation Report Results

The report shows us that

  • HelloAutoConfiguration class was enabled because StdOutHello class was present

  • hello @Bean factory method of HelloAutoConfiguration class was enabled because no other beans were located

  • entire HelloResourceAutoConfiguration class was not loaded because file hello.properties was not present

  • quietHello @Bean factory method of application class was not activated because hello.quiet property was not found

12.4. Actuator Conditions

We can also get a look at the conditionals while the application is running for Web applications using the Spring Boot Actuator. However, doing so requires that we transition our application from a command to a Web application. Luckily this can be done technically by simply changing our starter in the pom.xml file.

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
<!--            <artifactId>spring-boot-starter</artifactId>-->
        </dependency>

We also need to add a dependency on the spring-boot-starter-actuator module.

        <!-- added to inspect env -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

12.5. Activating Actuator Conditions

The Actuator, by default, will not expose any information without being configured to do so. We can show a JSON version of the Conditions Evaluation Report by adding the management.endpoints.web.exposure.include equal to the value conditions. I will do that on the command line here. Normally it would be in a profile-specific properties file appropriate for exposing this information.

Enable Actuator Conditions Report to be Exposed
$ java -jar target/appconfig-autoconfig-*-SNAPSHOT-bootexec.jar \
  --management.endpoints.web.exposure.include=conditions

The Conditions Evaluation Report is available at the following URL: http://localhost:8080/actuator/conditions.

Example Actuator Conditions Report
{
"contexts": {
  "application": {
    "positiveMatches": {
        "HelloAutoConfiguration": [{
            "condition": "OnClassCondition",
            "message": "@ConditionalOnClass found required class 'info.ejava.examples.app.hello.stdout.StdOutHello'"
            }],
        "HelloAutoConfiguration#hello": [{
            "condition": "OnBeanCondition",
            "message": "@ConditionalOnBean (types: info.ejava.examples.app.hello.Hello; SearchStrategy: all) did not find any beans"
            }],
...
,
    "negativeMatches": {
        "StarterConfiguredApp#quietHello": {
            "notMatched": [{
            "condition": "OnPropertyCondition",
            "message": "@ConditionalOnProperty (hello.quiet=true) did not find property 'quiet'"
            }],
            "matched": []
            },
        "HelloResourceAutoConfiguration": {
            "notMatched": [{
            "condition": "OnResourceCondition",
            "message": "@ConditionalOnResource did not find resource 'file:./hello.properties'"
            }],
            "matched": []
            },
...

12.6. Actuator Environment

It can also be helpful to inspect the environment to determine the value of properties and which source of properties is being used. To see that information, we add env to the exposure.include property.

Enable Actuator Conditions Report and Environment to be Exposed
$ java -jar target/appconfig-autoconfig-*-SNAPSHOT-bootexec.jar \
   --management.endpoints.web.exposure.include=conditions,env

This adds a full /env endpoint and a view specific /env/{property} endpoint to see information for a specific property name. The available Actuator links are available at http://localhost:8080/actuator.

Actuator Links
{
    _links: {
        self: {
            href: "http://localhost:8080/actuator",
            templated: false
    },
        conditions: {
            href: "http://localhost:8080/actuator/conditions",
            templated: false
    },
        env: {
            href: "http://localhost:8080/actuator/env",
            templated: false
    },
        env-toMatch: {
            href: "http://localhost:8080/actuator/env/{toMatch}",
            templated: true
        }
    }
}

12.8. Actuator Environment Report

The Actuator Environment Report is available at http://localhost:8080/actuator/env.

Example Actuator Environment Report
{
activeProfiles: [ ],
propertySources: [{
        name: "server.ports",
        properties: {
            local.server.port: {
                value: 8080
                }
            }
    },
    {
        name: "commandLineArgs",
        properties: {
            management.endpoints.web.exposure.include: {
                value: "conditions,env"
                }
            }
    },
...

12.9. Actuator Specific Property Source

The source of a specific property and its defined value is available below the /actuator/env URI such that the hello.greeting property is located at http://localhost:8080/actuator/env/hello.greeting.

Example Actuator Environment Report for Specific Property
{
  property: {
  source: "applicationConfig: [classpath:/application.properties]",
  value: "application.properties Says - Hey"
},
...

12.10. More Actuator

We can explore some of the other Actuator endpoints by changing the include property to * and revisiting the main actuator endpoint. Actuator Documentation is available on the web.

Expose All Actuator Endpoints
$ java -jar target/appconfig-autoconfig-*-SNAPSHOT-bootexec.jar \
   --management.endpoints.web.exposure.include="*" (1)
1 double quotes ("") being used to escape * special character on command line

13. Summary

In this module we:

  • Defined conditions for @Configuration classes and @Bean factory methods that are evaluated at runtime startup

  • Placed User-defined conditions, which are evaluated first, in with application module

  • Placed Auto-configuration classes in separate starter module to automatically bootstrap applications with specific capabilities

  • Added conflict resolution and ordering to conditions to avoid ambiguous matches

  • Discovered how class conditions can help prevent entire @Configuration classes from being loaded and disrupt the application because an optional class is missing

  • Learned how to debug conditions and visualize the runtime environment through use of the debug property or by using the Actuator for web applications