Introducing Butterfly DI Container
Being about 3 years old and reasonably matured, I thougt I'd take the liberty to introduce Butterfly DI Container to the JavaLobby community. Butterfly DI Container is a dependency injection container like Spring, Pico and Guice. For those of you unfamiliar with dependency injection, I have put up a small tutorial entitled What is Dependency Injection?
Butterfly DI Container is part of a small, ultra lightweight component stack called Butterfly Components (Open Source - Apache License 2.0). Rather than being a copy of Spring, Pico, Guice etc., Butterfly DI Container and the other components are build on a slightly different philosophy:
- External dependencies suck.
- Size does matter - keep it small.
- A feature is only worth implementing if it can be made really easy to use.
Thus you won't see any of the Butterfly components depend on external frameworks. They rarely depend on themselves internally too. External dependencies makes small frameworks grow huge in no time. Too often have I seen megabytes of external code included only to call a few methods here and there, that the developer was too lazy to implement himself / herself. If you are to include an external dependency in a component, it should be because you are using that external dependency a lot.
You won't see components that are 10-20 megabytes in size. That code needs to go into memory before it can be executed, and I'd rather see that memory used for app data than wasted on unnecessarily verbose code. Large code bases may also increase build times, by the way. And who wants to wait even a minute from the time you've mad a change, to the time you can see the effect in the app you are building? Long build times during development suck. Therefore, Butterfly DI Container is just a 99 KB jar file.
You won't see Butterfly components littered with features that are so difficult to use, that it would have been faster to code a tailored solution yourself. Some frameworks think the should have solutions to every imaginable problem. But many of these solutions are hard to learn how to use, and thus time consuming to learn. Sometimes to the extend that it would be faster to just code it yourself. And some solutions are so inflexible that you constantly need to work around their limitations. You won't see that in Butterfly components either. I'd rather leave a feature out, if it cannot be made very easy to use.
Butterfly Components home is here: http://butterfly.jenkov.com
But enough about the component stack. This text is only about the dependency injection container in the stack:
Butterfly DI Container.
The Butterfly DI Container's home is at http://butterfly.jenkov.com/container/index.html
Butterfly DI Container is not for everyone. It is for those of you who agree with my philosophy as listed above. Additionally, Butterfly DI Container is for those few of you out there who do not feel that XML, annotations or a Java API is the most optimal configuration mechanism for a DI container . I studied each of these options when I had to choose a configuration mechanism for Butterfly DI Container. At first Butterfly DI Container used XML, but it was too clumsy, even if it was more condensed than Spring's XML format. Then I looked at Pico's Java API, but I found that as soon as configurations became just a little bit complex, the Java API was hard to tweak to get the effect I wanted. Around that time Guice came out, and I took a quick look at its annotation configurations. But annotations by themselves are insufficient to configure all use cases of a DI container. Guice also acknowledges this by supplying an additional "provider" API (a Java API). But I had already abandoned a pure Java API.
Turning my back on a Java API, annotations and XML only left one configuration mechanism left. The hardest one to implement: A custom script language tailored for DI configuration. And that's what I'll be talking about for the rest of this text.
I decided to separate the container from its configuration mechanism. That way, the container and the configuration mechanism can change independently from each other. So, this is how it looks to create a container:
IContainer container = new Container();
And this is what it looks like to configure it:
ScriptFactoryBuilder scriptBuilder = new ScriptFactoryBuilder(container);
//inline factory definitions
scriptBuilder.addFactory("myBean = * com.jenkov.MyBean();");
//external file factory definitions
scriptBuilder.addFactories(new FileInputStream("scriptFile.bcs"), true);
To obtain an instance of the "myBean" factory added in the second code line, you call the IContainer.instance() method, like this:
MyBean myBean = (MyBean) container.instance("myMean");
Pretty simple. You can actually plugin plain Java factories too, and these factories can be referenced from script factories, and vice versa. But I'll leave that out in this text.
The script language is designed to look like a cross between a standard property file, and ordinary Java code. Here is how you define a simple bean "factory":
myBean = * com.jenkov.MyBean();
Here is what it means:
"myBean" is the factory name. It is this name you pass to the container.instance() method when you want an instance from that factory.
'=' separates the factory name from the factory definition.
'*' means "new instance" - which again means that a new instance is created and returned every time the factory is called.
'com.jenkov.MyBean()' is the class name and constructor to call.
; ends the factory definition
If you wanted a singleton instead, you'd write 1 instead of *, like this:
myBean = 1 com.jenkov.MyBean();
If you leave out the instantiation mode (*, 1 etc.) the factory definition will default to singleton. This is handy when using Butterfly DI Container for ordinary application configuration, instead of property files, like this:
numberOfThreads = 20; numberOfConnections = 20; dbUrl = "jdbc:h2:tcp://..."; operatorEmail = "email@example.com";
It looks close enough to a standard property file, that you can expect a server administrator to understand it. You can separate these properties out into a separate script file, that server administrators are allowed to change. Then you can have these values injected directly into your objects, inside other script files, like this:
myBean = * com.jenkov.MyBean(operatorEmail);
Or, like this:
myBean = * com.jenkov.MyBean().setOperatorEmail(operatorEmail);
Notice how the method setOperatorEmail() on the MyBean instance is called in the script here. You can call any method from inside a script file, and not just "setter" methods and constructors. This is when the script starts to look like Java.
You can actually chain method calls too. Methods that return void are interpreted as returning "this". So, you can chain standard setter calls like this:
myBean = * com.jenkov.MyBean()
.setOperatorEmail(operatorEmail) .setSomethingElse("something else");
You can also provide parameters to the constructors, like this:
myBean = * com.jenkov.MyBean(operatorEmail, "something else");
Pretty much like in Java.
You can also call static factory methods instead of constructors, like this:
myBean = * com.jenkov.MyBean.newInstance();
And you can of course chain method calls on this MyBean instance too, like this:
myBean = * com.jenkov.MyBean.newInstance() .setOperatorEmail(operatorEmail) .setSomethingElse("something else");
You can provide input parameters to a factory, and have these input parameters injected into the created object. Butterfly DI Container was the first DI container to offer that. Here is how that looks:
myBean = * com.jenkov.MyBean($0);
The $0 signals an input parameter. Not totally Java-ish, but still understandable. Here is how you provide an input parameter when instantiating the "myBean" object:
MyBean myBean = (MyBean) container.instance("myBean", "some value");
The String "some value" will then be injected into the constructor of the MyBean class.
You can also utilize input parameters internally in script files, like this:
myBean = * com.jenkov.MyBean($0); myBean1 = myBean("some value"); myBean2 = myBean("other value");
Notice how the "myBean" factory almost became a function that is called by the "myBean1" and "myBean2" factories. You can actually also call other factories without input parameters, like this:
myBean = * com.jenkov.MyBean(); myBean1 = myBean.setOperatorEmail(operatorEmail1); myBean2 = myBean.setOperatorEmail(operatorEmail2);
There are shortcuts for List's and Maps. Here is how that looks:
myMap = <"key1": "value1", "key2", 999 >;
myList = [999, "String", myBean];
In fact, Butterfly DI Container supports a whole list of other, more advanced features like
- More advanced configuration phase (life cycle phase)
- Dispose phase (life cycle phase)
- Custom life cycle phases
- Factory injection - so a component can call script factories at runtime to obtain instances.
- Internationalization of components (currently in testing)
- Runtime replacement of factories. Handy during unit testing. Replace a real factory with a mock, and run your test.
- Seamless integration with Java, when the script isn't expressive enough. Just call a static method, or a method on some factory class of yours from inside the script, and do the advanced stuff inside Java.
Since this is an introductory text, I'll leave out these advanced features. You've got a taste of how simple the script is to use. The interested reader can read more here.By the way, the script doesn't make Butterfly Container slow. It performs better than Guice, in the last performance comparison I did. And Guice claims to be faster than Spring... You can see the comparison here.
That's all from me now. Have fun.