All You Need to Know about Spring’s Java Config

Table Of Contents

Welcome to the exciting world of Spring’s Java Configuration!

In this comprehensive guide, we will learn Spring’s Java-based configuration. We will get familiar with core annotations like @Bean and @Configuration. We will explore the ways to organize configuration logic, delve into modular setups, and tailor configurations with ease. By the end, we will not only grasp the fundamentals but also be equipped to create well configured Spring applications.

Let us dive in and transform our Spring development experience!

Example Code

This article is accompanied by a working code example on GitHub.

Understanding Spring Configuration

In Spring based applications, we speak of “configuration” when we want to describe how our application context is made up of different, potentially related and interacting beans. It serves as the blueprint, guiding the behavior of our Spring-powered creations.

Configuration in Spring is all about empowering our applications with context, specifying how beans (Spring-managed objects) are created, wired, and used within the application.

Traditionally, Spring offered XML-based configuration, providing a structured way to define beans and their relationships. As the Spring ecosystem evolved, XML configurations have been replaced by more intuitive, expressive, and Java-centric approaches, simplifying the application configuration.

Spring Configuration Essential Features

Java-based configuration in Spring enables us to use plain Java classes (POJOs) and annotations to configure our application context. With Java-based configuration, we do not need XML files. Instead, we use Java code to define our beans and their relationships.

In the upcoming sections, we will learn Spring’s Java Configuration, exploring its core concepts, understanding annotations like @Bean and @Configuration, and know better ways of organizing and managing our Spring applications.

Let us first recap the essentials: beans, IoC, and DI.

Beans: The Core of Spring

In the world of Spring, beans are the fundamental building blocks of our application. Beans are Java objects managed by the Spring container in what is called the “application context”. Beans represent the various components of our application, from simple data objects to complex business logic. Spring creates these beans, manages their lifecycle, and wires them together, makes our application cohesive and organized.

Inversion of Control (IoC)

IoC, one of the main building blocks of Spring, is a concept where the control over the object creation and lifecycle is transferred from our application to the Spring container. Spring ensures that our beans are created, configured, and managed without our direct intervention.

Dependency Injection (DI)

Dependency Injection is the process where beans receive their dependencies from an external source, typically the Spring container, rather than creating them internally. With DI, our beans are ready to use, making our code cleaner, more modular, and easier to test. Spring handles the ‘injection’ of dependencies, ensuring our beans work seamlessly together in our application recipe.

👉 Learn more about beans, IoC, and DI in our article Dependency Injection and Inversion of Control.

Types of Spring Configuration

Spring offers various types of configuration options, each tailored to specific needs. Let us get familiar with them.

XML Configuration

It is the classic approach. Configure Spring beans and dependencies using XML files. Perfect for legacy systems and scenarios requiring external configuration. We’re not going to dive into XML configuration in this article, however.

Java-Based Configuration

  • Use annotations like @Component, @Service, and @Repository to define beans.
  • Configure beans with Java classes using @Configuration and @Bean.
  • Combine Java-based configuration with annotation scanning for effortless bean discovery.

Manual Java-based configuration can be used in special scenarios where custom configuration and more control is needed.

In the sections to follow, we will learn about customizations offered by Java-based configuration.

Enter Spring’s core concepts: @Bean and @Configuration.

Container Configuration vs. Application Configuration

In this article, we’re talking about the configuration of the Spring container (the “application context”). This configuration is about deciding which beans to include in the container and how to instantiate them.

Another aspect of configuration is to configure the way our application behaves by defining application properties in YAML or properties files. While those properties can also influence the way the application context is initialized, we’re not looking into application properties in this article.

To learn more about configuration using properties read our article Configuring a Spring Boot Module with @ConfigurationProperties.

Understanding @Bean

In Spring, @Bean transforms ordinary methods into Spring-managed beans. These beans are objects managed by the Spring IoC container, ensuring centralized control and easier dependency management. In our Employee Management System, @Bean helps create beans representing employees and departments.

