JAX London Blog

JAX London, 4-7 October 2021
The Conference for Java & Software Innovation
28
Sep

Spring Boot Auto Configuration

Spring Boot has always fascinated me. No more fiddling around with different versions of application servers and different environments between local development, testing and production. If you deal with Spring Boot, you quickly come across the term Auto Configuration - a term that most people interpret as "Spring Boot does it right somehow". But what exactly is right?

 

Spring Boot has always fascinated me. No more fiddling around with different versions of application servers and different environments between local development, testing, and production. When dealing with Spring Boot, you will quickly come across the term Auto Configuration – a term that most people interpret as “One way or another, Spring Boot does it correctly”. But what exactly is correct?

 

How does Spring Boot decide the infrastructure needed for a REST service or connection to a MongoDB? Let’s take a look behind the scenes of the Auto-Configuration concept and then look at how to create your own Auto Configurations.

Article series

Part 1: Understanding and creating Auto Configuration

 

Part 2: Externalized Configuration

 

Typically, Spring Boot and its features are explained in the form of tutorials in which the complete infrastructure for a REST service is provided in one class. Listing 1 shows such an example.

LISTING 1: SIMPLE SPRING-BOOT EXAMPLE

 

@SpringBootApplication
@RestController
public class SimpleWebApplication {
 
  @GetMapping("/hello")
  public String getHello() {
    return "Hello World!";
  }
 
  public static void main(String[] args) {
    SpringApplication.run(SimpleWebApplication.class, args);
  }
}

 

Of course, it is not a good idea to integrate the REST controller code directly into the start class. Nevertheless, it is an impressive example of the power and convenience of Spring Boot. What Spring Boot does here is quite remarkable. Based on the code in Listing 1 and the dependencies of the project, Spring Boot has to identify, configure, and deploy the necessary infrastructure: In this particular case a Web Application Server, the marshalling and unmarshalling of REST requests and, last but not least, the routing to the correct methods.

 

This is where most tutorials end and leave us impressed by the comfort that Spring Boot offers us. But why does the above code work? We will answer this question below. However, we need a good understanding of a basic concept: annotation composition.

Annotation composition

Spring Boot uses various annotations in many places. If we take the example from Listing 1, we already see three annotations: @SpringBootApplication, @RestController, and @GetMapping. Annotations, therefore, seem to be an important part of Spring Boot. These annotations are based on a concept that is not specific to Spring Boot, but originates from the Spring Framework itself: “Meta-annotations” and annotation composition based on them.

 

Let us start with meta-annotations: these are annotations that can be applied to other annotations. If we look at Spring’s @Service annotation, we see that @Service is annotated with @Component. Since @Component can be applied to an annotation, this is a meta-annotation.

 

Now, what is annotation composition? Let’s look at the annotation @RestRepository. Here it is noticeable that it was annotated with @Controller and @ResponseBody. Thus, @Controller and @ResponseBody are both also meta-annotations. Therefore, @RestController is a composition of @Controller and @ResponseBody.

 

The meta-annotation model (Box: “Digression: Java Meta Annotations”) is by no means an invention of the Spring Framework. Rather, it is a concept that Java itself offers.

Digression: Java meta-annotations

Java relies on the meta-annotation model to create its own annotations. The behavior and possibilities are expressed by meta-annotations. There are various meta-annotations predefined in the Java Runtime Library. The most commonly used annotations are discussed in more detail below.

 

@Target

 

Annotations can be applied to a wide variety of elements, including a type declaration (classes, interfaces, enums), fields, methods, and several other places. The @Target annotation limits what the annotation can be applied to. If, for example, you want to restrict an annotation to fields only, you can do this using @Target({ElementType.FIELD}).

 

@Retention

 

By default, annotations are preserved in the .class file, but are not loaded at runtime. If you want to read the annotation using Reflection, you must define it using @Retention(RetentionPolicy.RUNTIME).

 

@Documented

 

