How do Spring IoC and Dependency Injection work?

Before discussing how Inversion of Control (IoC) and Dependency Injection (DI) work in Spring, it’s essential to understand what each of them means. If you have any questions, check out this post where I cover these two very important concepts.

Spring has a container that manages Beans.

Beans are objects managed by the Spring container and are the dependencies. A Bean can be injected into other Beans.

Management is done automatically by Spring; we can define what a Bean is through an annotation and inject it as well.

Notice that in the following example, we use @Component, which indicates that this is a Spring component—it is a Bean.

@Getter
@Setter
@Component
@ConfigurationProperties("otica.storage")
public class StorageProperties {

    private Local local = new Local();
    private S3 s3 = new S3();
    private TypeStorage type = TypeStorage.LOCAL;

    public enum TypeStorage {
        LOCAL, S3
    }

    @Getter
    @Setter
    public class Local {
        private Path directoryPhotos;
    }

    @Getter
    @Setter
    public class S3 {
        private String idKeyAccess;
        private String keyAccessSecret;
        private String bucket;
        private Regions region;
        private String directoryPhotos;
    }

}

Spring manages this Bean automatically by placing it in its IoC Container, and we can use it in the application through dependency injection.

@Configuration
public class StorageConfig {

    @Autowired
    private StorageProperties storageProperties;

    @Bean
    @ConditionalOnProperty(name = "otica.storage.type", havingValue = "s3")
    public AmazonS3 amazonS3() {
        BasicAWSCredentials credentials = new BasicAWSCredentials(
                storageProperties.getS3().getIdKeyAccess(), storageProperties.getS3().getKeyAccessSecret());

        return AmazonS3ClientBuilder.standard()
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .withRegion(storageProperties.getS3().getRegion())
                .build();
    }

    @Bean
    public PhotoStorageService photoStorageService() {
        if (TypeStorage.S3.equals(storageProperties.getType())) {
            return new S3PhotoStorageService();
        } else {
            return new LocalPhotoStorageService();
        }
    }

}

Since we registered StorageProperties as a Bean in the Spring container, we can use the @Autowired annotation to perform dependency injection.

An interesting point is that when creating a Bean, it’s not always necessary to use the @Component annotation. We can create a class with the @Configuration annotation and define methods with the @Bean annotation, which also allows us to create Beans.

How is it possible?

@Configuration makes use of @Component.

package org.springframework.context.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Component;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";

    boolean proxyBeanMethods() default true;

    boolean enforceUniqueMethods() default true;
}

Other annotations also utilize @Component. For example, @Service and @Repository.

package org.springframework.stereotype;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";
}

package org.springframework.stereotype;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";
}

Cool, but how does all this magic happen in Spring?

In the class that starts a Spring application, the @SpringBootApplication annotation is used, which includes another annotation called @ComponentScan. This configures that all classes from the package where this annotation is used can become a Bean if they use @Component or another annotation that allows the creation of a Bean. That’s why everything is managed automatically by Spring.

package org.springframework.boot.autoconfigure;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.core.annotation.AliasFor;

@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 {
    @AliasFor(
        annotation = EnableAutoConfiguration.class
    )
    Class<?>[] exclude() default {};

    @AliasFor(
        annotation = EnableAutoConfiguration.class
    )
    String[] excludeName() default {};

    @AliasFor(
        annotation = ComponentScan.class,
        attribute = "basePackages"
    )
    String[] scanBasePackages() default {};

    @AliasFor(
        annotation = ComponentScan.class,
        attribute = "basePackageClasses"
    )
    Class<?>[] scanBasePackageClasses() default {};

    @AliasFor(
        annotation = ComponentScan.class,
        attribute = "nameGenerator"
    )
    Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

    @AliasFor(
        annotation = Configuration.class
    )
    boolean proxyBeanMethods() default true;
}

Regarding dependency injection, @Autowired was used, but we can also perform dependency injection through the constructor. In this case, we don’t need to specify any annotations; it just needs to be a Bean managed by Spring.

    private StorageProperties storageProperties;

    public StorageConfig(StorageProperties storageProperties) {
        this.storageProperties = storageProperties;
    }

As we can see, Spring manages Beans automatically, which brings more productivity and simplicity to projects.

Author

  • Natan Ferreira

    Hello there, I’m Natan Lara Ferreira, Full Stack Developer Java and Angular since 2016. I’m in Open Finance Brazil project using framework Quarkus and Angular since the beginning 2021. I'm a problem solver, critical thinker and team player.