DEV Community

Cover image for Understanding the Spring Framework: A Developer’s Journey to Clean Code 🚀
Kaweesha Marasinghe
Kaweesha Marasinghe

Posted on

Understanding the Spring Framework: A Developer’s Journey to Clean Code 🚀

Introduction: The Early Java Era

In the early era of programming, writing Java code was a bit frustrating. Developers had to set up long configurations, deal with verbose XML files, and manually manage dependencies that were rapidly updated. This era made it harder for developers to thrive and write efficient code.

In 2003, Spring Framework revolutionized the Java ecosystem by introducing Dependency Injection (DI) and Inversion of Control (IoC). These features significantly reduced developers' headaches by eliminating tightly coupled code and making it easier to write clean, modular code. 🙌


What is Inversion of Control (IoC)? 🤔

IoC Diagram

IoC is a design principle where the control of object creation and dependency management is transferred to a container (like the Spring IoC container). Instead of objects managing their own dependencies, the container does this work for you! 🎉

Normal (Without IoC) Behavior

Without IoC, developers are responsible for creating and managing their object dependencies.

For example, let’s consider a Car class that depends on an Engine class. In this scenario, the Car class manages the Engine object by itself:

public class Car {
    private Engine engine = new Engine();  // Car directly creates an Engine

    public void start() {
        engine.run();
    }
}
Enter fullscreen mode Exit fullscreen mode

In this case:

  • The Car class directly creates the Engine object.
  • The Car class is responsible for both creating and managing the Engine object, which creates a tight coupling between the two.
  • If we want to change the Engine class (for example, to use a HybridEngine), we’d need to modify the Car class. This violates the Open/Closed Principle: classes should be open for extension but closed for modification. 🚧

With IoC in Spring (Dependency Injection) 🛠️

In IoC and Dependency Injection (DI), Spring takes care of creating and injecting dependencies into your classes.

Simply put, the Spring IoC container creates the Engine object and injects it into the Car object when required. This way, you don’t have to manually manage object creation or dependencies. Spring handles it for you. 🧑‍💻

public class Car {
    private Engine engine;  // Engine is injected by Spring, not created by Car

    // Constructor-based injection
    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.run();
    }
}
Enter fullscreen mode Exit fullscreen mode

How Does Spring Know Which Objects to Inject? 🧐

Spring uses Spring Beans to manage the lifecycle and dependencies of objects. In Spring, a bean is an object that the Spring IoC container manages. The container knows which beans to create and inject based on your configuration. 🌱

Spring Beans and Bean Lifecycle 🔄

In Spring, a bean is any object that is managed by the Spring IoC container. The container is responsible for:

  1. Instantiation: Creating the bean.
  2. Dependency Injection: Injecting the required dependencies.
  3. Initialization: Calling any initialization methods (e.g., @PostConstruct).
  4. Destruction: Calling cleanup methods (e.g., @PreDestroy).

How to Define a Spring Bean? 🏷️

You can define beans using annotations:

  • @Component: Marks a class as a Spring-managed bean. Spring automatically detects and instantiates this class during component scanning.
  • @Autowired: Tells Spring to inject the required dependency (either via constructor, field, or setter).
  • @Configuration: Used to define bean definitions using Java configuration (using @Bean).

Example with annotations:

@Component
public class Car {
    private Engine engine;

    @Autowired
    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.run();
    }
}

@Component
public class Engine {
    public void run() {
        System.out.println("Engine is running...");
    }
}
Enter fullscreen mode Exit fullscreen mode

Advantages of IoC (and DI) 🏆

By using IoC and DI, Spring offers several advantages:

  1. Loose Coupling: The Car class is no longer tightly coupled with the Engine. It only depends on the interface or type, not the specific implementation. This makes it easy to swap the Engine type, such as using an ElectricEngine instead of a GasolineEngine. 🔄

  2. Easier Testing: Since dependencies are injected, you can easily substitute mock objects or different implementations when writing unit tests for the Car class. 🧪

  3. Flexibility: You can change the dependencies externally (e.g., through Spring configuration) without modifying the core business logic of the classes. 🔧


Spring Configuration Methods

There are three primary ways to configure Spring beans and the IoC container:

1. XML Configuration (Traditional) 📜

In the traditional approach, beans are defined in an XML configuration file. This method is becoming less common but is still used in legacy systems.

<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="car" class="com.example.Car">
        <constructor-arg ref="engine"/>
    </bean>

    <bean id="engine" class="com.example.Engine"/>
</beans>
Enter fullscreen mode Exit fullscreen mode

2. Annotation-based Configuration (Modern) 🏷️

The modern approach uses annotations like @Component, @Autowired, and @Configuration to define and inject beans.

@Component
public class Car {
    private Engine engine;

    @Autowired
    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.run();
    }
}
Enter fullscreen mode Exit fullscreen mode

To enable annotation-based configuration, use @ComponentScan in a configuration class:

@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
    // Spring scans the specified package for @Component annotated classes
}
Enter fullscreen mode Exit fullscreen mode

3. Java-based Configuration (Best for Type Safety) 🖥️

Java-based configuration uses @Configuration and @Bean annotations, which is more type-safe and easier to maintain.

@Configuration
public class AppConfig {

    @Bean
    public Car car() {
        return new Car(engine());
    }

    @Bean
    public Engine engine() {
        return new Engine();
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion 🏁

In this blog, we’ve explored:

  • IoC (Inversion of Control): The core design principle where Spring takes control of object creation and dependency management.
  • DI (Dependency Injection): A technique that helps Spring inject dependencies into your beans, allowing for clean and flexible code.
  • Spring Beans: Objects that are managed by Spring, and how their lifecycle is handled by the IoC container.
  • Spring Configuration: Different ways to configure Spring beans, from XML to annotations to Java-based configurations.

Spring's powerful IoC and DI capabilities help you write cleaner, more modular, and easily testable code. 🌟


Top comments (0)