Simply put, @Documented defines whether the custom annotation appears in the Javadoc documentation of an annotated class. However, the meaning of @Documented goes beyond this and defines that an annotation is part of the public contract – and thus has the same value as the methods of a Public API.

 

@Inherited

 

By default, annotations are not inherited. If class A was given the annotation @X, the annotation does not apply to a class B derived from A. If you want the annotation @X to be valid for subclasses as well, @Inherited can do that for the annotation @X.

On the trail of Spring Boot Magic

Having considered the basic concept of meta-annotations, we now look deeper into the magic of Spring Boot. Let’s go back to the example from Listing 1, where we find the central annotation of Spring Boot: @SpringBootApplication. @SpringBootApplication is an annotation composition that is meta-annotated with several annotations. Listing 2 shows an excerpt from the source code of @SpringBootApplication.

LISTING 2: @SPRINGBOOTAPPLICATION

 

/* ... */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
  @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
  @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class)
})
public @interface SpringBootApplication {
  /* ... */
}

The first four annotations are meta-annotations of the Java Runtime Library, whose importance for the current consideration is secondary. For us, the three subsequent annotations are more interesting at this point: @SpringBootConfiguration, @EnableAutoConfiguration, and @ComponentScan. These annotations are themselves also meta-annotated, resulting in a hierarchy as shown in Figure 1.

Figure 1: The Meta-Annotation Hierarchy of @SpringBootApplication

 

With @ComponentScan you can define that the classpath should be searched for classes annotated with @Component. This way Spring finds classes annotated with @Service, for example. @SpringBootConfiguration is meta-announced with @Configuration, which means that @SpringBootApplication is also a Spring configuration. This detail is the reason why it is possible to integrate @Bean definitions directly into the class annotated with @SpringBootApplication.

 

While the above annotations are interesting, we are looking for the magic of Spring Boot that automatically provides a suitable application configuration. There is one annotation that immediately catches the eye: @EnableAutoConfiguration. We have arrived at the core of Spring Boot magic! Of course, it’s not the annotation itself that does the magic, but an @Import that points to AutoConfigurationImportSelector. Usually, @Import is used to add more Spring configurations to the application context. However, in addition to classes annotated with @Configuration, @Import may also refer to classes that implement either ImportSelector or ImportBeanDefinitionRegistrar. Both are Spring interfaces that allow you to influence the structure of the application context.

 

The ImportSelector is interesting for the understanding of Spring Boot, because the class AutoConfigurationImportSelector implements exactly this interface. An ImportSelector can programmatically decide which Spring configurations should be added to the application context. This mechanism is used for loading the Auto Configurations. What exactly Auto Configurations are and how they work will be discussed later in this article, because we should find out where they come from first.

 

The AutoConfigurationImportSelector uses another, lesser-known Spring feature here: the SpringFactoriesLoader and the associated META-INF/spring.factories files. The spring.factories are files in properties format, with a class name as key and a list of associated classes, each separated by a comma. Listing 3 shows an excerpt from Spring Boot’s META-INF/spring.factories.

LISTING 3: EXTRACT FROM META-INF/SPRING.FACTORIES

 

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudServiceConnectorsAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
# ...

It is important that there are not just one META-INF/spring.factories, but several. If you search for spring.factories in the IDE of your choice in a Spring Boot based project, you will find multiple files. The task of the SpringFactoriesLoader is to aggregate them and make them available transparently at runtime. For the caller, it is irrelevant whether the list of class names comes from one file or from several. This forms the basis for implementing your own Auto Configurations and starters.

 

Try to list all available Auto Configurations yourself. For this, we use the same logic as the AutoConfigurationImportSelector. The corresponding code example can be found in Listing 4.

LISTING 4: SPRINGFACTORIESEXPLORATION

 

package de.digitalfrontiers.springboot.springfactories;
 
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.core.io.support.SpringFactoriesLoader;
import java.util.List;
 
public class SpringFactoriesExplorationApplication {
  public static void main(String[] args) {
    final List<String> candidates = SpringFactoriesLoader
      .loadFactoryNames(EnableAutoConfiguration.class, null);
    candidates.forEach(System.out::println);
  }
}

