Transcript
This transcript was autogenerated. To make changes, submit a PR.
Hello everyone, and welcome to my presentation about let's
build our own dependency injection framework. My name is Mark Hendriks. I'm a
software architect at Ordina, which is a it service
provider based in the Netherlands, Belgium and Luxembourg. And before we go
into building our own dependency injection framework, there's a concept that I
need to talk to you about, and that's inversion of control. And inversion of
control is nothing more than an architectural pattern in
which an outside entity, a container or framework,
wires together all the dependencies that you need and passes
them through the class that needs them, instead of that class having to instantiate
them themselves. Let's have a little code example which illustrates this a
bit better. The top example doesn't make use
of inversion of control because the my class has a dependency and
my dependency and it instantiates it with the new keyword.
And technically there's nothing wrong with this, it works. But if
we look at the second example, you see that the dependency is
passed through the constructor instead of it being instantiated
within the my class. And this is making use
of inversion of control. And technically speaking this is already dependency injection
because the my class is no longer responsible for instantiating
an instance of my dependency. And in this case it's
constructor injection because we're using a constructor and you can also
use field injection injecting it via fields, or you can
use setter injection using setter methods to inject dependencies into
a class. That way we're going to cover all three of those during this talk.
So yeah, dependency injection is nothing more than having an outside
entity providing you with the dependencies that you need.
And in spring you might have seen the at auto byte annotation before
they use it to tell that you want to have dependencies
injected in there. And Google juice,
Yafa Yakarta e. They all use at inject.
And if you look at this example, this is how you would do it in
spring. And I know you don't actually need to put the add auto
write annotation on there anymore in spring, because if there's only one
constructor, it knows implicitly that it's there. But this is just for demonstration
purposes, and this is the Java Jakarta ee
version of that. But how does it work? Well, for me it
felt a lot like magic, but of course it's not
magic. Let's be honest, if you look under the hood, it's actually
a clever use of the reflection API, which has been around Java
since the beginning of Java pretty much. And reflection is nothing more than can
API, in which you can inspect and manipulate code
and classes at runtime. And that might
seem a bit daunting, but as long has
you know what you're doing, it's not too scary to work with reflection.
From my own understanding, I wanted to know more about reflection, and I wanted
to know more about how dependency injection frameworks
actually worked. So I built my own dependency injection framework and I
called it the injectinator. And for those who know Phineas and Ferp,
which is a show by Disney, this is Professor Dolphinsmirtz
and he is terrible at naming all the suspensions enter into the post
fix inator. And that's what I got my inspiration from.
And you're going to see this throughout this talk. I'm just an engineer
and I'm terrible at naming things, like a lot of engineers,
so hence this name. Okay,
enough slides for now. Let's go straight into the code, shall we?
It's going to bring up intellij really quickly. Okay,
I've got a little demo project
here. Let me quickly remove this. Got a little demo set up
here. It's nothing more than a maven project.
I'm using Java 15, but the version of Java doesn't
really matter. I've tried it with Java 811 and 15
and it works on all versions, so it should work on your version.
Pretty much it just uses reflection.
I've got an awesome class which just
has a dependency on logger which isn't instantiated
anywhere here, and logger is nothing more
than just a simple logger, just to show that it's working. I'm going to use
these classes to show the purpose of the framework that we're
going to build, and an example class in which we're just going to wire it
together and get it started. So let me just close
these. The first thing that we need is a way to tell
our framework that we want to have something injected and we're
going to go the same route. We're going to use can annotation for that,
and we could use the add inject annotation, we could add
a dependency. We're not going to do that. We want to have our framework to
be self reliant, no dependencies to outside code.
So let's make our own annotation and let's call it injectme.
It's an annotation, and the first thing we need to do is tell the
compiler at what time do we want this annotation to be available.
Well, in our case we're going to use this for reflection. So we want this
to be available at runtime. So we're going to say retention and we're
going to say at runtime retention policy runtime.
And the second thing we want to do is tell the compiler on which types
of elements can we put this annotation.
And in our case I told you about three types of
dependency injection, constructor injection field injection and setter
injection. And those are actually the three types of elements
that we want to be able to put this annotation on. So just going to
say target and we're going to say we want to be able to
put us can element type constructor on element type
field and on element type method.
There's no setter here because setter is nothing more than a method, but we're going
to work around that in a while, in a bit.
Okay, this is sorted. Next thing that we want to do is make
a little configuration class in which we can wire
together the dependencies because we do have to tell the framework for
which actual implementation we want to be injecting that for.
So going to make an interface and to stay into theme we're
going to call this the syringe. And the syringe has two classes.
It has a configure so that
we can actually wire it together. And the second one being,
let me just type this out quickly. This will turn a class extends
t and we're going to call this get injectable because
we want to get the actual implementation of the, or actually the
implementation class of our dependencies that we're going to inject.
We're just going to say class type t and we're just going to call this
type and that's it. Now we're going to make an
actual implementation. So implement interface. I'm going to
call this my syringe for
the example. We're going to put it in the example package and we're going to
implement those. Okay, first thing that we want to do
is actually have some sort of a construction
in memory where we can save the dependency
with the actual implementation that we can map them. And we're going to use a
map for that private
map of type class. And we don't know which class it
is. And the second one, we don't know which class it is either,
because we can put in multiple dependencies in this map. So we're
going to call this injectables injectables
and it's nothing more than a new has map in
configure. We actually want to wire together the
interfaces with the implementation we're going to use. In our case, we're going
to use the logger with the mylogger implementation. So going to
say register injectable,
we're going to pass in the logger class and
the mylogger class.
This method doesn't exist obviously, so let's create
it. And what you actually have
to state here is going to use some generics. I'm going to say this will
be a type t. I'm going to call this the base class and
this will be a class which extends the
type t. And we'll call this the subclass.
And this is fairly simple. We just have to put it into the injectables map
injectables put base class
subclass and that's that. But we can do one more thing here
because we're working with classes. We can actually state where we want to use
this as a subclass of the base class. And what this does is actually
checking if it's actually assignable.
And I know the generics already take care of that, but I
would really like to feel fast and just make it extra sure that you
are working with an actual subclass of this base class.
Okay. And the next thing we want to do
is check if we have the injectable
ready, and if not we're going to throw an exception. So injectables
get for type.
We'll just save this and this is the injectable
and we're just going to say injectable.
If this is null, we'll throw a new
legal argument, exception. And we can say something
like no,
injectable registered for type.
Be creative with your message, I would say. And then we'll return
the injectable.
And this is an interesting one because we're now going to get a compiler error
because it says that we are expecting a
class that extends t, but we're actually giving back a class
which doesn't extend anything, not that we know
anyway, unknown type. So what we can do here is pretty much the
same as we did here. We're just going to pass this back as a
subclass of the type we just passed in.
And now it works. Okay.
Next thing that we're going to do is we're going to start off with the
example class and we want to instantiate
the injectinator so that we actually can start doing some injection.
So going to say injectinatorinjectinator
equals injectinator.
Getinjectinator of getinstance.
Let's go with getinjectinator we're going to pass in a
new my syringe and we're
going to create this. It's public static. We'll just say this is
a syringe and this is the syringe and this will return a
new injectinator which accepts the syringe for this
constructor doesn't exist so let's create it as
well. And we'll just say this syringe equals syringe.
This field doesn't exist so let's create that and
we'll just make this private.
Okay, that's part one, this part works.
Next, what we want to do is we want to get an actual implementation
of the awesome class which has all the dependencies injected. So let's get a
head start with that. So we're going to say awesome class, awesome class equals
injectinator and we'll say inject and we're
going to pass in the awesome class. This method doesn't exist so
let's create it. And this is not going to pass back an awesome class because
we want to make this generic. So we're just going to say it will return
a type t and we'll pass in
type t and we'll just call this the class to
inject to. And I spoke about
three places which we could do. Dependency injection. There are constructors, fields and setters.
And let's start off with field injection, shall we? So we'll just say return
inject via fields and we'll pass in
the class to inject to. This doesn't exist so let's create that
for now. We're going to return. No, first thing that we need to do
is make an actual instance of the class that we want to
inject into. So class to inject to get constructor
and new instance and we'll call this instance
and this will throw a couple of exceptions. And what I'm going to
do here today is the lazy man's route. But please don't
do this in production. I'm not going to properly handle
exceptions. I'm not going to do proper exception propagation either.
It's just to make it simple for sake of this talk
for tonight, but please don't do this in production.
Please throws exception,
just add it here as well. And what we're going to
do is for
that class to inject to. We're going to say get methods
or get method, my apologies. Get fields. We're starting with field injection and
as you can see there's two types. There's get fields and
there's get declared fields. And there's a
small distinction between the two. Get fields will only
give you all the publicly accessible fields, and the get declared
fields will give you all the non publicly accessible fields as well.
And we want to be able to annotate private
fields. So we're going to go with the get declared fields
and we're going to loop over those, just going to call it a field and
we're going to check if the field is annotated. So if field
is annotation present, going to check for
the injectme. If that's present.
First thing we need to do is set
accessible to true. This is just to comfort
the possible security manager that you might have running in your application.
And next, what we're going to do is field set.
And set takes two parameters
of object type and the first
one is the instance in which you want to set this field. And the
second parameter is the actual
value you want to set the field to in that instance that you passed in.
So we're going to say instance and
we're going to say field gettype,
but this isn't enough. What we're going to do now is we're going to do
a little bit of recursion because it
might be possible that the dependency that we want to inject has
dependencies of its own. So we're going to skip this
actually, we're just going to cut it and what we're actually going to do is
a little bit of recursion. We're going to call inject here again and we're actually
going to ask the syringe to get the injectable for
that type because this might be an interface and we want
to have an actual implementation of that interface. So that's why we ask the syringe
for an actual implementation class first and
then we return the instance. Okay,
so next thing we're going to do is the awesome class has
one method and it's log. And if I run this now this will give a
huge null pointer exception because we haven't instantiated
the actual dependency yet.
Then exactly as stated, logger lar causes a
null pointer but outwired.
My apologies. Inject me. If I just put inject me
in here and we run this code again. If everything went all right,
I made one little mistake and the
mistake is in the syringe, or actually it's in the injectinator.
What I forgot to do is we did the dewiring of
the different implementations of the dependencies
inside the configure method, and we haven't called this here, just a
small oversight. So this
syringe configure and now it should work.
It's not the most awesome message ever, obviously, but it just
demonstrates that it works. Okay,
so the next one we're going to do is constructor injection.
Let's go back to the injectinator here.
And what we're going to do next is we're going to keep this as the
default. So we're going to leave this at the bottom and we're going to
say class to inject to get constructors.
We only want to do this with publicly accessible constructors. So just
get constructors and we're going to loop over them.
And constructor is annotation present
for the injectme class. If that's the case we're going to say return
inject via constructor and
we're going to pass in the constructor and the class to inject to. And why
we do that, I'll tell you in a second.
Okay, we're going to start off with a return null. And there's a couple of
things that we need here. First, what we're going to do is we want to
know all the parameter types of the constructor that we're
going to use for the injection. So constructor get
parameter types and we're going to
put these in an array of type class.
And the next thing, what we want is an actual array to save
all the actual implementations into because we're going to need that to
actually call the constructor in a bit. So we'll just say object
call these dependencies and
it's not more than a new object array with the
length of the parameter types array.
And we're going to have a little external iterator
here and we're going to loop over the parameter
types. And what we're going to do is for the dependency
in index I zero base counting.
So we start with zero is we're going to do the same recursion here
and we're going to say inject and we're going to ask the syringe
to give us the injectable for the parameter
type. This throws an exception. Let's just
throw it and then we're going to do an I plus plus.
Okay, we could also do I plus plus here, but for
simplicity and readability's sake I'll just leave it here.
And what we're going to do now is actually we're going to tell the class
to inject to,
to give us the constructor with
these parameter types, make a new instance
and use all of these implementations of
the dependencies that you need to use that constructor that we're going to use.
Okay, and if that all works, let me
just remove this one for now. Let's get
ourselves a nice constructor, annotate it with
at injectme, and if I did everything correctly, this should
still work, same message, so it still works.
Okay, going back to injectinator, there's one
more place where we can implement dependency injection,
and that is via setters or method injection, I prefer that
name. Okay, so what we're going to do, we're going to say that it's the
second place that we're going to check. So we're going to do here is the
class to inject to get methods.
And you see there's two types here as well. It's get methods and get declared
methods. And case being methods is
only the publicly accessible ones and the get declared will also
give back all the non publicly accessible ones. But a setter
is public by default. So we're only going to check for publicly
accessible methods. I'm going to loop over those,
we're going to do the same check. So method is annotation
present for the injectme class and if that's
the case we're just going to do return inject via
setters. Class two, inject two,
this doesn't exist. So let's create that method as well.
This is all right. And we have to do the same here.
We're going to start off with getting a new instance,
just going to save this to instance and this will
throw an exceptional,
this will eventually give back the instance and we're
going to loop over the methods now. So class to inject
to get methods. And what
we're going to do is method is annotation present
for the injectme class,
and that's
far if obviously. So if it's available then
we're going to do pretty much the same. We're going to say method not
set, but it's called invoke in this case. And it takes two parameters as
well, both of type object and the first one
being the instance that you want to invoke this method on.
And the second object is actually four arcs,
which are the actual objects
that you want to pass into that method that you're calling. So invoke
instance. And in our case we're going to do the same recursion,
not instance, inject syringe,
get injectable. And this might seem a bit tricky, but I'll
get back to you in a sec. Get parameter
types, which gives back an array. And just going to
bluntly say that we want to get
the first one type
parameters get parameter types wrong
way around. Okay? And some of you may state, well, this could cause an
index out of bound exception. And you're correct, it can.
But the thing is, a setter, if you check the bean specification,
it's always public, it always starts with set. It always only has one parameter count.
So let's build in some safety checks for ourselves,
shall we? So we're going to check if the annotation
is present and also the method
name. For instance, get name starts
with set. That can be another check. It doesn't say much
at the moment. Let's just format this a little bit and
we can do one more check. We can also check if the method
get parameter count and
that should equal one and only then will
this type of this will
be invoked then. So we sorted
that out. This should no longer cause can index out of bound exception. And let's
see if this still works. Let's have a
look. Go back to the awesome class. Let's just remove
this constructor for now, no need
for it at the moment. And let's just put a
setter in here and put at inject
me on top of that.
And it still works. Be careful
though. Setter injection, it works. But the thing is,
even after construction, if something goes wrong with
calling the setters for whatever reason, you might end up with an
incomplete object. With a constructor injection,
your code will just fail and die pretty much because you won't be able
to get an actual half constructed object. And in this case you
can. So do be really careful with that.
Just keep that in mind. Okay.
If you take a look at the spring framework, for instance,
for all the dependencies that they inject, they give back a singleton.
And in our case we make a new instance every single
time that we need it. And to demonstrate that that's actually the case,
let's just quickly go into my logger,
put a private int called,
called count. We'll just
set it to zero and we'll just put in an extra message and
say times called.
And we'll say plus, plus call count.
And if we go to the example app and just
duplicate these,
it's two different instances and they each
have their own instance of the logger. So if you
run this, you should see times called. It's both
one in both examples. So let
me quickly show you how we can actually make
it so that we can decide if we want to have a new instance so
that we get a singleton instead. So what
we're going to do is we're going to introduce a enum and we'll just
call it injection type.
And there's only two values, either new or singleton
and that's that. Next we need to update the
injectme so that we tell it that it can take
a value. We'll say injection type.
And if we call this, I'll just call it injection type to start
off with. I'll show you what I mean.
And we'll say it defaults to the new.
There's no related problems. The thing is, if we want to
keep it as is, we don't need to do anything. But if we want
to say a singleton here, we actually have to say injection type.
Is the injection type singleton. And this can be
shorthanded. We only use this. So if you call this value instead,
that is the default. And in that case you
can just leave this out.
That's a nice shorthand for that.
Okay, I'm just going to remove this
for now. And what we're going to do next is we
already got our setter in place. So let's
start off with implementing the singleton
implementation for setters. So inject
via setters. And first of all, we actually want to store
our singleton implementation. So we're going to say private
final and we're going to call this, make this a map as well.
And it is a map of type class
unknown can object because we're going to store actual implementations
in there and we'll call this singletons and
it's nothing more than a new hash map.
Okay, so what we're going to do now is we're going
to do one extra check over here and the check will
be if the annotation has its value set to singleton
or not. So method get
annotation injectme class value
if that equals Singleton.
If that's the case, we're going to do
something new and otherwise we're
going to keep using the implementation that
we already have. And this looks pretty much like this, but I'm just
going to copy paste it. And the only thing that we're going to change
is that we're not actually, I'm just
going to clean this out,
get singleton and get singleton for the
method, get parameter
types and it is the first one.
This method doesn't exist, so let's create it and it'll
just give back an object which is fine. In our case
class, we don't know, we'll just call this type and
we're not going to return null.
And what we're going to do is singletons
that contains key. We're going to check if it's already present
in the list. And actually we're going to check if it's not present in the
list. If it's not in the list, we have to put it in there first.
So singletons put and we're going to put for type,
and what we're going to do is inject
syringe. It's the same stuff over again with
same recursion.
And this throws an exception. So let's just throw that and then we're
going to say return singletons
gettype. Okay,
fairly simple. So if I run this now, it will
still have the same outcome,
still two different implementations, two different instances
pretty much. And if we
go to our awesome class and we actually say injection
type singleton and now see
that it's been called twice. So even though both
the instances of the awesome class want to have
an instance of the mylogger, they only use
one actual instance for that. So that's
really nice. Okay, I'm going to leave it with
this for the building around the pension framework.
If after this talk you've got any questions,
feel free to contact me. You can find
me at the cheerfuldev on Twitter. That's my Twitter handle.
Just ask questions there if you want. And if you want to have a more
complete version of the framework that we just built together,
have a look at my GitHub repository. It's the
cheerful deaf and the project is called the injectinator. So thanks again
for your time, and I hope you learned something. Have a
good one.