Butterfly Container Script
- Factory Definition Basics
- Methods Returning Void
- Parameter Value Conversion
- Parameter Casting
- Static Method Factories
- Field Factories
- Collections and Arrays
- Maps
- Instance Life Cycles Phases
- Local Product Injection
- Factory Product Injection
- Factory Injection
- Named Local Factories
- Extending Factory Definitions
- Factory Input Parameters
Jakob Jenkov |
When Butterfly Container was first developed in 2006, it used a simple XML format for configuration. In 2007 that XML format was abandoned for a much simpler, more flexible configuration script language. Along with this language came a slightly simpler internal design and several new features. Among these were features that were not found in other dependency injection (DI) containers. For instance:
- Factory Injection
- Factory Extension
- Factory Input Parameters
- Method Chaining on void Methods
The current configuration script language is called Butterfly Container Script. It is a Java-like language with a couple of simplifications. The fact that it is Java-like means that it is a lot more intuitive to Java developers than the various XML-formats or Java API's used by other DI containers.
The rest of this text will explain the features of the Butterfly Container Script.
Factory Definition Basics
A container configuration script consists of a set of factory definitions. A factory returns an instance of some class. Class instances are also called objects or beans (from JavaBeans).
Each factory definition states how a given instance is to be created and configured, when calling the container.instance() method.
Below is an example of a simple factory definition.
bean = * com.jenkov.SomeObject();
This definition consists of the following parts:
- A factory name (the word "bean")
- An equal sign (the character '=')
- A factory mode (the character '*')
- A factory chain (the string "com.jenkov.someObject()")
- A semi colon (the character ';')
Each of these parts are described in more detail below.
Factory Name
The factory name is used when calling the container.instance() method. To instantiate the bean defined by the factory above, you would write this:
SomeObject someObject = (SomeObject) container.instance("bean");
Factory Mode
The factory mode defines how the object returned is created and managed by the container. The character '*' means that a new instance of the class com.jenkov.SomeObject is created for each instance("bean") call. Here are a list of all instantiation modes:
* = new instance |
1 = singleton |
1T = thread singleton |
1F = flyweight |
Thread singleton means one instance per thread calling the container.
Flyweight means one instance per different input parameter. The input parameters .hashcode() and .equals() methods determine if an input parameter has an instance associated with it already, or a new one needs to be created and associated with the input parameter for succeeding calls. In that respect a flyweight works a bit like a Map or cache, like "give me the instance for this key, or create a new one for this key and save it for later".
Factory Chain
The factory chain defines how an instance of the bean is obtained and configured. In the example above the no-arg constructor of the com.jenkov.SomeObject() class is called. The reason it is called a chain is that you can chain method calls to the contructor call. For instance:
bean = * com.jenkov.SomeObject().setValue("someValue");
As you can see the constructor call was chained with a call to the setValue() method. The object returned from the factory is the object returned by the last method call in the chain.
It is of course possible to use parameters in the constructor calls too, like this:
bean = * com.jenkov.SomeObject(100).setValue("someValue");
Every input parameter for a constructor or a method is itself a factory chain. Thus you can chain method calls or access fields on the parameters too. Here is an example:
bean = * com.jenkov.SomeObject(100).setValue("value".length());
Notice how the length() method is called on the "value" parameter.
Methods Returning Void
Methods that return void have been redefined in the Butterfly Container Script language. Instead of returning void they will return the object they are invoked on. If the setValue() method in the example above returns void, the factory will return the SomeObject instance the method was called on. This means that you can chain method calls even when the methods return void. For instance:
bean = * com.jenkov.SomeObject() .setValue("someValue") .setValue2("value2");
The factory defined by this script will return a SomeObject instance that has had the two methods setValue() and setValue2() invoked on it.
Parameter Value Conversion
The container is capable of converting values like 2.3, "http://www.jenkov.com" etc. passed as parameters to constructers and methods, to the most common Java primitives and objects. Here is a list of classes and primitives that parameters can be converted to, automatically:
byte Byte char Character short Short int Integer long Long float Float double Double BigInteger (java.math) BigDecimal (java.math) URL (java.net)
Parameter Casting
Sometimes a constructor or method call in a factory definition matches more than one constructor or method in the owning class. In these cases you will have to cast the parameter value to the desired type, just like you do in Java. Here is an example:
bean = * com.jenkov.SomeObject() .setValue ((int) "someValue") .setValue2((String) "value2" );
Here the value "someValue" is cast to an int, and the "value2" is cast to a String. You can cast to any type or class available on the classpath.
Static Method Factories
To call a static method instead of a constructor when creating an instance of some class, simply write as you would in Java:
bean = * com.jenkov.SomeObject.factoryMethod();
Notice that there are no parentheses after the class name. Like in Java this signals that the method to call is a static method. You can of course chain calls on static methods too.
Field Factories
You can return static fields from a factory too. You write it like in Java:
bean = * com.jenkov.SomeObject.FIELD;
You can also return fields from bean instances, like this:
bean = * com.jenkov.SomeObject().publicField;
Collections and Arrays
Butterfly Container Script provides a shortcut for defining collections and arrays for injection. Here is an example:
myList = * ["value1", factoryProduct, com.jenkov.TestProduct()];
A collection is marked by surrounding it with square brackets, and separating each element with a comma. The elements in the list can be any valid object, either constant, directly instantiated, or obtained from another factory.
You can also inject collections and arrays into objects like this:
myBean = * com.jenkov.MyBean() .setList( ["value1", factoryProduct, com.jenkov.TestProduct()] );
The container will convert the collection to either an array, List or Set depending the parameter type of the method or constructor the collecion or array is injected into. If no such information is available, like in the first example earlier, a List is created by default.
Note: The container uses java.util.ArrayList
and java.util.HashSet
as the List
and Set
implementations.
Maps
From version 2.5.9 Butterfly Container Script provides a shortcut to configuring Map instances. Here is an example:
myMap = * <"key1" : "value1", keyFactory2 : valueFactory2, com.jenkov.Key3() : com.jenkov.Value3()>;
A Map is marked by surrounding the key-value pairs with < and > . The key-value pairs are separated by commas, and the key is separated from the value with a colon.
Both keys and values can be any object valid in Butterfly Container Script. This means either a constant, an object obtained from another factory, or an object directly instantiated inside the Map definition.
You can also inject Map's into other beans, like this:
myBean = * com.jenkov.MyBean() .setMap( <"key1" : "value1", keyFactory2 : valueFactory2, com.jenkov.Key3() : com.jenkov.Value3()>);
Instance Life Cycles Phases
An instance created by a Butterfly Container passes through certain phases during its lifetime from creation to garbage collection. Below is a list of the phases currently supported:
- create
- config
- dispose
The create phase is the first phase a bean goes through in its life cycle. After creation the bean passes through the config phase, before being returned by the factory. Beans that are cached by the container, for instance singletons, only pass through the config phase once. When the container is shut down all cached beans pass through the dispose phase.
What happens in the create phase is defined by the bean definitions as we have seen them until now. Here is an example:
bean = * com.jenkov.SomeObject().setValue ("value");
When the factory chain is executed the create phase is over. The object returned by the last method call in the create phase is the object returned by the factory, and thus the object passed on to the rest of the phases.
Phase definitions for what is to happen in subsequent phases are appended to the create phase in the following format:
phaseName { factoryChains }
Below is an example of a config phase definition appended to the previous bean definition phase:
bean1 = * com.jenkov.SomeObject().setValue ("value"); config{ $bean1.setValue2("value2"); }
The $bean1 reference is a reference to the bean created in the create phase. The instance can be referenced using a $ and its factory name. Because the bean is called bean1 it can be referenced using $bean1. Referencing beans during phases is explained in more detail in Named Local Products.
You can have more than one factory chain per phase, except in the create phase. Here is an example:
bean1 = * com.jenkov.SomeObject().setValue ("value"); config{ $bean1.setValue2("value2"); com.jenkov.BeanCounter.increment(); }
The Create Phase
The create phase is defined in the first factory chain of the bean definition, and can only consist of a single factory chain. Here is an example:
bean1 = * com.jenkov.SomeObject().setValue ("value");
In this example the create phase consists of the factory chain
com.jenkov.SomeObject().setValue ("value");
First an instance of SomeObject is created and then the setValue() method is called on it. Finally the SomeObject instance is returned.
As you might have spotted there is an overlap between the create phase and the config phase, as it is possible to call methods on the created instance already in the create phase. This is made possible on purpose for two reasons.
- In 80-90% of the bean definitions all you need is to create an instance
of some class, and call a setter or two on it. Being able to chain
method calls directly on the created instance makes it possible
to fit the factory definition onto a single line. This makes the
factory definitions easier to read, and take up less space. This is also
the reason the create phase has no "create { }" around it.
Readability.
- Sometimes you actually need to return the product of a method call or a field, and not the first object instantiated or referenced.
The Config Phase
It is not always possible to complete all configuration necessary for a bean using chained method calls. Therefore factory definitions has an additional configure phase that can be used for this purpose. Below is an example that register the newly created instance with a static method in the MyClass class.
bean1 = * com.jenkov.SomeObject().setValue ("value"); config{ com.jenkov.MyClass.register($bean1); }
The config phase is executed right after the create phase. The config phase is handy if you need to run more than one factory chain when creating a bean. You can write as many factory chains as you need in the config phase.
The Dispose Phase
The dispose phase is executed when the container is shut down. Like the config phase you can access any cached beans and call methods on them. Here is an example:
bean1 = 1 com.jenkov.SomeObject().setValue ("value"); dispose{ $bean1.getConnection().close(); }
Though Butterfly Container won't complain about it, it does not make sense to add a dispose phase to a factory definition for instances that are not cached. The container has no references to uncached instances. If it did, it would seriously hurt garbage collection.
If a factory caches more than one instance, for instance in a flyweight, all instances will have the dispose chains executed against them. Each bean will go through all dispose chains before the next bean is subjected to any of them.
Local Product Injection
You can inject instances of classes created locally in a factory definition. Here is an example:
bean2 = * com.jenkov.OtherObject(com.jenkov.SomeObject());
Notice how the injected bean does not have a factory mode assigned to it. By default a new instance is created as everytime, since it is a local, unnamed, factory.
You can of course chain method calls on the local products too, like with any other parameter. Here is an example:
bean2 = * com.jenkov.OtherObject( com.jenkov.SomeObject().toString());
Factory Product Injection
It is of course possible to inject the output from one factory into another. When doing so, just refer to the name of that factory. Here is an example:
bean1 = * com.jenkov.SomeObject("initial value"); bean2 = * com.jenkov.OtherObject(bean1);
Notice how the bean1 is injected into OtherObject's constructor. Since bean1's factory creates a new instance for each call to it, a new instance is injected into every instance created of bean2.
You can of course chain method calls on the injected instance, like this:
bean1 = * com.jenkov.SomeObject("initial value"); bean2 = * com.jenkov.OtherObject(bean1.setNewValue("new Value"));
Notice how the setNewValue() method is called on the instance obtained from the bean1 factory, before it is injected into OtherObject's constructor.
Factory Injection
It is possible to inject a factory into a bean instead of the factory product. That way the bean can call the factory to obtain instances from it at runtime. To inject a factory instead of its product prefix the factory name with the # character, like this:
bean1 = * com.jenkov.SomeObject("initial value"); bean2 = * com.jenkov.OtherObject(#bean1);
The method or constructor having the factory injected must take an instance of the interface
com.jenkov.container.itf.factory.IFactory
Note: You can chain method calls on factories too. The definition
bean2 = * com.jenkov.OtherObject(#bean1.instance());
is equivalent to
bean2 = * com.jenkov.OtherObject(bean1);
Named Local Factories
Sometimes you need to configure an injected local product further, before returning the created bean. You can do so by naming the local product. Named local products are usually used to initialize cyclic references between a factory product and one of the injected products. Here is an example:
bean2 = * com.jenkov.OtherObject( someObject = * com.jenkov.SomeObject()); config { $someObject.setBean($bean2); }
This example injects an instance of SomeObject into the constructor of OtherObject. Notice that the injected local factory product, is named "someObject", and defined as a new instance for every call. It is then accessed from the config phase using its name, $someObject, where the created bean2 instance is injected into the local someObject instance.
The someObject local factory could have been defined as a singleton like this:
bean2 = * com.jenkov.OtherObject( someObject = 1 com.jenkov.SomeObject()); config { $someObject.setFurtherValue("value"); }
Named local products share the life cycle phases of their enclosing factory. Since the bean2 factory is not a singleton, the config phase is executed for every instance created. Thus, the setFurtherValue() method of the someObject singleton is called for each instance of bean2 created, even if the method call is redundant. If you needed the someObject to have its own life cycle phases, it should be defined as a global named factory instead, like bean2 is.
Extending Factory Definitions
It is possible to extend an existing factory definition. Here is an example:
bean1 = * com.jenkov.SomeObject("initial value"); bean2 = bean1.setValue("other value");
Notice how the bean2 definition obtains an instance from the bean1 factory, and calls the setValue() method on it before returning it.
Extending bean definitions is an easy way to avoid almost redundant bean definitions.
Factory Input Parameters
A factory definition can take input parameters. Input parameters can be given to the container.instance() method, like this:
container.instance("bean", param0, param1, param2, ...);
Input parameters are referenced by a $ plus the index of the input parameter to use. The input parameter indexes start from 0, just like in arrays. Here is an example:
bean = com.jenkov.SomeObject($0)
You can instantiate this bean from the container like this:
container.instance("bean", "value0");
You can also provide the input parameters from within another bean definition, like this:
bean1 = com.jenkov.SomeObject($0) bean2 = bean1("value0");
As you can see input parameters provide another easy way to customize and reuse factory definitions, just like extending factories does. You can of course chain the parameterized factory calls too, like this:
bean1 = com.jenkov.SomeObject($0) bean2 = bean1("value0").setSomeOtherValue("other");
The container cannot determine the type of input parameters until runtime, when the container.instance() method is called. Therefore it is only possible to call methods that exists in java.lang.Object on input parameters. This only counts for input parameter references ($0, $1 etc.). You can still chain method calls all you want before the parameter is passed to the receiving factory. Therefore,
bean = * com.jenkov.MyBean($0.getName()); bean2 = bean(com.jenkov.ParameterWithName());
is not valid, but this is:
bean = * com.jenkov.MyBean($0); bean2 = bean(com.jenkov.ParameterWithName().getName());
The input parameter feature adds some very exciting possibilities to Butterfly Container. Using parameterized factories you can create script files that are way more reusable than traditional dependency injection configuration XML scripts. You can provide ready-to-use container scripts for API's like the rest of the Butterfly Components, Eclipse SWT / JFace, Swing, Hibernate, Cayenne etc. Such scripts would greatly speed up the time it takes to get started using the API with Butterfly Container.
Tweet | |
Jakob Jenkov |