Rick

Rick
Rick

Sunday, February 16, 2014

Boon DI, because what Java needs is another DI framework

Boon Home | Boon Source | If you are new to boon, you might want to start here. Simple opinionated Java for the novice to expert level Java Programmer. Low Ceremony. High Productivity. A real boon to Java to developers!

Java Boon - Dependency Injection Boon

Java Boon is not meant to be a replacement for CDI, Weld, Java EE, Spring, Dagger or Guice. But you may find the Java Boon DI is easy to use and very lightweight and it is very easy to integrate other frameworks and Java Boon DI.
Java Boon DI has no class loader magic or runtime byte code manipulate or recompilation step. It is just a library. It works with Tomcat, JBoss, Netty, Swing apps, Vert.x, etc. Just drop it into your project and it works. No magic sauce. No class path scanning either.
Boon DI allows you to create objects in a very Scala, Guice, Python style instead of having 20+ constructors or factories. It also allows you to inject JSON configured objects which can be loaded based on namespace and environment config rules. Boon DI is less than other DIs for some things, but more than other DIs for some other important concepts.
Now you may want or need those things from Spring or Java EE but they are counter to what Boon is all about so you can use Boon for a good chunk of your DI needs and Spring, CDI, Java EE and Guice for the rest.
Boon DI was designed to be compatible with Spring, CDI (Java EE), Dagger and Guice.

Boon DI

I always planned on having a DI container back from the days when I wrote Crank I realized that it is sort of an essential piece of a framework. The goal is something lightweight that is easy to integrate with others. Less is more.
Boon DI has some unique characteristics in that it works really well with JSON. :)
I was not planning on implementing a DI lib until later this year, but I tried to use Dagger (Guice fork) on a project with Vert.x and after four hours of struggling, I broke down and wrote a simple DI framework that just supported @Inject, then one night I added @PostContruct, @Named, @Autowire and a few other things.
Then I gave it the ability to inject JSON serialized objects and a few other things, and here we are. Boon tends to look more like Guice, Dagger and CDI and less like Spring. To me Spring is a bit long in the tooth and Bob Lee and company were on to something. Gavin King jumped the shark with CDI. It took me a while to realize this because I am a big Spring fan, but Bob Lee was right. I like CDI but not as much as Guice and Dagger. I digress. Let's continue. I learned Spring, then CDI then Guice. I prefer Guice/Dagger, CDI and the Spring.
I could write a 100 page guide on Boon DI. I wish I could document as fast as I can code, but to give you a brief flavor for what it can do, look at my bastardized version of the Dagger/Guice coffee app, which has become more of a breakfast app because I added bacon! Everything is better with bacon. Then after this sample, I will cover some things that Boon DI has that the rest do not. Use Boon for most, and then use Java EE/Spring/Guice for the pieces that Boon DI does not have.
public class CoffeeApp implements Runnable {
    @Inject
    CoffeeMaker coffeeMaker;

    @Inject
    Coffee coffee;

    @Inject
    Sugar sugar;

    @Inject
    Bacon bacon;

    @Inject
    @Named( "brown" )
    Bacon brownBacon;

    @Inject
    @Named( "french" )
    Food frenchFood;

    @Inject
    @Named( "american" )
    Food americanFood;

    @Inject
    @Named( "new york" )
    Food newYorkFood;


    @Inject
    @Named( "rick's habit" )
    Food rickDrinks;

    @In( "rick's habit")
    Food rickDrinks2;

    @Inject
    @Named( "rick's habit" )
    Coffee rickCoffee;


    @Inject
    @Named( "black" )
    Coffee blackCoffee;



    @Inject
    @Named( "blue" )
    Supplier<Bacon> blueBaconSupplier;


    //Todo this works but I need a real unit test.
    @Inject
    @Named( "this is not found" )
    @Required
    Coffee notFound;

    @In("more stuff not found")
    Coffee notFound2;

    boolean started = false;