Examples of using @Bean:

public class Employee {
    // Employee properties and methods
}

public class Department {
    // Department properties and methods
}

@Configuration
@ComponentScan(basePackages = "io.reflectoring.javaconfig")
public class JavaConfigAppConfiguration {

    @Bean(name = "newEmployee", initMethod = "init", destroyMethod = "destroy")
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    @Cacheable(cacheNames = "employeesCache")
    public Employee newEmployee(final String firstName, final String lastName) {
        return Employee.builder().firstName(firstName).lastName(lastName).build();
    }

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Department newDepartment(final String deptName) {
        final Department department = Department.builder().name(deptName).build();
        acme().addDepartment(department);
        return department;
    }

    @Bean(name = "founder")
    @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
    public Employee founderEmployee() {
        final Employee founder = newEmployee("Scott", "Tiger");
        founder.setDesignation("Founder");
        founder.setDepartment(coreDepartment());
        return founder;
    }

    @Bean(name = "core")
    @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
    public Department coreDepartment() {
        return newDepartment("Core");
    }

    @Bean(name = "acme")
    @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
    public Organization acme() {
        final Organization acmeCo = new Organization();
        acmeCo.setName("Acme Inc");
        return acmeCo;
    }
}


Here, the newEmployee() and founderEmployee() methods create an Employee bean, newDepartment() and coreDepartment() methods create a Department bean and acme() method creats an Organization bean. Spring now manages these objects, handling their lifecycle and ensuring proper dependencies.

Spring Bean Lifecycle: A Summary

The lifecycle of a Spring bean involves its instantiation, initialization, use, and eventual disposal. When the Spring container starts, it instantiates the beans. Then, it injects the dependencies, calls the initialization methods (if specified), and makes the bean available for use. When the container shuts down, the beans are destroyed, invoking any destruction methods (if defined).

Spring beans undergo a series of stages known as the bean lifecycle. Understanding these stages is essential for effective bean management.

Here is a concise summary of the Spring bean lifecycle methods:

  1. Instantiation: Beans are created, either through constructor invocation or factory methods.

  2. Population of Properties: Dependencies and properties of the bean are set.

  3. Awareness: Beans implementing Aware interfaces are notified of the Spring environment and related beans. Examples include BeanNameAware, BeanFactoryAware, and ApplicationContextAware.

  4. Initialization: The bean is initialized after its properties are set. This involves calling custom initialization methods specified by the developer. If a bean implements the InitializingBean interface, the afterPropertiesSet() method is invoked. Alternatively, you can define a custom initialization method and specify it in the bean configuration.

  5. In Use: The bean is now in use, performing its intended functions within the application.

  6. Destruction: When the application context is closed or the bean is no longer needed, the destruction phase begins. Beans can implement the DisposableBean interface to define custom cleanup operations in the destroy() method. Alternatively, you can specify a custom destruction method in the bean configuration.

Understanding these stages ensures proper initialization and cleanup of Spring beans, facilitating efficient and well-managed Spring applications.

👉 Learn more about bean lifecycle in our article Hooking Into the Spring Bean Lifecycle .

Managing Spring Bean Lifecycle with @Bean

In the context of the Employee Management System, let us learn about managing the lifecycle of a Spring bean using @Bean methods and related annotations.

1. Custom Initialization and Destruction Methods:

public class Employee {

    // Bean properties and methods

    public void init() {
        // Custom initialization logic
    }

    public void destroy() {
        // Custom cleanup logic
    }
}

In our configuration class:

@Configuration
public class JavaConfigAppConfiguration {

    @Bean(name = "newEmployee", initMethod = "init", destroyMethod = "destroy")
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    @Cacheable(cacheNames = "employeesCache")
    public Employee newEmployee(final String firstName, final String lastName) {
        return Employee.builder().firstName(firstName).lastName(lastName).build();
    }
}

