Building Cloud Services: Spring Dependency Injection and Configuration Annotations
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 ...
}