29
■ ■ ■
CHAPTER 3
The Heart of Spring:
Inversion of Control
G
enerally, when you are writing code, your class will accumulate dependencies. Typically,
you will use dozens of other classes: the primitive wrapper classes, the collection classes,
and so on. Some of these will be as general as String or Integer, and others will be
domain specific.
For the most part, nothing changes with inversion of control. The internal implemen-
tation details of your classes should look much the same before and after. However, if
your class has a dependency on a particular external class that could reasonably have
alternative implementations, or if a class represents data that you might reasonably want
to change at some future date, inversion of control frameworks such as Spring allow you
to supply these dependencies at a later date. Because you are providing the dependencies
from outside the class instead of acquiring them directly, this form of inversion of control
is also known as dependency injection (DI).
Spring doesn’t do any magic here—you still need to supply reference variables with
which to manipulate these external dependencies—but it does move the configuration
details of these dependencies from compile-time to runtime.
Benefits and Disadvantages of DI
In principle, you don’t need a framework to inject dependencies into your code; you can
do this from code. In practice, however, most applications built by using inversion of control
use a framework of some type to carry out the dependency injection. Typically, as with
Spring, these read configuration information and then use the Java reflection API or bytecode
manipulation to invoke the appropriate methods on your code to inject the dependencies.
Although this behavior is not innate in the dependency injection approach, it is so
widespread that it might as well be. Unfortunately, it leads directly to the one real disad-
vantage that containers such as Spring have over hard-coding of dependencies: that they
lose some of the advantages of static type checking. The configuration information will
dency injection. The XML-based configuration files typically used can become confusing
if they are not thoughtfully maintained. Expressing relationships between Java components
in XML sometimes feels inelegant. The extensive use of reflection to inject dependencies
can make debugging more complex. These issues are specific to Spring’s implementation,
not to DI itself, but the use of Spring as an implementation more than compensates for these.
Coupling
The big win in using dependency injection is that it allows you to make your applications
loosely coupled. That is to say, any one class of your implementation will tend not to have
any dependencies on any other class’s specific implementation.
Minter_685-4C03.fm Page 30 Thursday, November 8, 2007 6:03 AM
CHAPTER 3
■
THE HEART OF SPRING: INVERSION OF CONTROL
31
You will still have dependencies on the type system that you’re establishing in your
application, of course, but loose coupling encourages the use of programming to inter-
faces rather than to abstract or concrete implementations.
Tight Coupling
Rather than talking in abstract terms, I will show you an example of some tightly coupled
code to illustrate these concerns (see Listing 3-1).
Listing 3-1.
A Minimal Tightly Coupled Component
package com.apress.coupling;
public class TightlyCoupled {
private Transport transport = new SmtpImpl();
public void sendMessage() {
transport.send();
}
}
This code is a little contrived in its simplicity, but it reflects a real-world scenario in
public void sendMessage() {
transport.send();
}
}
Again this code is somewhat contrived, but it does illustrate clearly the breaking of the
dependency on the specific transport implementation. By allowing the implementation
to be passed via the constructor, we allow alternative implementations to be passed in. By
breaking the tight coupling, we have also removed the hindrances to the development of
external tests and reusability.
For our testing, a mock object can be supplied to the constructor, allowing us to test
that the appropriate method call is made without needing any of the underlying infra-
structure associated with the real transport implementations.
For reusability, we can swap out the SMTP implementation for a SOAP, RMI, or any
other suitable transport.
The only notable disadvantage to the loose coupling approach, as illustrated in Listing 3-2,
is a slight increase in the verbosity of the resulting class implementation, mostly deriving
from the demands of the JavaBean specification when adding properties to classes.
Knowing When to Stop
Not all implementation details need to be exposed to the outside world, even when creating
an application to run within the Spring framework. Only dependencies that you might
want to substitute in order to test the component in isolation are likely to be candidates
for access in this way. Listing 3-3 shows a class with two candidate dependencies.
Listing 3-3.
A Simple Implementation with Two Dependencies
package com.apress.coupling;
import java.util.SortedSet;
import java.util.TreeSet;
Minter_685-4C03.fm Page 32 Thursday, November 8, 2007 6:03 AM
CHAPTER 3
■
to be a candidate for unit tests.
• We are unlikely to use an alternative implementation of SortedSet unless we are
involved in minute performance-related debugging concerns.
• TreeSet will have no dependencies beyond the JDK itself. The JDK is generally
assumed to be correct unless proven otherwise and does not require its own unit tests.
Minter_685-4C03.fm Page 33 Thursday, November 8, 2007 6:03 AM
34
CHAPTER 3
■
THE HEART OF SPRING: INVERSION OF CONTROL
Adding the specific set implementation to the API here would provide very little advantage
in return for the extra complexity added to our MailingList class implementation.
Of course, there are possible counterarguments even for this scenario, but in the end
the choice of when to make a dependency injectable resides with the developer. My advice
is to choose whatever approach will make your unit tests easiest to write, and then refactor
your code as it proves appropriate. Although this won’t give you the correct answer every
time, it will at least be easy to change your architecture without needing to substantially
rework your unit tests, which will in turn reduce the frustration involved in changing APIs
and will keep your code clean and the bug count low.
The Need for a Framework
The framework is not an absolute requirement of dependency injection. Taking our loosely
coupled example from Listing 3-2, it is obvious that the injection of the dependencies can
be carried out from conventional code.
Indeed, as Listing 3-4 shows, this is the sort of code you will have written frequently.
Dependency injection is not some strange abstract new technique; it is one of the normal
tools of the developer.
Listing 3-4.
Dependency Injection from Conventional Code
final Transport smtp = new SmtpImpl();
final LooselyCoupled lc1 = new LooselyCoupled(smtp);
Table 3-1.
BeanFactory Methods
Method Description
boolean containsBean(String name) Determines whether the factory contains a bean
with the given name.
String[] getAliases(String name) Determines the alternative names (aliases) for a
bean with the given name.
Object getBean(String name) Obtains an instance of the bean with the given name
from the factory. This may be a new instance or a
shared instance.
Object getBean(String name, Class
requiredType)
Obtains an instance of the bean with the given name
and type from the factory. This is used by the auto-
wiring feature described in the “Autowiring” section
of this chapter.
Class getType(String name) Determines the type of a bean with a given name.
boolean isPrototype(String name) Determines whether the bean definition is a proto-
type. A prototype is a named set of bean definition
information that can be used to abbreviate the
configuration of a “real” bean definition. If a bean
definition is a prototype, it cannot be instantiated
with a call to getBean().
boolean isSingleton(String name) Determines whether calls to getBean() for the named
bean will return a new instance with every call, or a
single shared instance (a singleton instance).
boolean isTypeMatch(String name,
Class targetType)
Determines whether the named bean matches the
provided type—essentially determines whether a
builder = builder.addConstructorArgReference("soap");
bf.registerBeanDefinition("looseSoap",builder.getBeanDefinition());
// Instantiate the smtp example and invoke it
final LooselyCoupled lc1 = (LooselyCoupled)bf.getBean("looseSmtp");
lc1.sendMessage();
// Instantiate the soap example and invoke it
final LooselyCoupled lc2 = (LooselyCoupled)bf.getBean("looseSoap");
lc2.sendMessage();
The first question that would tend to spring to mind after reading through Listing 3-5
and comparing it to Listing 3-4 is, “Why would I ever want to do something so ungainly?”
You wouldn’t, of course. Listing 3-5 is purely an illustration of what goes on under the
covers of the framework. You might use a few of these classes if you were extending part
of the framework itself, but most developers will never (or at most rarely) need to touch
Minter_685-4C03.fm Page 36 Thursday, November 8, 2007 6:03 AM
CHAPTER 3
■
THE HEART OF SPRING: INVERSION OF CONTROL
37
upon BeanDefinitionBuilder and the like. I will show you how the equivalent Spring
configuration would really be defined in the discussion of Listing 3-7 later in this chapter.
Listing 3-5 does illustrate some important parts of the architecture that you will be
working with, however, so it is worth taking the time to understand what is involved here.
The first line of the application establishes a DefaultListableBeanFactory instance. This
is a bean factory that provides no direct assistance in preparing the bean definition infor-
mation. The developer must programmatically assign all the bean definition information:
final DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
The next block of the implementation creates two new bean definitions for the trans-
port implementation classes. This is metadata about the implementations, not instances
of the implementations themselves. We declare that two beans should be available from
the factory, we specify the implementation classes that define them, and we specify that
final LooselyCoupled lc1 = (LooselyCoupled)bf.getBean("looseSmtp");
lc1.sendMessage();
// ...
final LooselyCoupled lc2 = (LooselyCoupled)bf.getBean("looseSoap");
lc2.sendMessage();
Only when the factory has been fully populated with all of the relevant bean definitions
do we use it to materialize actual objects. These are normal Java objects quite indistin-
guishable from the objects returned from the calls to the new operators in Listing 3-4.
XML Configuration
Although a Spring application can in principle be configured in any number of different
ways, XML configuration files are by far the most common approach. Indeed, for most
developers, the set of XML files used to configure the factory for a Spring project and the
BeanFactory instance itself are virtually synonymous. The XML is the representation of the
factory that will be available to you at runtime, so this is not a bad way of thinking of them,
but do bear in mind that it is a useful approximation to the reality of the situation.
Ultimately, we need to use a language of some sort to configure our dependencies.
Traditionally, this has been the Java programming language itself, occasionally resorting
to properties files when the problems of tight coupling became too painful. XML files offer
us a better balance of flexibility, readability, verbosity, and expressiveness.
Something to remember in particular is that there is no 1:1 correspondence between
factories and XML files. It is entirely possible (and normal) to use multiple files to configure
a single factory, or to use a single file to instantiate several discrete factories (though this
is unusual).
Listing 3-6 represents the same configuration information that we painstakingly hard-
coded in Listing 3-5 of the previous section.
Listing 3-6.
A Complete but Simple XML Spring Configuration File
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns=" /> xmlns:xsi=" /> xsi:schemaLocation=