In this example, the newEmployee() method creates an Employee bean. The initMethod attribute specifies a custom initialization method, and the destroyMethod attribute defines a custom cleanup method. When the bean is created and destroyed, these methods are invoked, allowing you to handle specific lifecycle events.

2. Implementing InitializingBean and DisposableBean Interfaces:


public class Department implements InitializingBean, DisposableBean {

    // Bean properties and methods

    @Override
    public void afterPropertiesSet() throws Exception {
        // Initialization logic
    }

    @Override
    public void destroy() throws Exception {
        // Cleanup logic
    }
}

In our configuration class, you can create the bean as usual:

@Configuration
public class JavaConfigAppConfiguration {

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Department newDepartment(final String deptName) {
        final Department department = Department.builder().name(deptName).build();
        acme().addDepartment(department);
        return department;
    }
}

In this approach, the Department class implements the InitializingBean and DisposableBean interfaces, providing afterPropertiesSet() for initialization and destroy() for cleanup. Spring automatically detects these interfaces and calls the appropriate methods during bean lifecycle stages.

Interface InitializingBean is implemented by beans that need to react once all their properties have been set by a BeanFactory: for example, to perform custom initialization, or merely to check that all mandatory properties have been set.

Interface DisposableBean is implemented by beans that want to release resources on destruction. A BeanFactory is supposed to invoke the destroy method if it disposes a cached singleton. An application context is supposed to dispose all of its singletons on close.

These examples demonstrate how @Bean methods, along with custom initialization and destruction methods or interfaces like InitializingBean and DisposableBean, enable precise control over the lifecycle of Spring beans within our Employee Management System.

Specifying Bean Scope

The @Scope annotation allows you to define the scope of the bean. For instance, @Scope("prototype") indicates a new instance of the bean for every request, while the default scope is Singleton, creating a single bean instance per Spring IoC container.

@Configuration
public class EmployeeConfig {

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Employee employee() {
        return new Employee();
    }
}

Summary of Spring Bean Scopes

In Spring, bean scope defines the lifecycle and visibility of a bean instance within the Spring IoC container. Choosing the right scope ensures appropriate management and usage of beans in our application. Here’s a summary of common Spring bean scopes:

  1. Singleton Scope: Beans in Singleton scope have a single instance per Spring IoC container. It’s the default scope. Singleton beans are created once and shared across the application. Use this scope for stateless beans and heavy objects to conserve resources.

  2. Prototype Scope: Beans in Prototype scope have a new instance every time they are requested. Each request for a prototype bean creates a new object. Use this scope for stateful beans or when you need a new instance every time.

  3. Request Scope: Beans in Request scope have a single instance per HTTP request. This scope is specific to web applications. Each HTTP request gets a new instance of the bean. Use this for stateful beans in a web context.

  4. Session Scope: Beans in Session scope have a single instance per HTTP session. Similar to request scope but persists across multiple requests within the same session. Use this for user-specific stateful beans in a web context.

  5. Application Scope: Beans in Application scope have a single instance per ServletContext. This means a single instance shared across all users/sessions in a web application. Use this for global, stateless beans that should be shared by all users.

  6. Custom Scope: Besides the standard scopes, Spring allows you to create custom scopes tailored to specific requirements. Custom scopes can range from narrow scopes like thread scope to broader scopes based on business logic.

Choosing the appropriate scope depends on the specific use case and requirements of our beans. Understanding these scopes ensures that our beans are managed effectively, optimizing resource utilization and ensuring the correct behavior of our Spring application.

Overriding Default Bean Name

By default, the bean name is the same as the method name. However, you can specify a custom name using the name attribute in the @Bean annotation.

@Configuration
public class JavaConfigAppConfiguration {

    @Bean(name = "founder")
    @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
    public Employee founderEmployee() {
        final Employee founder = newEmployee("Scott", "Tiger");
        founder.setDesignation("Founder");
        founder.setDepartment(coreDepartment());
        return founder;
    }
}

In this example, the bean is named founder, allowing specific identification within the application context.

