Conf42 Enterprise Software 2021 - Online

Let's build our own Dependency Injection framework!

Video size:

Abstract

Ever wondered how all the big frameworks make the “magic” called Dependency Injection happen? Then this is the talk for you! My journey from clueless to having a clue has given me a greater understanding of how this “magic” can be understood.

Allow me to demystify Dependency Injection.

Summary

  • Mark Hendriks: Inversion of control is an architectural pattern in which an outside entity wires together all the dependencies that you need and passes them through the class that needs them. He built his own dependency injection framework and he called it the injectinator.
  • We want to have our framework to be self reliant, no dependencies to outside code. Let's make our own annotation and let's call it injectme. Tell the compiler at what time do we want this annotation to be available. And the second thing we want to do is tell the compiler on which types of elements can we put this annotation.
  • Next thing that we want to do is make a little configuration class in which we can wire together the dependencies. We want to get the actual implementation of the, or actually the implementation class of our dependencies that we're going to inject. Make sure that you are working with an actual subclass of this base class.
  • There are three places which we could do dependency injection. There are constructors, fields and setters. But please don't do this in production. I'm not going to properly handle exceptions.
  • The next one we're going to do is constructor injection. We only want to do this with publicly accessible constructors. What we want is an actual array to save all the actual implementations into. If I did everything correctly, this should still work.
  • There's one more place where we can implement dependency injection, and that is via setters or method injection. Be careful though. If something goes wrong with calling the setters for whatever reason, you might end up with an incomplete object.
  • Inject via setters. First of all, we want to store our singleton implementation. If after this talk you've got any questions, feel free to contact me. Have a look at my GitHub repository for a more complete version of the framework.

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.
...

Mark Hendriks

Software Architect @ Ordina JTech

Mark Hendriks's LinkedIn account Mark Hendriks's twitter account



Awesome tech events for

Priority access to all content

Video hallway track

Community chat

Exclusive promotions and giveaways