Multiple ways to configure Spring

Nicolas Fränkel - Sep 26 '21 - - Dev Community

Two weeks ago, I wrote how you could write a Spring application with no annotations. Many alternatives are available to configure your Spring app. I'd like to list them in this post, leaving Spring Boot out of the picture on purpose.

Core concepts

A couple of concepts are central in Spring. The related documentation doesn't describe most of them. Here is my understanding of them:

  • Bean: A bean is an object managed by the Spring IoC container
  • Bean Factory: A bean factory is a component that manages the lifecycle of beans, especially regarding the instantiation of new objects
  • Bean Definition: A bean definition is the set of properties given to the Spring container and that the requested bean will have, e.g., class, name, scope, dependencies, etc.
  • Context: A context is a bean factory with additional capabilities (mainly internationalization and event publishing)

Spring core concepts

To configure a Spring application, one creates one or more contexts and registers the necessary bean definitions in the desired ones. In the following sections, we will configure the following simple model:

A simple application model

Property file

Yes, you read that well: you can actually configure beans via a property file. It was the first way to do it, and though it's deprecated, it still works.

We first need to create the relevant property file:

john.(class)=ch.frankel.blog.Person       # 1
john.$0=John Doe                          # 2
jane.(class)=ch.frankel.blog.Person       # 3
jane.$0=Jane Doe                          # 4
jane.$1(ref)=john                         # 5
Enter fullscreen mode Exit fullscreen mode
  1. Define a new bean with name john and of class Person
  2. Set the single constructor argument to pass
  3. Define a new bean with name jane and of class Person
  4. Set the first constructor argument to pass
  5. Set the second constructor argument to give, the reference to john

Here's the related code snippet:

var context = new GenericApplicationContext();                // 1
var factory = context.getDefaultListableBeanFactory();        // 2
var reader = new PropertiesBeanDefinitionReader(factory);     // 3
var properties = new ClassPathResource("beans.properties");   // 4
reader.loadBeanDefinitions(properties);                       // 5
context.refresh();                                            // 6
Enter fullscreen mode Exit fullscreen mode
  1. Create a new empty context
  2. Get its underlying bean factory
  3. Create a reader over the bean factory
  4. Get a handle on the above property file
  5. Parse the file to create bean definitions in the context
  6. Instantiate the beans from the beans definitions

XML

XML is the way most developers think about when they configure a Spring application. It has been available for ages and still is today. To use it, we only have to transform the previous property file to XML format:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="john" class="ch.frankel.blog.Person">
        <constructor-arg value="John Doe" />
    </bean>
    <bean id="jane" class="ch.frankel.blog.Person">
        <constructor-arg value="Jane Doe" />
        <constructor-arg ref="john" />
    </bean>
</beans>
Enter fullscreen mode Exit fullscreen mode

Because of its widespread usage, configuring a context and populating it with beans can be implemented in a one-liner:

var context = new ClassPathXmlApplicationContext("beans.xml");   // 1
Enter fullscreen mode Exit fullscreen mode
  1. Create the application context, parse the XML file, create the bean definitions, and refresh the context!

Groovy DSL

One can alternatively also use Groovy. For that, Spring provides a dedicated DSL.

import ch.frankel.blog.Person

beans {
    john Person, 'John Doe'
    jane Person, 'Jane Doe', john
}
Enter fullscreen mode Exit fullscreen mode

To use it for configuration is also a one-liner:

var context = new GenericGroovyApplicationContext("beans.groovy");
Enter fullscreen mode Exit fullscreen mode

Just remember that Groovy is not a first-class citizen in the Spring ecosystem anymore.

Self-annotated classes

When Spring introduced self-annotated classes not long after Java 5, people considered them a significant improvement over XML. With this approach, you add annotations to your code that Spring recognizes at startup time. For me, it's a bit odd to use Spring to make one's code more decoupled and to end up coupling it to a third-party framework.

Anyway, here's how we can configure Spring for the above model:

@Component                                              // 1
class John extends Person {
    public John() {
        super("John Doe");
    }
}