The @Qualifier annotation is used to resolve the ambiguity that arises when multiple beans of the same type are present in the Spring container. We can use it to define the name of the bean we want to inject. In this case, we want to inject the bean with the name founder:

public class EmployeeController {
    
    private final Employee founder;

    public EmployeeController(@Qualifier(value = "founder") Employee employee){
        this.founder = employee;
    }
}

There are two methods in JavaConfigAppConfiguration that configure an Employee bean. We need to use the qualifier to tell Spring which bean we are tring to wire.

Understanding @Bean and its related concepts is pivotal in Spring configuration. It not only creates instances but also allows fine-grained control over their lifecycle, scope, and naming conventions, empowering our Employee Management System with Spring-managed components.

Understanding @Configuration

@Configuration is a annotation that marks a class as a source of bean definitions. It allows you to create beans using @Bean methods within the class. In our Employee Management System, @Configuration organizes the creation of beans, providing a centralized configuration hub.

@Configuration
public class JavaConfigAppConfiguration {
    @Bean
    public Employee employee() {
        return new Employee();
    }

    @Bean
    public Department salesDepartment() {
        return new Department();
    }
}

With @Configuration, we encapsulate our bean definitions, promoting modularity and enhancing maintainability. Spring ensures that these beans are available for injection wherever needed.

In the above example, we tell Spring to add a bean of type Employee and a bean of type Department to the container. We can then inject those beans into other beans if needed.

Building Maintainable Applications

Maintainability is a key consideration while developing Spring applications. As applications grow, it becomes essential to structure the configuration in a way that’s modular and adaptable. Java-based configuration in Spring provides a robust solution, enabling the development of maintainable applications through modular configurations.

Organise Configuration Logic

  • Java Classes as Configuration Units: With Java-based configuration, each Java class can encapsulate a specific set of configuration concerns. For instance, one class can handle data source configuration, another can manage security, and yet another can configure caching mechanisms.
  • Encapsulation and Cohesion: Each class focuses on a particular aspect of the application’s configuration, promoting encapsulation and ensuring high cohesion, making the codebase more comprehensible and maintainable.

Reuse and Compose Configurations

  • Reusable Configurations: Java configuration classes are highly reusable. A configuration class developed for one module can often be employed in other parts of the application or even in different projects, fostering a culture of code reuse.
  • Composing Configurations: By composing multiple configuration classes together, developers can create complex configurations from simpler, well-defined building blocks. This composition simplifies management and promotes a modular architecture.
    @Configuration
    @Import({DatabaseConfiguration.class, 
             AppCacheConfiguration.class,
             SecurityConfiguration.class})
    public class InfrastructureConfiguration {
    }
    

In this example:

@Import Annotation: The InfrastructureConfiguration class uses the @Import annotation to import the configuration classes of individual modules. This annotation allows us to compose configurations by combining multiple configuration classes into a single configuration class.

By importing the SecurityConfiguration, DatabaseConfiguration, and AppCacheConfiguration classes into the InfrastructureConfiguration, we create a unified configuration that encompasses all the specific settings for authentication, database interactions, and caching.

You can see it in action by running the testImportedConfig() unit test in the example code shared on Github.

Testing and Unit Testing Advantages

  • Isolated Testing: Each configuration class can be unit-tested in isolation, ensuring that specific functionalities are correctly configured.
  • Mocking Dependencies: In testing, dependencies can be easily mocked, enabling focused testing of individual configuration components without relying on complex setups.

Clear Hierarchical Structure

  • Hierarchical Structure: Java configuration fosters a clear hierarchical structure within the application, where configurations can be organized based on layers, modules, or features.
  • Enhanced Readability: The hierarchical arrangement enhances the readability of the configuration code, making it easier for us to navigate and understand the application’s structure.

Java-based configuration in Spring empowers developers to create maintainable applications. By embracing modular configurations, we can build flexible, adaptable systems that respond effectively to changing requirements.

Automatic Java Configuration

