We've talked in a previous lesson
about how one of the main features
of Spring is that it uses dependency injection and inversion of
control. But what does this actually mean?!
It's actually really cool, so let's try to understand why.
A lot of us, myself included, have been spending a lot of time
lately doing jigsaw puzzles. Jigsaw puzzles are fun and relaxing,
but what's interesting is that when you have a jigsaw puzzle, you
can only use the jigsaw pieces for that puzzle. For example, if
I have a puzzle of Diagon Alley from Harry Potter, I can't use
any of those puzzle pieces in a flower collage puzzle.
Puzzle pieces are tightly coupled to each other: you can only use
puzzle pieces in one jigsaw puzzle.
On the other end of the spectrum, we have Lego. Lego pieces can
be used to build anything! You can buy a Lego set for Hogwarts
Castle, or you can get a Lego set for the Millenium Falcon.
Because Lego pieces are created to fit to any
other lego piece, you can use Lego pieces to build anything
you want: you're not limited to what's on the box!
So you can use your Harry Potter and Star Wars Lego sets and use them
to build anything!
There's actually a web site called
Rebrickable
that shows you things you can build with
mixed Lego sets!
Java classes should be independent from each other, like Lego pieces.
Java classes should be designed in such a way that they can fit into
any program.
This makes them more reusable - you can use a class in a project without
having to edit it or bring other classes with it.
This allows you to test them independently of other classes, also.
Java classes should not be like a jigsaw puzzle.
In software design you learned about Robert C. Martin's SOLID
principals. By following SOLID, your Java classes will be just like
Lego pieces. If you didn't learn SOLID or need a review, I've added some links
in the up above, and there are several videos where Uncle Bob
lectures about his SOLID principles, like this one:
Dependency Injection
Dependency injection uses the principles of SOLID to keep classes independent
from each other while still allowing you to use independent classes together
if you need to.
For example, say you are designing an application that manages various
delivery drivers that can deliver packages and cargo, and can also
pick up individual customers and deliver them to destinations.
public class Driver {
private String name;
private Vehicle vehicle;
public Driver() {
vehicle = new Vehicle("car49", true);
}
}
public class Vehicle {
private String id;
private boolean active;
}
Here we've created a dependency between Driver and Vehicle.
If we want to instantiate Driver, it's going to require that
Vehicle is also instantiated. This creates a very tight
level of coupling between the two classes that we don't want.
And what if there are different kinds of vehicles? Maybe there
are cars for transporting individual customers, and trucks for
transporting packages and other cargo.
public abstract class Vehicle {
private String id;
private boolean active;
}
public class Car extends Vehicle {
private int numPassengers;
}
public class Truck extends Vehicle {
private double maxCargoVolume;
private double maxCargoWeight;
}
A driver could be assigned any particular vehicle if they're
licensed to drive it.
public class Driver {
private String name;
private Vehicle vehicle;
public Driver() {
vehicle = new Car("car49", true, 2);
// or instead:
// vehicle = new Truck("truck10", true, 128.5, 1500.0);
}
}
Now what if we add a new vehicle Bicycle for carrying
food deliveries in one or more carrying boxes?
public class Bicycle extends Vehicle {
private int numBoxes;
}
Every time we want to start using a new vehicle type, we have
to go in and modify our Driver class. This violates the
Open-Closed principle of SOLID:
A class should not have to be modified for it to be extended.
Inversion of Control
Inversion of control is a principle that reverses, or inverts,
the process we've demonstrated above.
Instead of the Driver class having control over the construction
of the Vehicle class,
we should instead reverse or invert the control so that Driver is not
constructing instances of the Vehicle class at all.
We can accomplish this by creating an instance of Vehicle somewhere
else and pass the instance of Vehicle into the Driver class.
This is what dependency injection does.
The basic premise of Dependency injection is that if ClassA depends
on ClassB, ClassA is not responsible for instantiating ClassB.
This can solve several problems.
For example, instead of instantiating vehicle inside of Driver,
we can instantiate it elsewhere and simply pass the instance
of vehicle to driver. And this is dependency injection! We are
injecting the dependency (which is the vehicle object) into the
Driver class by passing the vehicle object into the Driver's constructor.
public class Driver {
private String name;
private Vehicle vehicle;
public Driver(Vehicle vehicle) {
this.vehicle = vehicle;
}
}
This decreases the level of coupling between the two classes
and thanks to polymorphism, means that we can now add as many
new types of vehicles as we want without having to modify
the Driver class:
// somewhere else, external to Driver class:
Vehicle v1 = new Car("car49", true, 2);
Vehicle v2 = new Truck("truck10", true, 128.5, 1500.0);
// vehicle is injected into Driver via the constructor
Driver d1 = new Driver(v1);
Driver d2 = new Driver(v2);
Now Driver doesn't have to worry about the implementation of Vehicles at all
because Driver no longer has control over the instantiation of Vehicles.
Vehicle is created outside of Driver, and the reference passed to Driver.
In other words, the vehicle is injected into the driver constructor.
Dependency Injection is a type of Inversion of Control.
The flow of control has been inverted by Dependency Injection: the dependency
is being created externally instead of internally.
Dependency Injection can also be done via setter methods in the Driver class.
For example, a vehicle can be passed into a setVehicle() method of the
Driver class, and the setVehicle() method assigns the vehicle parameter to
an instance variable:
public class Driver {
private String name;
private Vehicle vehicle;
public Driver(Vehicle vehicle) {
this.vehicle = vehicle;
}
public void setVehicle(Vehicle v) {
this.vehicle = v; // or v.clone()
}
}
Dependency Injection in Spring
Spring uses dependency injection to make a lot of tasks much
easier for us when we develop enterprise applications.
Spring is instantiating objects for you behind the scenes.
when you start your application class with
@SpringBootApplication,
one of the things it does is a component scan: it looks for classes
with annotations and it instantiates each one of those and puts them
in a container called the Inversion of Control Container.
The Inversion of Control container is where all those instantiated
classes are kept until they are called upon by something else in the
application.
For example, when Spring starts up and sees your class with the
@Controller annotation,
it instantiates it and keeps it in the IoC
container so that it can give it to you when your code needs it.
When some code in your application needs the controller, Spring goes
and finds it in the IOC container, and then injects it where it's
needed.
Spring does this with other classes and objects, too. When Spring
sees certain annotations when it scans for components, or when you
ask for an instance of certain classes, Spring locates it for you
in the inversion of control container and gets it for you,
all set up and ready to go.
For example, in an earlier video, I showed you how to use
the HttpServletResponse class to modify the body of the response
before it's sent to the client.
Your handler did not instantiate the HttpServletResponse object,
it was a parameter in your method: spring had one already constructed
and filled with all the information it needed and it was put in the
inversion of control container just waiting for someone to ask
to use it.
When you created the parameter,
Spring said, "Oh, I have
one of those in my Inversion of Control container.."
so it retrieved the instance of the response object
and injected it into the method by using the parameter you added
to your handler method.
This is also where the request params come from: they are injected
into the handler method when we create parameters for them.
When Spring sees @ResponseParam("firstName")
String firstName
Spring says, "Oh, I've got a String here that came from the query string
and it's called "firstName", here you go."
One of Spring's jobs is to watch for parameters and classes with
certain annotations and references to certain objects and classes:
when it sees them, it looks for something in the inversion of
control container that fits the shape of what you asked for
and injects it into the parameter.
It's like going to an auto parts shop to look for a part for a car
- you can ask the shop owner, "Do you have a fuel door for a
Jeep Patriot?" and the owner says,
"Sure, I have one right here!"
The owner doesn't have to tell you to wait so he can go and
build one, he's got one right in the shop.
Let's do a quick example of Dependency Injection so you can get
a better idea of how it works in Spring.
Demonstration of Dependency Injection
Start up a new project in Eclipse.
Create a very basic Message Bean. Add a class
Message to your
.beans package and give it
a private String member called
message, and you can assign
a default value if you like.
Add a default constructor. It doesn't have to do anything
unless you prefer to use it to assign message a default
value.
Add get/set methods for the message instance variable,
and a toString() that just returns the value in the message
instance variable. Feel free to use Eclipse's shortcuts
(highlight the member and click Alt-Shift-S or right-click
the member and select "Source"; then
select Generate Getters and Setters or Generate
toString())
Now, add the @Component annotation to the Message class header.
@Component is one of the annotations that Spring looks for when your
application starts.
When Spring sees @Component on your Message class, it will
instantiate this class for you and store it in the inversion of
control container until you ask for it.
Here is what your Message class might look like so far:
import org.springframework.stereotype.Component;
@Component
public class Message impolements Serializable {
private String message = "Demonstrating Dependency Injection!";
private long serialVersionUID = 1L;
public Message() {}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
@Override
public String toString() {
return "Message [message=" + message + "]";
}
}
Next, add a controller to your project and add a private data member
that will eventually hold a reference to a Message instance:
private Message msg;
Make sure that when you're prompted to import Message,
you import your own!
Now we'll add another annotation here:
@Autowired.
@Autowired
private Message msg;
@Autowired tells
Spring to go look inside the Inversion of Control Container and see
if there's anything in there that will fit into this variable.
Of course, Spring will find the Message instance it created when it
did the component scan at application startup!
Now let's map to root with a handler method.
We'll print our message object to the response body
so let's add the code to do that.
Note that we haven't instantiated our Message class!
Will this work? I guess we'll find out.
Go ahead and run your program, then use your browser
to go to http://localhost:8080
We are able to see our message object's toString(), even though we didn't
explicitly say Message whatever = new Message() !
That's because Spring did that part for us when it saw the @Component
annotation in the Message class, then it stored the instance in the
inversion of control container. And recall that it also does this
with the HttpServletResponse object, too!
When we @Autowired the message instance variable,
Spring went to the container
to look for somethign that would fit into that variable, and it found
the message instance it created on startup.
We'll play a bit more with @Autowired throughout the term so we can access
lots of other stuff.
Exercise
This interesting exercise demonstrates dependency injection
with your own classes. We're going to pretend we work
at a vet clinic that has patients (animals) that are the
pets of our various clients.
First, create a bean called Animal with private
String members for name (e.g. the animal's name,
such as "Fluffy"), type (e.g. "cat", "dog", etc),
and breed (e.g. "Persian", "Beagle", etc.)
Make sure there's a default constructor.
You might also want to add an all-args constructor, too.
You can use the shortcuts to create basic
accessor/mutator methods, toString(), etc.
Don't worry about the finer details like
validation, they don't matter for this quick
exercise. Use any defaults you like, if you want.
Now we're going to create a Repository, which
is just a class to store data, in this case
it's going to be a basic collection or list.
The first part of our repository is an interface.
Why an interface? This follows the rules of SOLID!
We will make an interface that contains abstract
methods that represent the functionality of the
data store we want to create.
Add a new package .repositories
Add an interface PetParent
to the .repositories package.
Add the following to your PetParent interface:
adoptPets() - accepts a List of Animal
objects and returns nothing
getPetList() - accepts no arguments
and returns a List of Animal objects
numPets() - accepts no arguments and
returns an integer representing the number
of pets a client has
Remember that all methods in an interface
are automatically abstract.
Now we add a collection class that implements
PetParent. The Client class is a concrete
class that represents a client that has
one or more pets (a collection of Animals!)
Add the Client class to the .repositories package.
Add the @Component annotation to the Client class.
The Client class needs to implement PetParent.
When it prompts you to override the abstract
methods, just accept.
Client needs a private data member that is a list
of Animals. It's best to use the CopyOnWriteArrayList()
as your actual type because it's thread-safe
(in a web application, it's possible other users might
try to access/modify your data at the same time, and
we want to make sure changes aren't overridden by
each other).
List<Animal> pets = new CopyOnWriteArrayList<Animal>();
Override adoptPets() to copy all the items from
the parameter list to the pets data member list.
e.g.
pets.addAll(paramList);
Override getPetList() to return the pets collection.
Override numPets() to return the size of the
collection e.g. pets.size()
Now you can add an index.html to your /templates folder
and add the form code I wrote for you:
https://pastebin.com/Qa0uZsJW
In your controller, map the index.html to the root "/".
Now the interesting bits!
In your controller, add a private data member that's of type
PetParent. Remember, PetParent is an interface, but we can
use interfaces as declared types. Remember to import the
correct package, when prompted.
private PetParent petParent;
Now we don't usually need these, but add a constructor method
to your controller and give it a parameter of type
PetParent.
public MainController(PetParent petParent) { ... }
In the constructor, call super() and then assign
the parameter to your petParent data member.
Now add one more handler method that's mapped to
"/adopt" (or if you changed your form's action
attribute, use that. If you want to output to
the response body, make it a void method.
If you want to use System.out.println(), make
it return a String and just have it load
the index page when it's done. Add code to the handler
method to do the following:
Add a parameter for the HttpServletResponse, if
you're using it.
Add request parameters for the form inputs (adopter name,
pet name, type, breed).
Use the form inputs pet name, type, and breed to
construct a new Animal instance.
Add the new animal object to the pet list.
How? You have the instance variable petParent.
You can invoke getPetList(), which returns the List<Animal>
of pets. Then chain the add() onto that:
petParent.getPetList().add(pet);
(here, my animal object variable is called pet)
Create a nice bit of formatted output that lists
the adopter's name followed by all their pets.
You can iterate through the pet list and just concat
each pet object to your output string.
Send your output to the response!
To test out the program, run it. Then enter
some data into the form and submit it.
You should see the pet parent name followed by
the one pet you just added.
Now this is super cool: hit the Back button and go
back to the form, enter a new set of data and
submit it. You'll see 2 pets in your list!
How did this work?? We didn't instantiate anything
and we didn't even need @Autowire! And we didn't even
use the Client class, yet somehow we did!
Here's what happened:
On startup, Spring did the component scan and
saw the Client class with @Component, so it
instantiated Client and put that object into
the IOCC.
When your controller started, it created the
private instance variable
PetParent petParent
(remember, PetParent is implemented by Client).
Then the controller's constructor executed. Spring
has some serious magic: if you add exactly one
constructor to your controller, Spring will
execute it and you won't need to @Autowire it!
Spring sees the PetParent parameter in the
constructor and thinks, "Do I have anything that
fits into that parameter?" It checks the IOCC
and guess what it finds? The Client instance it
created on startup! Remember, Client implements
PetParent so "Client is a PetParent". It fits!
In the constructor, the address of the client object
from the IOCC is placed into the petParent member
of the controller. Now your controller has access
to this client object via the petParent
member.
When the adoption form handler executes, it's
referencing the same petParent member, which is
referencing that same Client instance in the IOCC!
If you want to explore, try commenting-out the constructor
and adding @Autowired to the petParent member, instead.
It should still work!
Another way you can inject is via a set-method. Why
not try that, too? Keep the constructor commented out,
comment out the @Autowired above the data member.
Now add a setPetParent(PetParent petParent) that does
the same thing as the constructor. Don't forget to add
@Autowired. Still works the same!
So now you know 3 ways to do dependency injection.
Using the constructor is by far the best way and what
most developers prefer, but it's good to know the
other two ways, also!