Any seasoned Java developer knows what dependency injection is. Beside all (perfectly correct) things said in regard to this pattern, DI frameworks are making our lives easier by taking care of all classes spread across project code and connecting them into shining working app.
Well, not all DI frameworks and libraries made equal. Most of them targeted toward quite large projects. Some of them are so huge by themselves that there is just no point to use them in smaller projects.
The size of DI frameworks/libraries are only half of the story, most of them oriented to same use case - assemble your application from pieces and then forget about dependency injection. It's true, that this is most often used scenario, but, well, it is not the only one.
If you try to use DI at run time to create new instances, you quickly find that creating instances this way is very slow. There are two reasons for this. First of all, call to constructor using reflection is slow. Second is that some frameworks add a lot of overhead by performing run-time dependency resolution for every object instantiation. Result is extremely slow, so slow that it makes use of DI framework meaningless for this use case.
Some time ago I decided to create small DI framework which would be convenient to use in small projects. It appeared quite interesting task by itself.
While trying to achieve best possible performance I've discovered that use of Lambda Factory to create code which instantiates objects at run-time allows achieve extremely fast instantiation. But the setup of lambda takes more time than reflection-based constructor invocation. In other words, reflection-based construction is just fine when it is necessary to create only few instances but not in cases when many instances need to be created. Lambda-based approach has opposite properties - it's slow to setup but then works much faster. Unlike reflection calls, this approach is subject of JIT optimization, so if some object is created often its creation quickly gets optimized.
The Booter Injector combines these two mentioned above instance creation methods and transparently switches from one method to other at run time. This results to very good performance and short startup time. This library also has very small footprint - single jar, slightly above 50K (yes, K, not M) and zero external dependencies.
Of course, it has much less features than, for example, Spring or Google Guice. Actually rich set of features was not intent of the library. In fact set was intentionally limited for different reasons. For example, only "all-args constructor" (or default constructor) is supported. No direct field- or setter-based injection. There are several reasons for this:
- all-args construction is more consistent and easier to test
- it's faster during dependency resolution and at run time
- If class has only one constructor, there is no need to add specific annotations, in many cases all necessary dependencies can be discovered without additional annotations at all.
Booter Injector utilizes lazy dependency resolution, so initial configuration of DI container is not necessary (although possible). Also, it uses two annotations to facilitate dependency resolution at run time:
-
@ImplementedBy
- this one is basically identical annotation used by Google Guice and links interface with default (or sole) implementation -
@ConfiguredBy
- this class-level annotation is used to provide additional configuration for the DI related to particular class
Last annotation provides convenient way to keep class DI configuration close to the class being created and/or structure DI configuration for the application.
Comments/ideas/contributions to the library are welcome.
Top comments (2)
That's really interesting. I've looked into the code. I really want to know how this library works. Thanks. Can you provide more details on this implementation?
Any runtime reflection based DI container basically nothing else than a map between types (take a look at Key class) and function which is able to instantiate such type. Such a function, in turn, may invoke other similar functions to create necessary dependencies.
This library uses several tricks to achieve high performance, but the main one is the use or run-time generated lambdas to invoke constructor (or provider method). Unfortunately, these tricks making library incompatible with JDK other than 8.
Overall, run-time dependency injection should be ditched in favor of compile-time injection.