    @PostConstruct
    void init() {
        started = true;
    }


    @Override
    public void run() {
        coffeeMaker.brew();
    }

    @Test
    public void test() {
        CoffeeApp.main();
    }

    static Sugar staticSugar = new Sugar();//singleton ish


    static Bacon prototypeBacon = new Bacon();//prototype
    ...

Boon supports 1. @PostConstruct 2. @Inject 3. @Named 4. @In (old school Seam) 5. @Autowired (from Spring) 6. @Required (from Spring) 7. @Qualifier (from Spring)
Most of these are standard annotations or de factor standard or in the case of @In just something that I like. Boon does not care about the package name, just the name of the annotation so although Boon defines @PostConstruct, @Inject, @Autowire, and @Required, it will work with the ones from Guice, Spring, Weld, or you can define your own with the same name and Boon will work with them. I don't believe is compile time dependencies for annotations. This allows you to mix and match frameworks and you can also just use Boon for one off projects or unit tests and it will happily work with your existing injection annotations. Boon does not support strongly type qualifier CDI injection. It supports the Spring notation of qualifier with Named and Qualifier. The @In in Boon is like combining an @Required, @Qualifer, and @Autowire in one annotation (or a @Inject/@Named).
package org.boon.di;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author Rick Hightower
 *
 * Same effect at @Inject @Required if no argument passed.
 * Same effect as @Inject if @In(required=false) is passed.
 *
 * Same effect as @Inject @Named("bar") @Required if @In(value="bar" )
 */
@Target( {ElementType.METHOD, ElementType.FIELD, ElementType.TYPE, ElementType.PARAMETER} )
@Retention( RetentionPolicy.RUNTIME )
public @interface In {
    boolean required() default true;

    String value() default "";


}
package org.boon.di;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


/**
 * @author Rick Hightower
 */
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
}
package org.boon.di;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author Rick Hightower
 */
@Target( {ElementType.METHOD, ElementType.FIELD, ElementType.TYPE} )
@Retention( RetentionPolicy.RUNTIME )
public @interface Named {
    String value() default "";
}

package org.boon.di;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author Rick Hightower
 */
@Target( {ElementType.METHOD} )
@Retention( RetentionPolicy.RUNTIME )
public @interface PostConstruct {
}

package org.boon.di;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author Rick Hightower
 */
@Target( {ElementType.METHOD, ElementType.FIELD, ElementType.TYPE, ElementType.PARAMETER} )
@Retention( RetentionPolicy.RUNTIME )
public @interface Required {
}

Boon does not support scopes. It instead supports modules like Guice and Provider JDK 8 style interfaces which essentially take the place of scopes with out all of the auto proxy, byte code generation, business. I implemented scopes with Spring for JSF before it was in the Spring project. I understand how scopes work but I also owned a training and consulting business and I found few people really groked scopes and they ended up being a source of debugging hell for my clients. Sometimes less is more. If you want scopes, see CDI and Spring they have them. If/when Boon add interception, I might add some simplified notion of scopes or not.

    @Inject
    @Named( "blue" )
    Supplier<Bacon> blueBaconSupplier;
If Boon DI ever did support scopes (which it will not), then it would just use plain reflection interception which believe it or not is pretty damn fast in JDK 1.7 and even faster in JDK 1.8.
Instead of scopes, you have a Supplier which is a JDK 1.8 concept.
package org.boon.core;

public interface Supplier<T> {

    /**
     * Returns an object.
     *
     * @return an object
     */
    T get();
}
In general it works pretty similar to most DI frameworks so not much to see, it just works. Let's define a bunch of classes and interface and then show the CoffeeApp consuming them all. Later I will write up a step by step tutorial like I did for Spring and CDI. :)
Let's say that I have these classes:
package org.boon.di;

/**
 * Created by Richard on 2/3/14.
 */
@Named( "foo" )
public class BaseObject {
}
package org.boon.di;

/**
 * Created by Richard on 2/3/14.
 */