Auto Configuration

Now that we know how Auto Configurations are found, the question, of course, arises as to what exactly an Auto Configuration actually is. The answer is surprisingly simple: All entries that appear in the spring.factories for EnableAutoConfiguration are themselves normal Spring configurations (classes, annotated with @Configuration). What makes Auto Configurations so special is when they are loaded. The AutoConfigurationImportSelector is executed very late in the application context. So @Import declarations are taken into account beforehand, and the component scan also takes place before. Thus the Application Context already contains all Bean definitions of the application before the Auto Configurations are considered. This makes it possible to attach conditions to the Bean declarations in the Auto Configurations. These conditions are described by @ConditionalOn annotations (box: “Overview of @ConditionalOn Annotations”). We will look at these annotations later in a concrete example.

Overview of @ConditionalOn Annotations

Spring Boot offers many different @ConditionalOn annotations for different purposes. The most common annotations are described below.

 

@ConditionalOnProperty

 

A property is expected, which may also have a certain value. This condition is suitable, for example, for implementing feature toggles.

 

@ConditionalOnBean, @ConditionalOnMissingBean

 

With these annotations, it can be expressed that a Bean of a certain type exists in the application context (@ConditionalOnBean) or that there are no Beans of this type (@ConditionalOnMissingBean). This is discussed in more detail in the section “Conditional Bean Definitions”.

 

@ConditionalOnClass, @ConditionalOnMissingClass

 

Similar to the annotations @ConditionalOnBean and @ConditionalOnMissingBean, these annotations define an expectation: either that a class is available on the classpath or that it is not.

 

@ConditionalOnJava

 

If certain configurations or Beans are dependent on a certain Java runtime version, this can be expressed using this annotation.

 

@ConditionalOnResource

 

This condition expects that resources are available. All resource paths supported by Spring are possible, for example, classpath:/index.html.

 

@ConditionalOnWebApplication, @ConditionalOnNotWebApplication

 

Bean declarations can be created with these annotations, depending on whether the application is a web application or not. With @ConditionalOnWebApplication you can additionally differentiate whether it is a servlet-based or reactive web application.

 

When I discovered Spring Boot for myself, my expectation was that Spring Boot would first provide the necessary infrastructure before my Bean declarations were considered. Interestingly, the concept is the other way around. First, the application and the application context are loaded, and then Spring Boot looks (more precisely: looks at the Auto Configurations) at what is still needed.

Create your own Auto Configurations and starters

With the basic understanding of Auto Configuration we can now start with our own Auto Configurations. The Auto Configuration for the SSH server of the Apache MINA project will serve as an example. The goal is to integrate an SSH server into a spring boot application. All of the following examples can be found in the corresponding GitHub project.

 

Let us start with a simple application where the setup for the SSH server is implemented statically (Listing 5).

LISTING 5: SSHSERVERAPPLICATION.JAVA

 

@SpringBootApplication
public class SshServerApplication {
 
  @Bean(initMethod = "start", destroyMethod = "stop")
  public SshServer sshServer(ShellFactory shellFactory) {
    final ServerBuilder builder = ServerBuilder.builder();
    final SshServer server = builder.build();
    server.setPort(8022);
 
    server.setKeyboardInteractiveAuthenticator(
      new DefaultKeyboardInteractiveAuthenticator());
    server.setPasswordAuthenticator(passwordAuthenticator());
    server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());
 
    server.setShellFactory(shellFactory);
 
    return server;
  }
 
  private PasswordAuthenticator passwordAuthenticator() {
    return (username, password, session) ->
      "admin".equals(username) && "password".equals(password);
  }
 
  @Bean
  public ShellFactory shellFactory() {
    return new NoopShellFactory();
  }
 
  public static void main(String[] args) throws Exception {
    SpringApplication.run(SshServerApplication.class, args);
  }