@Component                                              // 1
class Jane extends Person {
    public Jane(John john) {                            // 2
        super("Jane Doe", john);
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Mark the class for registration. Spring will instantiate a bean named after the class name, unqualified and uncapitalized
  2. Inject the bean of class John. Alternatively, we could inject by name by having the parameter @Qualifier("john") person. Note that since it's auto-wiring, we need to reduce the number of candidates to one, and there are two Person beans.

It is the most complex code of all, as we want to create two instances of the same class. Hence, with annotated classes, we need to create two different subclasses to annotate them.

To create the context with self-annotated classes is straightforward:

var context = new AnnotationConfigApplicationContext(John.class, Jane.class);
Enter fullscreen mode Exit fullscreen mode

Note that you need to explicitly list all the necessary classes you want to be part of the context. Spring Boot makes it easier for you by implementing classpath scanning, so you don't need explicit listing.

Configuration classes

As mentioned above, self-annotated classes have a couple of downsides:

  • Coupling with the framework
  • Confusing then bean with the class, and thus requiring subclassing when we need different instances of the same class

To fix those issues, we can keep using annotations but move them to a dedicated class. This class plays the same role as the former beans.xml.

@Configuration                                                 // 1
public class AppConfiguration {

    @Bean                                                      // 2
    public Person john() {
        return new Person("John Doe");
    }

    @Bean                                                      // 2
    public Person jane(@Qualifier("john") Person person) {     // 3
        return new Person("Jane Doe", person);
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Mark the class as a configuration class
  2. The container will register the return value of this method as a bean
  3. As the context contains two Person beans, we need to inject by name

As usual, it's straightforward to create a context from the above configuration class:

var context = new AnnotationConfigApplicationContext(ClassConfigurator.class);
Enter fullscreen mode Exit fullscreen mode

Kotlin DSL

The Kotlin DSL is the latest newcomer to the available alternatives. It avoids the usage of annotations.

GenericApplicationContext().apply {             // 1
    beans {                                     // 2
        bean("john") {                          // 3
            Person("John Doe")
        }
        bean("jane") {                          // 3
            Person("Jane Doe", ref("john"))     // 4
        }
    }.initialize(this)                          // 5
    refresh()                                   // 6
}
Enter fullscreen mode Exit fullscreen mode
  1. Instantiate a new context
  2. Create the bean definition DSL
  3. Define a named bean
  4. Inject the dependency by name
  5. Add the bean definitions to the context
  6. Instantiate the beans from the beans definitions

Bean definitions

All the previous configuration alternatives provide an abstraction layer over bean definitions. Then, the container creates beans out of bean definitions. We can bypass these abstraction layers and directly use the bean definition API.

Bean Definition abridged class diagram

Let's first define a specialized bean definition:

public class PersonBeanD extends GenericBeanDefinition {

    public PersonBeanDefinition(String name) {
        this(name, null);
    }

    public PersonBeanDefinition(String name, String manager) {
        setBeanClass(Person.class);                                               // 1
        var arguments = new ConstructorArgumentValues();
        arguments.addGenericArgumentValue(name, "java.lang.String");              // 2
        if (manager != null) {
            arguments.addGenericArgumentValue(manager, "ch.frankel.blog.Person"); // 2
        }
        setConstructorArgumentValues(arguments);                                  // 3
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Set the bean class
  2. Set the argument and its type
  3. Set the arguments

We can now create the context:

var context = new GenericApplicationContext();                               // 1
context.registerBeanDefinition("john", new PersonBeanD("John Doe"));         // 2
context.registerBeanDefinition("jane", new PersonBeanD("Jane Doe", "john")); // 2
context.refresh();                                                           // 3
Enter fullscreen mode Exit fullscreen mode
  1. Create the context
  2. Register the bean definition
  3. Instantiate the beans from the beans definitions

Beans

Spring provides a simple API for simple bean definitions, so we don't need to create dedicated bean definition classes. This mechanism creates such a definition when necessary.

Bean registration API

Here's the code to create the sample configuration:

var context = new GenericApplicationContext();
context.registerBean("john", Person.class, "John Doe");
context.registerBean("jane", Person.class, "Jane Doe", "john");
context.refresh();
Enter fullscreen mode Exit fullscreen mode

Conclusion

Spring is based on several core concepts: bean factories, contexts, bean definitions, and beans.

It offers different ways to create contexts and beans and to register the former in the latter. It ranges from deprecated Java properties to Kotlin DSL. Now, you can choose the one the best adapted to your context.

The complete source code for this post can be found on Github:

To go further:

Originally published at A Java Geek on September 26th, 2021

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .