Butterfly Container Script - Design Considerations

Jakob Jenkov
Last update: 2014-05-25

As configuration mechanism I chose a simple script language I decided to call Butterfly Container Script. I will only explain the details of the language relevant to this text. If you are interested you can read more about it here: Butterfly Documentation.

The Script Language

The language consists of simple factory definitions. Here is an example:

bean = * com.jenkov.MyObject();

This script defines a factory called "bean" which produces new instances (marked by the *) of the class com.jenkov.MyObject. This is equivalent to this Spring configuration:

<bean id="bean" class="com.jenkov.MyObject" singleton="false" />

Already here the Butterfly Container Script is more dense than the corresponding Spring XML configuration, though only a little. The difference becomes more evident when you configure a method to have a few objects injected. Here is an example:

bean = * com.jenkov.MyObject("value")
                .setSecondValue("value2")
                .setThirdValue("value3");    

Notice how it is possible to chain method calls even if the methods return void. This is a conscious design decision in the Butterfly Container Script, to make configurations shorter to write.

Here is the corresponding Spring XML configuration:

<bean id="bean" class="com.jenkov.MyObject" singleton="false">
    <constructor-arg type="java.lang.String" value="value"/>
    <property name="secondValue" value="value2"/>
    <property name="thirdValue" value="value2"/>
</bean>

Already here you can see how much noise the Spring XML configurations contain. The Butterfly Container Script is both shorter and looks a lot more like Java. From here on, Spring XML only gets more and more unreadable.

Full Freedom in Life Cycle Phases

And now a few things you can't even do with Spring's XML configuration, though I see absolutely no reason why not:

beanCounter = 1 com.jenkov.BeanCounter();

bean        = * com.jenkov.MyObject();
    config{ beanCounter.increment(); }

Now what is going on here? First, a "beanCounter" factory is defined as a singleton (marked by the 1). Second, a "bean" factory is defined, with a configuration phase attached. The configuration phase is executed after the object is instantiated. But lo and behold! The config phase doesn't call any methods on the MyObject instance the factory returns. Rather, it calls the increment() method of the "beanCounter" singleton!!

This little trick is totally impossible with Spring XML or Pico. You may not use this feature so often, but when you do need it, it comes in quite handy.

Input Parameters

And here is something else that isn't possible with neither Spring XML, Pico or Guice:

bean  = * com.jenkov.MyObject($0);

bean1 = * bean("value1");
bean2 = * bean("value2");
bean3 = * bean("value3").setSecondValue("blabla");

Now what is going on here? First a "bean" factory is defined as a new instance factory. The $0 marks an input parameter passed to the factory, either from other factories, or from outside the dependency injection container.

Second, three factories are defined as calls to the "bean" factory, with different input parameters. These input parameters will be injected into the MyObject constructor when the MyObject instance is created in the "bean" factory. Notice how the "bean3" factory even chains a call on the MyObject instance returned by the "bean" factory.

Using input parameters it is possible to create factories that are merely used as templates for other factories. Spring-lovers will claim that this is possible in Spring XML too, by letting one bean-definition extend another. While this gets you some of the way, you still cannot change what is injected into the constructor in the extended bean definition (unless I am totally mistaken).

Collections

Another area in which I had to make a decision was in the support for configuration of lists and maps. Here is now a List and a Map configuration would have looked without special support:

myList = * java.util.ArrayList();
    config{ $myList.add("value1");
            $myList.add("value2");
          }

myMap = * java.util.HashMap();
    config{ $myMap.put("key1", "value2");
            $myMap.put("key2", "value2");
          }

With the builtin array, List and Map support, here is how it looks now:

myList = * ["value1", "value2"];

myMap  = * <"key1" : "value1",
            "key2" : "value2">;

Much simpler, right? You can of course reference other factories from inside List and Map definitions. For arrays you use the List formatting. The parser will figure out whether to use an array or List depending on what parameter you inject it into. If not injected, it defaults to an ArrayList.

In contrast, look at all the nasty XML code needed in Spring:

<map>
    <entry>
        <key><value>key1</value></key>
        <value>value1</value>
    </entry>
    <entry>
        <key><value>key2</value></key>
        <value>value2</value>
    </entry>
</map>

Shift into Java When You Need to

Butterfly Container Script is designed to be shorter than the equivalent Java code. However, this brevity comes at a cost. Sometimes it is just easier to do more advanced object configuration from plain Java code. Butterfly Container Script is designed to allow you to do that.

Imagine you have a static method you would like to call from your Butterfly Container Script. Here is how you do it:

myBean = * com.jenkov.MyBean.staticFactoryMethod();

Now you can put whatever code into the static method staticFactoryMethod you need to run.

You can also use the static method as a function in your script. Imagine you have a static method that takes a date in string format, and returns it as a java.util.Date object. Here is an example of using that static method as function:

toDate = * com.jenkov.DateUtil.toDate($0);

timer  = * com.jenkov.Timer(toDate("2008-24-12"));

Notice how the timer factory uses the toDate factory as a function to convert the date in string format to a java.util.Date object for injection into the Timer constructor.

You can also use instance methods as functions, like this:

dateFormat = 1 java.text.SimpleDateFormat("yyyy-MM-dd");
toDate = * dateFormat.parse($0);

timer  = * com.jenkov.Timer(toDate("2008-24-12"));

This configuration creates a singleton factory called dateFormat of the type java.text.SimpleDateFormat. Then the configuration defines a toDate factory which calls the parse() method of the singleton SimpleDateFormat instance. The toDate function is used in the timer factory, exactly like the in the previous configuration example.

As you can see, it is easy to shift into Java when you need to, and still weave it into the container. You can even plug real non-reflection, non-annotation Java factories into the container, and reference them from inside your scripts. This is outside the scope of this article though. You can read how to do that in the Butterfly Documentation.

Summary

There are several other, minor features that Butterfly Container Script has, that Spring XML lacks, but explaining them all is outside the scope of this text. I just wanted to underline the most important design goals of this script language:

  1. Dense

  2. Java-like

  3. Very Flexible

  4. Very Extensible

Jakob Jenkov

Featured Videos

Java Generics

Java ForkJoinPool

P2P Networks Introduction



















Close TOC
All Tutorial Trails
All Trails
Table of contents (TOC) for this tutorial trail
Trail TOC
Table of contents (TOC) for this tutorial
Page TOC
Previous tutorial in this tutorial trail
Previous
Next tutorial in this tutorial trail
Next