The application is simply structured. It is already a Spring Boot application with the declaration of two Beans. One Bean is the SshServer itself. Here the server is configured with a port, the authentication is initialized and a so-called ShellFactory is set. The ShellFactory provides the user with an interactive shell after a successful SSH connection setup. MINA does not specify how this shell is implemented. Since the development of a complete bash equivalent would go beyond the scope of this article, we use a NoopShellFactory (Listing 6) in this example, which simply outputs “noop” when called and closes the connection.

LISTING 6: NOOPSHELLFACTORY

 

public class NoopShellFactory implements ShellFactory {
  @Override
  public Command createShell(ChannelSession channel) throws IOException {
    return new AbstractCommandSupport("noop-shell", channel.getExecutorService()) {
      @Override
      public void run() {
        final var out = new PrintWriter(getOutputStream());
        out.println("noop");
        out.flush();
        getExitCallback().onExit(0);
      }
    };
  }
}

If we now want to equip another application with an SSH server, we have different options: either copying the code into the new application or extracting the logic and making it reusable. Of course, we don’t want to copy the code, but we decide to use a reusable auto configuration.

 

The first step is to extract the Bean declarations into a custom Spring configuration. Based on the example from Listing 5, this results in a class annotated with @Configuration, which we call MinaSSHDAutoConfiguration. However, this new class must neither be in the scanning area of the component scan nor be imported using @Import. In our example, the Spring application from Listing 5 is in the package de.digitalfrontiers.springboot.example. Therefore the MinaSSHDAutoConfiguration must not be in this package or in a package below. However, if we take de.digitalfrontiers.springboot.mina.autoconfigure as a package name, the MinaSSHDAutoConfiguration is outside the component scan area.

 

For Spring Boot to find the Auto Configuration, it must be entered in META-INF/spring.factories:

 

1

2

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\

  de.digitalfrontiers.springboot.mina.autoconfigure.MinaSSHDAutoConfiguration

Conditional Bean Definitions

We have now achieved that the configuration of SshServer can also be used by other applications. However, the configuration is always identical for all applications, which is certainly not sufficient. For example, if an application needs a special ShellFactory to retrieve monitoring information via SSH, it must be possible to replace the preconfigured NoopShellFactory. It is not sufficient to define another Bean of the type ShellFactory, because then two ShellFactory beans exist and it cannot be decided which of them should be used. If you remove the NoopShellFactory declaration in our MinaSSHDAutoConfiguration, a ShellFactory must always be defined in the application context of the application, even if the default NoopShellFactory would be sufficient for the application. The idea behind Auto Configurations is that they start with sensible defaults and step aside when the defaults are no longer sufficient. Here this is the NoopShellFactory. This should be used by default unless a Bean of type ShellFactory is explicitly declared. This can be achieved with the @ConditionalOn annotations, in this case with @ConditionalOnMissingBean. Listing 7 contains the resulting MinaSSHDAutoConfiguration.

LISTING 7: MINASSHDAUTOCONFIGURATION

 

@Configuration
@ConditionalOnClass(SshServer.class)
public class MinaSSHDAutoConfiguration {
 
  @Bean(initMethod = "start", destroyMethod = "stop")
  @ConditionalOnMissingBean
  public SshServer sshServer(ShellFactory shellFactory, PasswordAuthenticator passwordAuthenticator) {
    final ServerBuilder builder = ServerBuilder.builder();
    final SshServer server = builder.build();
    server.setPort(8022);
 
    server.setKeyboardInteractiveAuthenticator(
      new DefaultKeyboardInteractiveAuthenticator());
    server.setPasswordAuthenticator(passwordAuthenticator);
    server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());
 
    server.setShellFactory(shellFactory);
 
    return server;
  }
 
  @Bean
  @ConditionalOnMissingBean
  public PasswordAuthenticator passwordAuthenticator() {
    return (username, password, session) ->
      "admin".equals(username) && "password".equals(password);
  }
 
  @Bean
  @ConditionalOnMissingBean
  public ShellFactory shellFactory() {
    return new NoopShellFactory();
  }
}