In the world of Spring Boot, @EnableAutoConfiguration is a powerful annotation that simplifies the process of configuring your Spring application. When you annotate your main application class with @EnableAutoConfiguration, you are essentially telling Spring Boot to automatically configure your application based on the libraries, dependencies, and components it finds in the classpath.

Automatic Configuration Using @EnableAutoConfiguration

  • Dependency Analysis: Spring Boot analyzes your classpath to identify the libraries and components you are using.
  • Smart Defaults: It then automatically configures beans, components, and other settings, providing sensible default behaviors.

Spring Boot automatically configures essential components like the web server, database connection, and more based on your classpath and the libraries you include.

Example: Minimal Application with Auto-Configuration:

@SpringBootApplication
public class JavaConfigApplication {

}

In this example, @SpringBootApplication includes @EnableAutoConfiguration. This annotation signals Spring Boot to configure the application automatically, setting up defaults based on the classpath.

Customize and Override Auto Configurations

  • Selective Overrides: While Spring Boot provides auto-configuration, you can still customize and override these configurations as needed.
  • Properties-Based Tuning: Properties in application.properties or application.yml can fine-tune auto-configured settings.

You can customize auto-configurations using properties. For instance, you can disable a specific auto-configuration by setting a property.

Example: Disabling a Specific Auto-Configuration:

# application.properties
spring.autoconfigure.exclude=\
    org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

In this example, DataSourceAutoConfiguration is explicitly excluded, meaning Spring Boot won’t configure a datasource bean by default.

@OverrideAutoConfiguration annotation can be used to override @EnableAutoConfiguration. It is often used in combination with @ImportAutoConfiguration to limit the auto-configuration classes that are loaded. It is also useful when we run tests and we want to control the auto configured beans.

See following example:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class, 
                webEnvironment = WebEnvironment.DEFINED_PORT)
@ActiveProfiles("test")
@OverrideAutoConfiguration(exclude = {EnableWebMvc.class})
public class ExcludeAutoConfigIntegrationTest {

}

Simplified Application Setup

  • Reduced Boilerplate: @EnableAutoConfiguration drastically reduces boilerplate code, eliminating the need for explicit configuration for many common scenarios.
  • Rapid Prototyping: It allows developers to quickly prototype and build applications without getting bogged down in intricate configuration details.

With auto-configuration, you can rapidly prototype applications without extensive configuration.

Example: Creating a REST Controller:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyController {
    @GetMapping("/hello")
    public String hello() {
        return "Hello, Spring Boot!";
    }
}

In this example, a simple REST endpoint is created without explicit configuration. Spring Boot’s auto-configuration handles the setup, allowing developers to focus on the business logic.

Spring Boot’s auto-configuration simplifies @RestController development. It handles HTTP message conversion, request mapping, and exception handling. Developers can focus on business logic while Spring Boot manages content negotiation and routing, streamlining RESTful API creation with sensible defaults and minimizing manual configuration.

Perform Conditional Configuration

  • Conditional Loading: Auto-configuration is conditional; it’s applied only if certain conditions are met.
  • @ConditionalOnClass and @ConditionalOnProperty: Conditions can be based on the presence of specific classes or properties, giving you fine-grained control.

Auto-configurations are conditionally applied based on specific conditions.

Example: Conditional Bean Creation:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyConfiguration {
    @Bean
    @Conditional(MyCondition.class)
    public MyBean myBean() {
        return new MyBean();
    }
}

In this example, MyBean is created only if the condition specified by MyCondition is met. Conditional annotations are integral to Spring Boot’s auto-configuration mechanism.

Simplified Application Development

  • Faster Development: With auto-configuration, developers can focus more on business logic and features, accelerating development cycles.
  • Convention Over Configuration: It follows the convention over configuration principle, encouraging consistency across Spring Boot applications.

Auto-configuration simplifies the development process and promotes convention over configuration.

Example: Spring Boot Starter Usage:

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