public interface Food {
}
Bacon is food!
package org.boon.di;

public class Bacon extends FoodImpl implements Food {
    boolean crispy;
    String tag;
}

I will inject this Cheese in later.
package org.boon.di;

/**
 * Created by Richard on 2/3/14.
 */
@Named("french")
public class Cheese extends FoodImpl implements Food {
}

package org.boon.di;

/**
 * Created by Richard on 2/3/14.
 */
public class Coffee {
}

package org.boon.di;

public class CoffeeMaker extends BaseObject {

    @Inject
    Heater heater; // Don't want to create a possibly costly heater until we need it.
    @Inject
    Pump pump;

    public void brew() {
        heater.on();
        pump.pump();
        System.out.println( " [_]P coffee! [_]P " );
        heater.off();
    }
}

Boon DI support Guice/Dagger style modules but with no annotations at all so this would work like a Guice/Dagger style module. (If you follow the Dagger tutorial you will be surprised how close Boon DI is minus the need for some compile time gak.)
package org.boon.di;

public class DripCoffeeModule {

    ElectricHeater providesHeater() {
        return new ElectricHeater();
    }


    @Named( "black" )
    Coffee providesBlackCoffee() {
        return new Coffee();
    }
}
Here is a heater interface.
package org.boon.di;

public interface Heater {
    void on();

    void off();

    boolean isHot();
}

Here is a named heater.
package org.boon.di;



@Named
public class ElectricHeater extends BaseObject implements Heater {
    boolean heating;

    @Override
    public void on() {
        System.out.println( "~ ~ ~ heating ~ ~ ~" );
        this.heating = true;
    }

    @Override
    public void off() {
        this.heating = false;
    }

    @Override
    public boolean isHot() {
        return heating;
    }
}
Creating a pump on the fly.
package org.boon.di;

public class PumpModule {
    Pump providesPump() {
        return new Pump() {
            @Override
            public void pump() {

            }
        };
    }

}
French fries are American food.
package org.boon.di;

@Named("american")
public class FrenchFries extends FoodImpl implements Food {
}

Notice we use the @Named which can be the standard @Named or the one that ships with boon. :)
Hot dogs are sort of like food too.
package org.boon.di;

@Named("chicago")
public class Hotdogs extends FoodImpl implements Food {
}

Notice we use the @Named which can be the standard @Named or the one that ships with boon. :)
package org.boon.di;

interface Pump {
    void pump();
}
package org.boon.di;

/**
 * Created by Richard on 2/3/14.
 */
public class Sugar extends FoodImpl {
}
Is sugar a food? Not sure?
Ok here is the CoffeeApp that uses them all.
package org.boon.di;


import org.boon.core.Supplier;
import org.junit.Test;

import static org.boon.Exceptions.die;

public class CoffeeApp implements Runnable {
    @Inject
    CoffeeMaker coffeeMaker;
    @Inject
    Coffee coffee;
    @Inject
    Sugar sugar;
    @Inject
    Bacon bacon;
    @Inject
    @Named( "brown" )
    Bacon brownBacon;
    @Inject
    @Named( "french" )
    Food frenchFood;
    @Inject
    @Named( "american" )
    Food americanFood;

    @Inject
    @Named( "new york" )
    Food newYorkFood;


    @Inject
    @Named( "rick's habit" )
    Food rickDrinks;

    @In( "rick's habit")
    Food rickDrinks2;

    @Inject
    @Named( "rick's habit" )
    Coffee rickCoffee;


    @Inject
    @Named( "black" )
    Coffee blackCoffee;



    @Inject
    @Named( "blue" )
    Supplier<Bacon> blueBaconSupplier;


    //Todo this works but I need a real unit test.
//    @Inject
//    @Named( "this is not found" )
//    @Required
//    Coffee notFound;

//    @In("more stuff not found")
//    Coffee notFound2;

    boolean started = false;

    @PostConstruct
    void init() {
        started = true;
    }


    @Override
    public void run() {
        coffeeMaker.brew();
    }

    @Test
    public void test() {
        CoffeeApp.main();
    }

    static Sugar staticSugar = new Sugar();//singleton ish


    static Bacon prototypeBacon = new Bacon();//prototype

    static {
        prototypeBacon.crispy = true;
    }

    public static void main( String... args ) {
        Module m1 = ContextFactory.classes( CoffeeApp.class, CoffeeMaker.class, FoodImpl.class );
        Module m2 = ContextFactory.module( new DripCoffeeModule() );
        Module m3 = ContextFactory.module( new PumpModule() );
        Module m4 = ContextFactory.suppliers( ProviderInfo.providerOf( Coffee.class, new Supplier<Coffee>() {
            @Override
            public Coffee get() {
                return new Coffee();
            }
        } ) );

        Module m5 = ContextFactory.objects( staticSugar );
        Module m6 = ContextFactory.prototypes( prototypeBacon );

        Module m7 = ContextFactory.suppliers(
                ProviderInfo.providerOf( Bacon.class ),
                ProviderInfo.providerOf( "orange", Bacon.class ),
                ProviderInfo.providerOf( "red", new Bacon() ),
                ProviderInfo.providerOf( "brown", Bacon.class, new Supplier<Bacon>() {
                    @Override
                    public Bacon get() {
                        Bacon bacon = new Bacon();
                        bacon.tag = "m7";
                        return bacon;
                    }
                } ) );


        Module m0 = ContextFactory.suppliers( ProviderInfo.providerOf( "blue", new Supplier<Bacon>() {
            @Override
            public Bacon get() {
                Bacon bacon = new Bacon();
                bacon.tag = "m7";
                return bacon;
            }
        } ) );


        Module m8 = ContextFactory.classes( Cheese.class );
        Module m9 = ContextFactory.objects( new FrenchFries() );

        Module m10 = ContextFactory.objects( ProviderInfo.providerOf( "new york", new Hotdogs() ) );


        Module m11 = ContextFactory.classes( ProviderInfo.providerOf( "rick's habit", Coffee.class ) );

        Context context = ContextFactory.context( m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m0 );
        Heater heater = context.get( Heater.class );
        boolean ok = heater instanceof ElectricHeater || die();
        CoffeeApp coffeeApp = context.get( CoffeeApp.class );
        coffeeApp.run();

        validateApp( coffeeApp );


        context = ContextFactory.context( m0, m8, m9, m10, m11, m1, m2, m3, m4, m5, m6, m7 );
        coffeeApp = context.get( CoffeeApp.class );
        validateApp( coffeeApp );

        Bacon blueBacon = context.get( Bacon.class, "blue" );
        ok = blueBacon != null || die();


        ok = context.has( Coffee.class ) || die();

        ok = context.has( "black" ) || die();

        ok = context.get( "black" ) != null || die();
        ok = context.get( "electricHeater" ) != null || die();

        ok = context.get( "foodImpl" ) != null || die();





        context.remove( m0 );
        context.add( m11 );
        context.addFirst( m0 );
        coffeeApp = context.get( CoffeeApp.class );
        validateApp( coffeeApp );

    }