A @Bean declaration annotated with @ConditionalOnMissingBean is only considered if a declaration does not already exist in the application context. In concrete terms, this means that applications contain an SSH server by default, which uses the NoopShellFactory. However, applications that want to use a specific implementation of the ShellFactory only need to declare a Bean of the type ShellFactory.

 

The attentive reader probably has not missed the additional annotation @ConditionalOnClass in the Auto Configuration’s class declaration. This annotation expresses that the entire MinaSSHDAutoConfiguration is only applicable if the class SshServer exists on the classpath. If SshServer cannot be found, the entire Auto Configuration is ignored. This opens up interesting possibilities for structuring the Auto Configuration as well as possible starters.

Auto Configuration and Starter

So far we have not yet looked at the starters and their importance. But after we have created the first own Auto Configuration, now is the right time to do so.

 

Auto Configurations take care of the configuration of libraries if they are located on the classpath. A starter takes care of exactly this classpath by taking over dependency management. We need the dependency of the actual library (in our example sshd-mina) and the library that contains our recently created MinaSSHDAutoConfiguration. The Spring Boot documentation recommends splitting the dependency into two modules: One is the respective starter, the second contains the Auto Configuration. Figure 2 shows the exemplary implementation.

Fig. 2: Starter and Auto-Configure dependencies

 

Thus, the Spring Boot application defines a dependency on the mina-sshd-spring-boot-starter. And in the spirit of Spring Boot, this is the only thing the application developer has to do in order to benefit from Auto Configuration and dependency management. The starter has dependencies to all libraries relevant for this starter (here: sshd-mina) as well as the appropriate autoconfigure modules. This structure allows a clear separation of responsibilities: The starter is responsible for dependency management, the autoconfigure module for providing the autoconfiguration.

 

Therefore, it is noticeable that the dependency of the module mina-autoconfigure on sshd-mina was specified as optional. On the other hand, it is also noticeable that the starter contains the addition sshd in the module name, but this is not the case with mina-autoconfigure. The latter is not a typo but was chosen deliberately.

 

Mina offers a number of different libraries based on the Mina core library. Take for example the support for SFTP provided by Mina. Since this is also based on the SshServer, it makes sense to implement the corresponding Auto Configuration in the same module as MinaSSHDAutoConfiguration (in mina-autoconfigure). The dependency on the necessary library is then also declared as optional and finally, a suitable starter is provided. The result is a module structure as shown in Figure 3.

Fig. 3: New module structure with support for SFTP

 

This allows a developer to decide whether they need only SSH or SFTP as well – primarily by setting a dependency. Which Auto Configuration is activated is primarily decided by the classpath and by the annotation @ConditionalOnClass.

Conclusion

The Auto-Configuration concept is as simple as it is ingenious. Once you have started to think and develop in the structure of Auto Configuration and Starter, there are many possibilities. Copied code (which we all know exists far too often) can be reduced and unified. Corporate conventions can be implemented in Starter and Auto Configuration and no longer just in long, rarely updated procedural instructions.

Outlook

But it does not only end with Auto Configurations and starters. Everyone who has developed with Spring Boot knows the application.yaml or application.properties. They can be used to configure parameters such as ports. We have also set a port in our SSH server code example. Wouldn’t it be extremely helpful if you could easily change this port using an entry in the application.yaml? This is exactly what Spring Boot has another concept for, with features like validation and type safety, called “Externalized Configuration”. We will discuss this concept in the next part of this series.

 

Now we have created our own Starter and Auto Configurations, and it would be really great if you could get Spring Initializr to offer your own starters. And typical for the Spring ecosystem: This is also possible! How exactly, we will see in the second part of this article series.

Behind the Tracks

Software Architecture & Design
Software innovation & more
Microservices
Architecture structure & more
Agile & Communication
Methodologies & more
Emerging Technologies
Everything about the latest technologies
DevOps & Continuous Delivery
Delivery Pipelines, Testing & more
Cloud & Modern Infrastructure
Everything about new tools and platforms
Big Data & Machine Learning
Saving, processing & more