In this example, including the spring-boot-starter-web dependency automatically configures the application for web development. Spring Boot starters encapsulate auto-configurations, providing an intuitive way to include essential dependencies.

Auto Configured Spring Boot Starter Packs

  • Starter Packs: Spring Boot Starter Packs are built around auto-configuration, bundling essential dependencies for specific use cases (e.g., web, data, security).
  • Simplified Integration: Starters handle complex integration details, allowing developers to seamlessly integrate technologies like databases, messaging systems, and security frameworks. Starters handle complex integrations, ensuring seamless setup.

Example: Using Spring Boot Starter for JPA:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

Including spring-boot-starter-data-jpa simplifies Java Persistence API (JPA) integration. Spring Boot’s auto-configuration manages JPA entity managers, data sources, and transaction management.

Conclusion: Effortless Spring Boot Applications

  • Boosted Productivity: @EnableAutoConfiguration is at the heart of Spring Boot’s philosophy, boosting developer productivity by simplifying setup and reducing configuration overhead.
  • Maintenance Ease: Applications configured with auto-configuration are easier to maintain and update, ensuring compatibility with evolving libraries and technologies.

In essence, @EnableAutoConfiguration encapsulates the spirit of Spring Boot—making Java configuration easier, more streamlined, and immensely developer-friendly. By leveraging this annotation, developers can focus on crafting robust applications while Spring Boot takes care of the intricate details under the hood.

Summary

In the realm of Spring, mastering Java-based configuration is a gateway to flexible and maintainable applications. Through our journey, we’ve unlocked the power of @Configuration, @Bean, and @PropertySource, seamlessly integrating external properties and enhancing modularity.

Next Steps

As you embark on your coding odyssey, delve deeper into Spring’s official documentation. Explore frameworks and libraries, implement what you’ve learned, and test your creations. Experience the joy of transforming concepts into working programs.

Remember, each line of code is a step towards mastery.

🚀 Happy coding!

Here is a action packed plan for you:

  1. Explore Spring’s Official Documentation: Dive into Spring’s official documentation to grasp core concepts and best practices.
  2. Experiment with Various Frameworks: Experiment with Spring Boot, and other Spring frameworks to understand their nuances.
  3. Master Libraries and Integrations: Explore libraries like Jasypt and integrations like Hibernate, Ehcache for a holistic understanding.
  4. Implement Java-Based Configurations: Practice creating @Configuration classes, defining beans with @Bean, and integrating external properties with @PropertySource.
  5. Write Working Programs: Apply concepts learned by writing small, working programs to solidify your understanding.
  6. Test Your Applications: Embrace testing methodologies like JUnit, AssertJ to validate your Java-based configurations and ensure reliability.
  7. Understand Error Handling: Delve into error handling techniques to fortify your applications against unforeseen issues.
  8. Explore Declarative Approaches: Understand declarative programming paradigms, like Spring’s annotations, for concise and readable code.
  9. Participate in Community Forums: Engage with the developer community on forums and platforms to learn from real-world experiences and challenges.
  10. Continuous Learning and Practice: Keep the momentum going. Stay updated, practice regularly, and challenge yourself with complex scenarios to hone your skills.
Written By:

Sachin Raverkar

Written By:

Sachin Raverkar

Sachin is a Java enthusiast with over two decades of product development expertise. He enjoys architecting and delivering SAAS products as well as sharing expertise with people all over the world.

Recent Posts

Understanding Null Safety in Kotlin

One of the standout features that sets Kotlin apart is its robust approach to null safety. Null safety is a critical aspect of programming languages, aiming to eliminate the notorious null pointer exceptions that often plague developers.

Read more

Merge Sort in Kotlin

Sorting is a fundamental operation that plays a crucial role in various applications. Among the many sorting algorithms, merge sort stands out for its efficiency and simplicity.

Read more

Extension Functions in Kotlin

One of Kotlin’s standout features is extension functions, a mechanism that empowers developers to enhance existing classes without modifying their source code.

Read more