    private static void validateApp( CoffeeApp coffeeApp ) {
        boolean ok;


        ok = coffeeApp.started || die();

        ok = coffeeApp.coffee != null || die();

        ok = coffeeApp.sugar == staticSugar || die();


        ok = coffeeApp.bacon != prototypeBacon || die();


        ok = coffeeApp.bacon.crispy || die();


        ok = coffeeApp.brownBacon != null || die();


        ok = coffeeApp.frenchFood != null || die();

        ok = coffeeApp.frenchFood instanceof Cheese || die( coffeeApp.frenchFood.toString() );


        ok = coffeeApp.americanFood != null || die();

        ok = coffeeApp.americanFood instanceof FrenchFries || die( coffeeApp.americanFood.toString() );


        ok = coffeeApp.newYorkFood != null || die();

        ok = coffeeApp.newYorkFood instanceof Hotdogs || die( coffeeApp.newYorkFood.toString() );


        ok = coffeeApp.rickDrinks == null || die();

        ok = !( coffeeApp.rickDrinks instanceof Coffee ) || die( coffeeApp.rickDrinks.toString() );



        ok = coffeeApp.rickDrinks2 == null || die();

        ok = !( coffeeApp.rickDrinks2 instanceof Coffee ) || die( coffeeApp.rickDrinks.toString() );

        ok = coffeeApp.rickCoffee != null || die();

        ok = ( coffeeApp.rickCoffee instanceof Coffee ) || die( coffeeApp.rickCoffee.toString() );


        ok = coffeeApp.blackCoffee != null || die();

        ok = ( coffeeApp.blackCoffee instanceof Coffee ) || die( coffeeApp.blackCoffee.toString() );



        ok = coffeeApp.blueBaconSupplier != null || die();

        ok = ( coffeeApp.blueBaconSupplier.get() instanceof Bacon ) || die( coffeeApp.blueBaconSupplier.get().toString() );
    }


}

As you can tell that this is not much of a tutorial as Boon DI pretty much works like you expect it to, i.e., like Guice, Dagger, CDI or Spring would do, but without all of the class loader, byte code manipulation, etc. Boon is a scaled down DI library. It is not a container, but you could build a container with it.
Instead of spending a lot of time explaining Boon DI, let's talk about what it does that the others do not do.

Boon Creators (Scala, Groovy, Python style object construction in Java)

On top of Boon DI and Boon JSON support Boon has Boon creators, which allow you to create arbitrary objects and utilize injection with them without a DI container. This allows you to use injection fields @Inject and @PostConstruct to avoid the need to have 20 constructors or complicated factories to create Java objects. This provides Groovy, Python and Scala style construction with plain old Java.
import static org.boon.di.Creator.create;
import static org.boon.di.Creator.newOf;

Let's define some classes to work with
    @Named
    public static class SomeClass {

        String someField = "someclass";

    }


    public static class Bar {

        String name = "bar";

    }

    public static class Foo {

        /* Works with standard @Inject or boon @Inject or Guice/Dagger Inject or Spring @Autowire*/
        @In("bar")
        Bar bar;

        /*Works with Boon @Id, @Named also can use @Named form CDI
        or Guice or use @Qualifer from Spring works with all
         */
        @Inject @Named("someClass")
        SomeClass object;


        @Inject
        SomeClass object2;

    }


    public static class Foo2 {

        /* Works with standard @Inject or boon @Inject or Guice/Dagger Inject or Spring @Autowire*/
        @Inject
        Bar bar;

        /*Works with Boon @Id, @Named also can use @Named form CDI
        or Guice or use @Qualifer from Spring works with all
         */
        @Inject @Named("someClass")
        SomeClass object;


        @Inject
        SomeClass object2;

    }


    Class<Foo> fooType = Foo.class;

    Class<Foo2> fooType2 = Foo2.class;

Now we can have a Map that acts sort of like an application context in Spring and the injection just works (Remember that Boon has a literal style map construction concept so this would put the LOC close to Groovy, Python and Scala for object construction and we are doing this in plain old Java. )
        Bar bar = new Bar();
        bar.name = "baz";
        Map myMap = map( "bar", bar );

        Foo foo = create( fooType, myMap );
Or more tersely
        Foo foo = create( fooType, myMap, map("bar", new Bar("baz") );

So now you have named constructor args. :)
This is actually quite a common case so you can drop the map with newOf
        Foo foo = newOf( fooType, "bar", bar );

You also inject maps so this is sort of like injecting raw config.
        Foo foo = newOf( fooType,
                "bar",
                map("class",
                "org.boon.di.CreatorTest$Bar", "name", "barFromMap") );
You can also inject JSON. :)

