The Opportunities Missed by Annotation Based Dependency Injection
There has been a lot of fuss lately about the @Inject JSR, which defines a set of Java annotations to be used as configuration for dependency injection. Having developed Butterfly DI Container, a Java dependency injection container, I am of course watching the development of this JSR.
On may 26th I received an email from one of the JSR's supporters, asking about my opinion about it, and whether I supported it or not. I did reply to that email, but since then I've had a bit more time to think. This text contains my opinion about annotation based configuration of dependency injection container. Well, the same is true for dependency injection containers configured programmatically.
Here is a list of the topics covered in this text:
- The Integration Opportunities of Dependency Injection Containers
- The Limitations Imposed by Choice of Configuration Mechanism
- The Opportunities of a Dependency Injection Domain Specific Language
- Using a DI DSL for Wiring Up Components
- Using a DI DSL for Application Configuration
- Using a DI DSL for Internationalization (I18N)
- Using a DI DSL to Externalize DDL and SQL
NOTE: This text uses SVG diagrams which not all browsers can show natively. If you cannot see any of the diagrams, and you are using Internet Explorer, you should install the Adobe SVG Reader Plugin. Firefox 3.0.5+ and Chrome users should be fine without this plugin.
The Integration Opportunities of Dependency Injection Containers
In 2006 when I was designing the first version of Butterfly DI Container, I was giving considerable thought to what configuration mechanism to use. Of course the best suited mechanism depends on what you want the dependency injection container to be able to do.
Little by little I realized the vast opportunities hidden in the realm of dependency injection containers. A dependency injection container may initially have been intended for wiring up components, but they can do so much more than that. A dependency injection container utilized to its full potential is a single, central integration unit for your application. Not only can it bind your components together, but it can also provide a simple, standardized integration into your application of:
- Injecting application configuration parameters.
- Internationalization - both texts and components like number formatters.
- Externalization of DDL, SQL, OQL, XPath, Reg. Expression, URI's etc.
- Externalization of Aspect Oriented Programming (AOP) configuration..
- Integration of binary resources like images, PDF templates, etc.
The opportunities are illustrated here too:
In fact, used with a bit of imagination a dependency injection can be used to integrate pretty much anything you need integrated into your application. This eliminates the need for you to invent your own integration mechanisms. For instance, should you use property files or XML files for configuration of your application? Or for your localized texts? Or for your externalized SQL?
Using a dependency injection container you have the opportunity to standardize how you configure and externalize all these resources. Of course the dependency injection container must be geared towards being able to integrate all these resources.
The Limitations Imposed by Choice of Configuration Mechanism
Dependency injection containers today typically use one of these four configuration mechanisms:
- Java Code - Method calls, Factories, Providers etc.
- Annotations - @Inject etc.
- Domain Specific Language (DSL)
Not all of these configuration mechanisms have the same possibilities. Java code and annotations are mostly useful for wiring up components, but how do you specify that you want a configuration parameter injected into an object? Or an externalized SQL statement?
If I am developing a third party API for use in somebody else's project, I may be able to mark where to inject components using the @Inject annotation, but how can I specify a configuration parameter? I can't, because I don't know what parameters the user of my API will have. If someone is to wire up my component and inject configuration parameters into it, they will need some extra mechanism to extract configuration parameters and inject them into the components of my API.
Java code and annotations do not provide a seamless integration between components and these external resources.
Then there is the whole issue of how to configure injection into a third party API for which you don't have the source code for. How will you do this with annotations? You can't. You need some other mechanism to support the annotation based configuration.
Both XML and a DSL work far better as configuration mechanism when integration is a high priority. Spring uses XML, and Butterfly DI Container uses a DSL.
The Opportunities of a Dependency Injection Domain Specific Language
When designing Butterfly DI Container I did initially start with an XML configuration format, but quickly abandoned it for a DSL. The XML format, though a lot more concise than Spring's config format, was still to verbose, and inflexible.
By designing a DSL tailored to dependency injection, and more specifically, the goals I had for Butterfly DI Container, it was possible to arrive at a more concise, more intuitive configuration language, than XML could ever be. I had complete freedom in designing the language. No Java or XML syntax to clutter the language, unless I wanted to.
The Butterfly DSL (called Butterfly Container Script - BCS) is designed with the following goals in mind:
- The DSL should be as concise as possible.
- The DSL should be as flexible as possible.
- Wiring components should look as much like Java as possible.
- Configuring applications should look as much like property files as possible.
- Localizing texts and components should look as much like property files as possible.
- It should be easy to switch into Java whenever necessary.
In the following sections I will provide examples of how you can use Butterfly Container Script for
- Wiring of Components
- Application Configuration
- Localization of Components
- Externalization of DDL and SQL
... and all without ever leaving the script language, nor having it bend out of shape trying to achieve this. The last 3 are opportunities missed by a Java code / annotation based injection mechanism.
Using a DI DSL for Wiring Up Components
Using Butterfly Container Script wire up components is pretty straightforward. Here is a simple example:
myBean = * com.jenkov.MyBean(); myBigBean1 = * com.jenkov.MyBigBean(myBean); myBigBean2 = * myBigBean1.setName("Big Bean2");
The first line defines a bean factory called "myBean". The * means that a new instance is created for every call to this factory. A 1 would have meant a singleton. The class name and constructor is written right after the *. This looks a bit like a cross between a property file and regular Java code. It is pretty easy to see what Java code is executed as configuration of objects returned by these factories.
The second line defines a bean factory called "myBigBean1" which gets an instance of the object returned by the "myBean" factory injected into its constructor.
The third line defines a bean factory called "myBigBean2" which is defined as a call
to the "myBigBean1" factory. The
setName() method is then called on
the instance returned from the "myBigBean1" factory. This shows the flexibility of
the DSL. Factories can easily be called and thus extended by other factories.
You can call any constructor, and call any method on any publicly available class using Butterfly Container Script. It does not have any annoying limitations like only being able to call "setters" or "getters" (which Spring's XML format has).
Using a DI DSL for Application Configuration
Butterfly Container Script was also designed to be a viable mechanism for ordinary application configuration. Look at this example here:
dbDriver = "org.h2.Driver"; dbUrl = "jdbc:h2:tcp://localhost/mydatabase/mydatabase"; dbUser = "sa"; dbPassword = ""; dataSource = 1 com.jenkov.db.jdbc.SimpleDataSource( dbDriver, dbUrl, dbUser, dbPassword);
Look at the first 4 lines which are standard application configuration parameters. They look pretty close to a standard property file, dont' they? The only difference is the quotes ("") and the delimiting semicolon (;). But any half decent administrator should be able to learn that little difference.
Notice also how the * is ommited. When the "instantiation mode" is ommited the factory defaults to singleton, which is appropriate for configuration parameters.
Notice how, on line 5 the first 4 application configuration parameters are injected
directly into the
DataSource implementation. It doesn't look any
different than if ordinary objects were injected.
It is easy to separate application configuration parameters into their own script file, so whoever is going to configure the application isn't confused by all the component wirings.
Using a DI DSL for Internationalization (I18N)
A new feature available from version 2.9.9 of Butterfly DI Container is the ability to
localize components. In other words to inject a version of a component that fits a certain
language. This language is represented by a Java
Locale object which is
attached to the currently executing thread. This way each thread calling the container
may obtain different instances matching different
associated with a given thread may also change over time, for instance between HTTP requests.
Here is a simple example:
UK = 1 java.util.Locale.UK; DK = 1 java.util.Locale('da', 'dk'); locale = * com.myapp.MyThreadLocalLocale.getLocale(); numberFormatter = L <UK : java.text.NumberFormat.getInstance(UK), DK : java.text.NumberFormat.getInstance(DK) >; welcomeText = L < UK : "Welcome", DK : "Velkommen" >; myComponent = * com.myapp.MyWebComponent(welcomeText, numberFormatter);
First the relevant locales are defined (UK and DK).
Second the "locale"
factory is defined. This factory must be present for the localization features
of Butterfly DI Container to work. The "locale" factory is defined as a call
to a static method which returns whatever
Locale is associated with
the calling thread. This
Locale is attached to the calling thread
using a correponding static method call
You'll have to program this little bit yourself (the static methods).
Third two localized (L) factories are defined called "numberFormatter" and "welcomeText".
These are defined as Maps using the < and > notation. Each Map uses the
(UK or DK) as keys into the Map, and the corresponding component or text as value.
Finally a "myComponent" factory is defined. This is defined as a
instance which has an instance from the factory "welcomeText" and "numberFormatter" injected.
However, what object is returned by these factories depends on what
associated with the thread obtaining the "myComponent" instance.
The localization features of Butterfly Container Script are pretty powerful. They allow you to use the same script language and same configuration mechanism for yet another configuration purpose: Your texts and localized components.
Using a DI DSL to Externalize DDL and SQL
Using Butterfly Container Script it is also pretty easy to externalize both DDL and SQL. Here is a simple example:
readPersonSql = "select * from persons where id = ?"; insertPersonSql = "insert into persons(id, name) values(?,?)"; updatePersonSql = "update persons set name=? where id=?"; deletePersonSql = "delete from persons where id=?"; personDao = * com.myapp.dao.PersonDao( readPersonSql, insertPersonSql, updatePersonSql, deletePersonSql);
Notice how the SQL statements are defined just as simply as an application configuration parameter,
and easily injected into the
PersonDaoinstance in the "personDao" factory. You can also
easily split the SQL files out into their own file(s), so they are not confused with component wirings.
All in all the @Inject JSR leaves me feeling a bit sad. I am a bit disappointed with the lack of ambition in this JSR. I mean, if we are to standardize how DI is to be done, by all means, give us a complete DI framework in the JDK. Have the courage to envision a complete DI language built into Java, and supply a complete DI framework that works with this language. Don't just settle for 4 annotations, which only cover a small subset of the use cases of a dependency injection container. After all, the .NET camp had the guts to add LINQ to the C# language. Why can't Java have a DI language?