This appendix is about the Butterfly DI DSL called Butterfly Container Script (BCS). We include this appendix to illustrate the last of the four configuration mechanisms mentioned in chapter 2.
What we show here is far from a complete listing of the capabilities of Butterfly Container. Rather we show a few of Butterfly Container's solutions; these problems are also mentioned elsewhere in this book:
Contextual injection via input parameters
Reinjection via factory injection
This appendix also looks at a few features made possible by a DSL.
Butterfly Container is an open source project and is part of a growing collection of components called Butterfly Components. Should you be interested in learning more, you can find additional information at http://butterfly.jenkov.com
.
For reasons too involved to discuss here, Butterfly Container was designed to use a DSL tailored to dependency injection for its configuration. This mechanism is somewhat similar to the XML files of Spring, except BCS is not an XML format.
Butterfly Container Script was designed to look like a cross between a standard property file (name = value) and Java code. This was done to make it easier to read, write, and learn since most Java developers are familiar with both Java and property files. This language design also makes it feasible to use BCS files for application configuration (explained later).
A script file consists of one or more bean factories. It's easiest to explain the basics of BCS by using an example:
myBean1 = * com.myapp.MyBean();
This configuration consists of five parts:
The Factory ID identifies this Factory. This ID must be unique throughout the container instance. You supply this ID to the container when obtaining an instance or when referencing that factory from other factories.
The equals sign indicates the beginning of the product part of the factory definition.
The instantiation mode is also sometimes referred to as scope. However, not all bean scopes in Butterfly Container are achieved using the instantiation mode. Sometimes a scope is achieved by combining the instantiation mode with a special bean factory, for instance your own bean factory.
BCS currently includes the following instantiation modes:
*
—A new instance is created on every call to a Factory.
1
—A singleton is created once and returned on every call to a Factory.
1T
—One singleton per thread-calling factory is created once and returned on every call to a Factory.
1F
—Flyweight; one instance per provided input parameter is created once and returned on every call to a Factory with same input parameter.
The Factory chain defines what object is to be returned by the Factory. In our example that's an instance of the class com.myapp.Bean
. A Factory can create more than one object when it executes but return only one of them. We'll explain more about the Factory chain later.
The semicolon signals that the Factory chain is finished.
In the Factory chain of a Factory definition you can call constructors, call methods, and even define local bean factories not visible outside this factory definition. Here's a somewhat more complex example:
myBean1 = * com.myapp.MyBean1(); myBean2 = * com.myapp.MyBean2("A Text", myBean1) .setMoreText("more text");
This configuration consists of two Factories: myBean1
and myBean2
. The definition of myBean1
is pretty much the same as the first example. myBean2
shows a few new things. First, it contains two constructor parameters to the constructor of the class com.myapp.MyBean2
. The second constructor parameter references the myBean1
factory, meaning an instance obtained from this factory should be obtained and injected into the constructor.
The Factory chain contains the method call setMoreText("more text")
. This call further configures the MyBean2
instance before returning it. In fact, if the setMoreText()
method had returned an object, it would be this object that was returned by the myBean2
Factory and not the MyBean2
instance. Since the setMoreText()
method returns void
, it's interpreted as returning the instance the method was called on—the MyBean2
instance, in other words. That also means that you can chain method calls on methods returning void
, like this:
myBean2 = * com.myapp.MyBean2("A Text", myBean1) .setMoreText("more text") .setAValue(1) .setMoreOfSomething("more...");
As you can see, the factory now consists of a chain of constructor/method calls, hence the name Factory chain. The Factory chain can be as long as you need it to be.
This is one of the advantages of having a tailored DSL: the freedom to add features like method chaining on methods returning void
.
BCS allows contextual injection by enabling factories to receive input parameters.
Here's how that looks:
myBean1 = * com.myapp.MyBean($0).setValue($1); myBean2 = * myBean1(com.myapp.SomeBean(), "Value Text");
The dollar sign ($
) signals that an input parameter is to be injected. The number (0, 1
, and so on) specifies which input parameter to inject. As you can see, input parameters are not named or typed. This can lead to type errors in your Factory definitions if you aren't careful. This is one of the limitations of a custom DSL. It's only as advanced as you make it; you don't get Java's full type checking. In reality this is a big problem, as you might think. You can set up unit tests to check the validity of the factories, if you want to.
The myBean2
Factory calls the myBean1
Factory and provides two input parameters to it: a com.myapp.SomeBean
instance and the string "Value Text"
. Notice that each input parameter is itself a Factory chain, so you could also have provided input parameters to the SomeBean
instance and called methods on it, like this:
myBean1 = * com.myapp.MyBean($0).setValue($1); myBean2 = * myBean1(com.myapp.SomeBean("config", $0) .setTitle("The Title"), "Value Text");
The SomeBean
constructor is now given two parameters: the string "config"
and the input parameter $0
. This input parameter is not the same as the $0
of the myBean1
Factory. The $0
input parameter of the myBean1
Factory is the SomeBean
instance. The $0
input parameter of the myBean2
Factory is whatever you provide as first parameter to the myBean2
Factory.
You can also provide input parameters to a Factory when obtaining beans from the container from Java code, like this:
MyBean myBean = (MyBean) container.instance("myBean1", new SomeBean(), "Value Text");
If the method you're trying to inject parameters into is overloaded, it can be hard or even impossible for the container to tell which of the overloaded methods to call. It's therefore possible to specify the type of the injected parameter, like this:
myBean1 = * com.myapp.MyBean((long) $0).setValue((String) $1);
BCS makes it possible to inject the Factory of a product rather than the product itself. This is done by putting a # in front of the Factory name when referencing it, like this:
firstBean = * com.myapp.FirstBean($0).setValue($1); secondBean = * com.myapp.SecondBean().setFirstBeanFactory(#firstBean);
In the secondBean
Factory, the firstBean
Factory is injected into the method setFirstBeanFactory()
, by referencing the injected Factory as #firstBean
.
While you could declare the type of the Factory inside the SecondBean
instance of type com.jenkov.container.itf.factory.IGlobalFactory
, this would result in a hard reference in your code to a Butterfly Container class. You don't want that. Instead, Butterfly Container is capable of adapting the Factory to your own custom interface as long as the interface has an instance()
method. Here's an example of such an interface:
public interface FirstBeanFactory { public FirstBean instance(); }
In the SecondBean
class you can now create a field of the type FirstBeanFactory
, like this:
public class SecondBean { protected FirstBeanFactory firstBeanFactory = null; public void setFirstBeanFactory(FirstBeanFactory factory){ this.firstBeanFactory = factory; } public void doSomething(){ FirstBean firstBean = this.firstBeanFactory.instance(); } }
Notice how the use of the FirstBeanFactory
interface inside the SecondBean
is perfectly type-safe: no casts and no references to any Butterfly Container–specific classes. You can even specify typed parameters to the instance()
method. If you look at the Factory definition of firstBean
, you can see that it actually takes two parameters. You can add them to the instance()
method like this:
public interface FirstBeanFactory { public FirstBean instance(long constructorParam, String value); }
These parameters are typed as well, making it easy for maintenance developers to see what types are required by the firstBean
factory.
You could even call the instance()
method by the name of the Factory in the container to call, like so:
public interface FirstBeanFactory { public FirstBean firstBean(long constructorParam, String value); }
This can be used to add several Factory methods to the same interface, referencing different Factories in the container, for instance:
public interface BeanFactories { public long numberOfConnectionsInPool(); public String dbUrl(); public FirstBean firstBean(long constructorParam, String value); // etc... }
The container would determine at runtime which Factory to obtain the object from by matching the method name with a factory in the container. It doesn't matter which factory you inject. If your method is called instance()
, the injected factory is called. If your method is called something else, the Factory having the same name as the method is called.
While this is a powerful feature, you should use it with care. By using named Factory methods in your interface, you create a hard link between the method names and the Factory names. If the Factory names are later changed, the interface methods will stop working and you won't know why, unless you have a unit test determining that it works. You also have the slight disadvantage in that Java doesn't allow the same characters in method names as Butterfly Container allows in Factory names. For instance, if your Factory is named dao/projectDao
, you cannot name a method in Java dao/projectDao()
. You can still inject and call the Factory, however, using the method name instance()
. But then you can have only one method per interface. In most cases, this is sufficient, so it's not a big limitation.
3.149.239.100