The Spring Framework has found a very strong user base over the years. Software houses and enterprises found the framework to be the best fit for their plumbing needs. Surprisingly, the core principle that Spring has been built for—the Dependency Injection (DI)—is very simple and straightforward to understand. In this chapter, we aim to discusses the fundamentals of the framework from a high ground. We will try to get a basic understanding of Dependency Injection principles. I will present a simple problem of object coupling and tight dependencies to begin with. Then we will aim to solve decoupling and dependencies by using Spring Framework.
Let us consider a simple program whose aim is to read data from various data sources. It should read data from a file system or database or even from an FTP server.
For simplicity, we will start writing a program that reads the data from a file system. The following example code is written without employing any best practices or dependency injection patterns—it’s just a simple and plain Java program that works.
This snippet shows a VanillaDataReaderClient
class that uses
FileReader
to fetch the data.
Example 1-1.
public
class
VanillaDataReaderClient
{
private
FileReader
fileReader
=
null
;
private
String
fileName
=
"src/main/resources/basics/basics-trades-data.txt"
;
public
VanillaDataReaderClient
()
{
try
{
fileReader
=
new
FileReader
(
fileName
);
}
catch
(
FileNotFoundException
e
)
{
System
.
out
.
println
(
"Exception"
+
e
.
getMessage
());
}
}
private
String
fetchData
()
{
return
fileReader
.
read
();
}
public
static
void
main
(
String
[]
args
)
{
VanillaDataReaderClient
dataReader
=
new
VanillaDataReaderClient
();
System
.
out
.
println
(
"Got data using no-spring: "
+
dataReader
.
fetchData
());
}
}
As the name suggests, the VanillaDataReaderClient
is the client that
fetches the data from a data source. When the program is executed, the
VanillaDataReaderClient
gets
instantiated along with a referenced FileReader
object. It then uses the VanillaFileReader
object to fetch the
result.
The following snippet shows the implementation of VanillaFileReader
class:
Example 1-2.
public
class
VanillaFileReader
{
private
StringBuilder
builder
=
null
;
private
Scanner
scanner
=
null
;
public
VanillaFileReader
(
String
fileName
)
throws
FileNotFoundException
{
scanner
=
new
Scanner
(
new
File
(
fileName
));
builder
=
new
StringBuilder
();
}
public
String
read
()
{
while
(
scanner
.
hasNext
())
{
builder
.
append
(
scanner
.
next
());
builder
.
append
(
","
);
}
return
builder
.
toString
();
}
}
Simple and sweet, isn’t it?
The limitation of the client above is that it can only read the data from file system.
Imagine, one fine morning, your manager asks you to improvise the program to read data from a Database or Socket instead of File.
With the current design, it is not possible to incorporate these changes without refactoring the code—as only File Reader associated to the client, it can’t work for any non-file resources. We have to associate a DatabaseReader or SocketReader with the client for any non-file resource reading excercise.
Lastly (and very importantly), we can see the client and the reader are coupled tightly.
That is, client depends on VanillaFileReader
’s contract,
meaning if VanillaFileReader
changes, so does the client.
If the client has already been distributed and used by, say, 1,000 users across the globe, you
will have fun refactoring the client!
If your intention is to build good scalable and testable components, then coupling is a bad thing.
So, let’s work out on your manager’s demands and make the program read the data from any source. For this, we will take this program one step further—refactoring so the client can read from any datasource. For this refactoring, we have to rely on our famous Design to Interfaces principle.
Writing your code against interfaces is a very good practice, although I am not going to sing praises about the best practices of designing here. The first step in designing to interfaces is to create an interface. The concrete classes will implement this interface, binding themselves to the interface contract rather than to an implementation. As long as you keep the interface contract unchanged, the implementation can be modified any number of times without affecting the client.
For our data reader program, we create an IReader
interface. This has just one
method:
public
interface
IReader
{
public
String
read
();
}
The next step is to implement this contract. Because we have to read the data from
different sources, we create respective concrete implementations such as FileReader
for reading from a File System, DatabaseReader
for reading from a Database, FtpReader
for reading from FTP Server, and so on.
The template for concrete implementation goes in the form of
XXXReader
as shown
here:
public
class
XXXReader
implements
IReader
{
public
String
read
(){
//impl goes here
}
}
Once you have the XXXReader
ready, the next step is to use it in the client program. However,
instead of using the concrete class reference, we will be using the
interface reference.
For example, the modified client program shown in this snippet has a IReader
variable reference (highlighted in bold), rather than
FileReader
or FtpReader
. It has a constructor that takes in an IReader
interface:
public
class
DataReaderClient
{
private
IReader
reader
=
null
;
private
static
String
fileName
=
"src/main/resources/basics/basics-trades-data.txt"
;
public
DataReaderClient
(
IReader
reader
)
{
this
.
reader
=
reader
;
}
private
String
fetchData
()
{
return
reader
.
read
();
}
public
static
void
main
(
String
[]
args
)
{
...
}
}
Looking at the client code, if I ask you to tell me the actual reader that has been used by the client, would you be able to tell me? You can’t!
The DataReaderClient
does not know where it is fed
the data until runtime. The IReader
class will only be
resolved at runtime by using polymorphism. All we know is that the client can get any of the
concrete implementations of IReader
interface. The
interface methods that were implemented in concrete incarnations of IReader
are invoked appropriately.
The challenge is to provide the appropriate concrete implementations of IReader
to the client. One way to do this is to create an
instance of IReader
in the client program—a FileReader is
created and passed on to the client as shown here:
public
class
DataReaderClient
{
private
IReader
reader
=
null
;
private
static
String
fileName
=
"src/main/resources/basics/basics-trades-data.txt"
;
...
public
static
void
main
(
String
[]
args
)
{
IReader
fileReader
=
new
FileReader
(
fileName
);
// Ummh..still hard wired?
DataReaderClient
client
=
new
DataReaderClient
(
fileReader
);
System
.
out
.
println
(
"Got data using interface design priciple: "
+
client
.
fetchData
());
}
}
Well, it is still hardwired as the client should have to know about which IReader
it is going to use. Of course, we could swap FileReader
with DatabaseReader
or FtpReader
without much
hassle, as they all implement IReader
interface. So, we
are in a much better position when our Manager comes along and changes his mind!
However, we still have the concrete IReader
coupled to the client. Ideally, we
should eliminate this coupling as much as possible. The question is how
can we provide an instance of IReader
to DataReaderClient
without
hardwiring? Is there a way we can abstract the creation of this FileReader
away from the client?
Before I ask you more questions, let me tell you the answer: yes! Any Dependency Injection (DI) framework can do this job for us. One such framework is Spring Framework.
The Spring Framework is one of the Dependency Injection (or Inversion of Control) frameworks that provides the dependencies to your objects at runtime very elegantly. I won’t explain the framework details yet, because we are eager to find the solution to the IReader problem using Spring Framework first.
The object interaction is a part and parcel of software programs. The good design allows you to replace the moving parts with no or minimal impact to the existing code. We say the objects are coupled tightly when the moving parts are knitted closely together. However, this type of design is inflexible—it is in a state where it cannot be scalable or testable in isolation or even maintainable without some degree of code change.
Spring Framework can come to the rescue in designing the components and eliminating dependencies.
Spring Framework works on one single mantra: Dependency Injection. This is sometimes interchangeable with the Inversion of Control (IoC) principle.
When a standalone program starts, it starts the main program, creates the dependencies, and then proceeds to execute the appropriate methods. However, this is exactly the reverse if IoC is applied. That is, all the dependencies and relationships are created by the IoC container and then they are injected into the main program as properties. The program is then ready for action. This is essentially the reverse of usual program creation, thus the name Inversion of Control principle.
Coming back to our IReader
program, the solution is to inject a concrete
implementation of IReader
into the
client on demand.
Let’s modify the client program—see the decoupled client code here:
public
class
DecoupledDataReaderClient
{
private
IReader
reader
=
null
;
private
ApplicationContext
ctx
=
null
;
public
DecoupledDataReaderClient
()
{
ctx
=
new
ClassPathXmlApplicationContext
(
"basics-reader-beans.xml"
);
}
private
String
fetchData
()
{
reader
=
(
IReader
)
ctx
.
getBean
(
"reader"
);
return
reader
.
read
();
}
public
static
void
main
(
String
[]
args
)
{
DecoupledDataReaderClient
client
=
new
DecoupledDataReaderClient
();
System
.
out
.
println
(
"Example 1.3: Got data: "
+
client
.
fetchData
());
}
}
As we can see, there is lots of magic going on in this class—the magician being the ApplicationContext class! Let’s see briefly what’s going on in there.
When the class is instantiated by the JVM, Spring Framework’s
ClassPathXmlApplicationContext
object is created in the class’s
constructor. This context class reads a configuration XML file and creates any objects
declared in that file—a FileReader
by the name of “reader” bean in this
case. The instantiation of the ApplicationContext
creates
the container that consists of the objects defined in that XML file.
When the fetchData is invoked, the same reader bean is fetched from the container and the
appropriate methods on the FileReader
were executed.
Don’t worry if you didn’t get much of it, you will learn about them in no time, trust me! We will discuss the framework fundamentals later in the chapter, but for now, let’s continue with our reader example.
After creating the client class, create a XML file that consists of definitions of our
FileReader
. The XML file is shown here:
<?xml version="1.0" encoding="UTF-8"?>
<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
name=
"reader"
class=
"com.madhusudhan.jscore.basics.FileReader"
>
<constructor-arg
value
="src/main/resources/basics/basics-trades-data.txt" /></bean>
</beans>
The purpose of this XML file is to create the respective beans
(instances of our classes) and their relationships. This XML file is
then provided to the ApplicationContext
instance, which creates a
container with these beans and their object graphs along with
relationships.
Looking at the reader bean in the preceding configuration file, the ctx = new
ClasspathXmlApplicationContext("basics-reader-beans.xml")
statement creates a
container to hold the beans defined in the basics-reader-beans.xml. The
FileReader
class is created by the framework with a name
reader
and stored in this container. The Spring container is simply a
holder for the bean instances that were created from the XML file. An API is provided to
query these beans and use them accordingly from our client application.
For example, using the API method ctx.getBean("reader")
, you can access the respective bean instances. That’s
exactly what we are doing in our client, in the fetchData
method by using
the statement: reader = (IReader)
ctx.getBean("reader");
The bean obtained is a fully instantiated FileReader
bean, so you can invoke the methods
normally: reader.read()
.
So, to wrap up, here are the things that we have done to make our program work without dependencies by using Spring:
We have implemented
IReader
creating aFileReader
class as a simple POJO.We have created a simple XML configuration file to declare this bean.
We then created a container (using the ApplicationContext) with this single bean by reading the XML file.
We queried the container to obtain our fully qualified instance of the FileReader class so we can invoke the respective methods.
Simple and straightforward, eh?
One quick note to the curious here: usually we use the new
operator
to create a brand new Object in Java. In the above client code, you don’t see us using
new
operator to create a FileReader. The framework has taken over this
responsibility for us—it creates these objects by peeking at the declared definitions in the
XML configuration file.
Before we wind up the chapter, there’s one more thing we should look at. The client and
the reader are still coupled! That is, the client will always be injected with a type
of IReader
defined in configuration. Ideally, the
client should not have to know about the IReader
—instead, we can have a
service layer to disassociate the reader with the client. This way the service would be
glued to the client rather than the IReader
. Let’s do
this by creating a service.
The ReaderService
is a simple class which abstracts
away the implementation details of the IReader
from the
client. The client will only have knowledge of the service; it will know nothing about where
the service is going to get the data from. The first step is to write the service. This
snippet is the our new service class:
public
class
ReaderService
{
private
IReader
reader
=
null
;
public
ReaderService
(
IReader
reader
)
{
this
.
reader
=
reader
;
}
public
String
fetchData
()
{
return
reader
.
read
();
}
}
The service has a class variable of
IReader
type. It is injected with an IReader
implementation via the constructor. It
has just one method—fetchData
which
delegates the call to a respective implementation to return the
data.
The following XML file wires the ReaderService
with
an appropriate IReader
in our
basics-reader-beans.xml file:
<beans>
<bean
name=
"readerService"
class=
"com.madhusudhan.jscore.basics.service.ReaderService"
>
<constructor-arg
ref=
"reader"
/>
</bean>
<bean
name=
"reader"
class=
"com.madhusudhan.jscore.basics.readers.FileReader"
>
<constructor-arg
value=
"src/main/resources/basics/basics-trades-data.txt"
/>
</bean>
</beans>
When
this config file is read by the Spring’s ApplicationContext
in your program, the ReaderService
and FileReader
beans are
instantiated. Note that, as the ReaderService
has a
reference to a reader (constructor-arg ref="reader"
), the
FileReader
is instantiated first and injected into
ReaderService
. If for what ever reason the FileReader
was not instantiated, the framework throws an
exception and quits the program immediately—hence the framework is said to be designed as
fail-fast.
Here is the modified ReaderServiceClient
:
public
class
ReaderServiceClient
{
private
ApplicationContext
ctx
=
null
;
private
ReaderService
service
=
null
;
public
ReaderServiceClient
()
{
ctx
=
new
ClassPathXmlApplicationContext
(
"basics-reader-beans.xml"
);
service
=
(
ReaderService
)
ctx
.
getBean
(
"readerService"
);
}
private
String
fetchData
()
{
return
service
.
fetchData
();
}
public
static
void
main
(
String
[]
args
)
{
ReaderServiceClient
client
=
new
ReaderServiceClient
();
System
.
out
.
println
(
"Got data using ReaderService: "
+
client
.
fetchData
());
}
}
The
notable thing is that the client will only have knowledge of the service—no Reader
s whatsoever—thus achieving the true decoupling of the
software components.
If you wish to read data from a database, remember that except for config, no other code
changes are required—enable an appropriate reader in the config and re-run the program. For
example, in the next snippet, the FileReader
is now
swapped with a DatabaseReader
—without changing even
single line of code—by just modifying the meta data.
This is shown here:
<bean
name=
"readerService"
class=
"com.madhusudhan.jscore.basics.service..ReaderService"
>
<!-- The reader now refers to DBReader -->
<property
name=
"reader"
ref=
"reader"
/>
</bean>
<!-- *** DBReader ** -->
<bean
name=
"reader"
class=
"com.madhusudhan.jscore.basics.readers.DatabaseReader"
>
<property
name=
"dataSource"
ref=
"dataSource"
/>
</bean>
<!-- Datasource that DBReader depends -->
<bean
id=
"dataSource"
class=
"org.apache.commons.dbcp.BasicDataSource"
>
....</bean>
The
ReaderService
is given a reference to the respective
dynamically
when the program is run.IReader
This chapter introduced the Spring Framework from the 10,000-foot view. We have seen the problem of object coupling and how the framework solved the dependency issues. We also glanced at a framework’s containers and scratched the surface of the framework’s usage, leaving many of the fundamentals to the upcoming chapters.
We are going to see the internals of the framework in depth in the next chapter.
Get Just Spring now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.