import static org.boon.json.JsonCreator.*;

        Foo foo = createFromJsonMap( fooType, 
                "{'bar':{'class':'org.boon.di.CreatorTest$Bar', 
                   'name': 'barFromJson'}}" );

You can also load the JSON from any resource location (file, class path, ftp, http, file system, etc.)
        Foo foo = createFromJsonMapResource( fooType, "classpath://config/config.json" );

You can also create the object form not one JSON file but from every JSON file that is in a package location of the class path or a file director or…
        Foo foo = createFromJsonMapResource( fooType, "classpath://config/" );
You can also create a Boon DI context from many resource locations as follows:
        Context context = ContextConfig.JSON.createContext( "classpath://config/", "classpath://fooConfig/" );
The above would load all json files in those locations.
When you create objects, you do not have to specify an instance, you can just specify the type.
        Foo foo = create( fooType, map( "bar", Bar.class ) );
You can load config files that have namespaces match rules (like prod, dev, qa, etc.).
        //Load DI context for dev 
        context = ContextConfig.JSON.createContext( "dev", false, "classpath://config_files/" );

        //Load DI context for prod    
        context = ContextConfig.JSON.createContext( "prod", false, "classpath://config_files/" );

        //Load DI context for qa
        context = ContextConfig.JSON.createContext( "qa", false, "classpath://config_files/" );
Config files can include other config files.
        context = ContextConfig.JSON.createContext( "qa", false, "classpath://config_files/" );
JSON config file in /config_files
{

    /* QA namespace. */
    "META": {
        namespace:qa,
        include:[classpath://include/],
        version : 1.0
    },

    "foo": {
        "class":"org.boon.di.CreatorTest$Foo",
        "name": "QA Foo"
    }

}
json config file in /include
{

    "bar":
    {
        "class":"org.boon.di.CreatorTest$Bar",
        "name": "QA Bar"
    }
}
You can load config files if their META headers meet certain rule guidelines
        context = ContextConfigReader.config().namespace( "qa" ).resource( "classpath://config_files/" )
                .rule( gte( "version", 1.0 ) ).read();
The above loads qa DI config under classpath://config_files if version is greater than 1.0. :)
The load rules can be complex.
        context = config().namespace( "qa" ).resource( "classpath://config_files/" )
                .rule(
                    and(
                            gte ( "version", 1.0 ),
                            lt  ( "version", 3.3 ),
                            eq  ( "developer", "John Fryar")
                      )
                ).read();
This allows you to configure many servers with the same config, and then just load certain configs if they meet a certain criteria (WestCoast, preferred customer service, etc.).

Thoughts

Thoughts? Write me at richard high tower AT g mail dot c-o-m (Rick Hightower).

Further Reading:

If you are new to boon start here:

Why Boon?

Easily read in files into lines or a giant string with one method call. Works with files, URLs, class-path, etc. Boon IO support will surprise you how easy it is. Boon has Slice notation for dealing with Strings, Lists, primitive arrays, Tree Maps, etc. If you are from Groovy land, Ruby land, Python land, or whatever land, and you have to use Java then Boon might give you some relief from API bloat. If you are like me, and you like to use Java, then Boon is for you too. Boon lets Java be Java, but adds the missing productive APIs from Python, Ruby, and Groovy. Boon may not be Ruby or Groovy, but its a real Boon to Java development.

Core Boon Philosophy

Core Boon will never have any dependencies. It will always be able to run as a single jar. This is not just NIH, but it is partly. My view of what Java needs is more inline with what Python, Ruby and Groovy provide. Boon is an addition on top of the JVM to make up the difference between the harder to use APIs that come with Java and the types of utilities that are built into Ruby, Python, PHP, Groovy etc. Boon is a Java centric view of those libs. The vision of Boon and the current implementation is really far apart.

Contact Info

No comments:

Post a Comment

Kafka and Cassandra support, training for AWS EC2 Cassandra 3.0 Training