Building Cloud Services: Spring Dependency Injection and Configuration Annotations

2020, Jul 25
Link to project here.

When a Spring application starts, it automatically discovers our controllers and then routes requests to them based on their annotations. Often our controllers are going to depend upon other objects that we define, but each object may have to be configured differently depending on the situation in which it is being used.

For example, lets say we have a controller for our video service:

@Controller
public class VideoService {
    private StorageSystem storage;

    // ... do things ...
}

Where StorageSystem is an interface that we define for interacting with the multiple classes for our storage solutions AmazonS3Storage or LocalStorage.

When writing the controller we could simply:

private StorageSystem storage = new AmazonS3Storage(...);
// Or
private StorageSystem storage = new LocalStorage(...);

The downside of this approach is whenever we need to change the storage method, we have to make changes to the code itself and recompile the application, which is not ideal. Instead, we would like the application to automatically instantiate and inject the StorageSystem that we need into the controller on startup. Spring allows us to do this via a process called dependency injection.

Table Of Contents


Dependency Injection


A dependency is something an object needs in order to function. For our VideoService, one of its dependencies is an object that implements a StorageSystem interface. So dependency injection using Spring means that it will automatically provide an object that implements the StorageSystem interface to VideoService without us having to explicitly construct it. This can be achieved with the annotation @Autowired.

@Autowired, @Configuration and @Bean

This annotation tells Spring to automatically look at the configuration that is provided to it, and then find and instantiate (or reuse) an implementation of a given class or interface.

@Controller
public class VideoService {
    @Autowired
    private StorageSystem storage;

    // ... do things ...
}

In order for this to work, we need a way to tell Spring how to populate these individual instances that are expected to be auto-wired. One way this can be done is through a Configuration object. This object defines the mapping between the actual concrete implementations of the different interfaces that our controllers depend upon, and the specific class that we want to use for that interface.

We can define a configuration object by using the @Configuration annotation on the class. Then at runtime, Spring will create an instance of our VideoService object and then search through our configuration files for methods annotated with @Bean, calling them based on the required return type.

@Configuration
public class VideoConf {
  @Bean
  public StorageSystem StorageSystem() {
    return new LocalStorage();
  }
}

Based on the code above, anywhere in our code where we use the @Autowired annotation that carries the type StorageSystem, Spring will automatically take the value it received from calling StorageSystem() and set the appropriate member variable with that value. In this case it would be an instance of LocalStorage.

So we are completely decoupling how our objects get constructed and configured from the configurations themselves. All we have to do is:

  • Create a class that has @Autowired on its various dependencies.
  • Define an interface that abstracts away the different implementation details like Amazon S3 VS Local Storage
  • Define a class annotated with the @Configuration annotation that contains @Bean annotated methods for creating actual implementations.

It's worth noting that Spring will only call each method once by default, and it will reuse same the object reference in all auto-wired variables for that type, which is typically what is desired.

@ComponentScan() and @EnableAutoConfiguration

Writing an @Bean method for every component in our application would be a lot of work, particularly if there is only a single implementation of the interface.

Instead, we can use the @ComponentScan() annotation to tell Spring to find all of the implementations in a package and then automatically associate them with the relevant @Autowired interfaces. Of course, this annotation only works if the package nominated for the scan only contains a single implementation of each interface. @EnableAutoConfiguration is the annotation that tells Spring to look for all the @Autowired annotations and automatically fill them in.

Because @ComponentScan() scans the package and all sub-packages, if there are a lot of classes in these packages, it can take a long time to complete the scan and complete the setup. This only occurs on application launch, so this wouldn't be a big deal in most environments. However, in certain cases such as Google App Engine, your application can be shutdown when there are is not enough traffic to your application and this issue can cause a performance hit.

Note: Spring only looks at code that is annotated with @Controller or @Service. If a class should be used as an implementation to auto-wire, you can add @Service to the class.

@EnableAutoConfiguration
@ComponentScan(basePackages = "com.mobile")
@Configuration
public class Application {
    //... stuff ...
}