Guice-Bean Extension Layer
Our goal is to create a general-purpose bean injection layer on top of the raw custom injection API provided by Guice. This layer will take care of registering the necessary MembersInjectors as well as scanning class hierarchies for potential bean properties. All the client needs to do is decide whether or not to supply a binding for a given property. The primary class in this layer is BeanListener:
public class BeanListener implements TypeListener { public BeanListener( final BeanBinder beanBinder ) { // ...etc... } }
This is a TypeListener implementation that you can register with Guice to provide bean injection for any type. It accepts a single argument of type BeanBinder, which the client is expected to implement:
public interface BeanBinder { <B> PropertyBinder bindBean( TypeLiteral<B> type, TypeEncounter<B> encounter ); }
The single bindBean method should supply a PropertyBinder based on the type of bean being injected. This means clients can customize or cache information on a per-type basis, or simply provide the same PropertyBinder implementation for all bean types. Returning null will disable bean injection for that type. The PropertyBinder interface does the same job as BeanBinder, but at the property level:
public interface PropertyBinder { <T> PropertyBinding bindProperty( BeanProperty<T> property ); }
This is the point where clients decide whether or not to bind a particular property. They can either return an implementation of PropertyBinding which should inject the property, or return null to ignore this property. This process continues until all the bean properties have been scanned - unless the client returns the LAST_BINDING constant from the PropertyBinder API, which will immediately stop the scanning for the current bean. LAST_BINDING is supposed to be used by binders who know they can't supply any more bindings, because then there's no point in continuing the scan. So what does a PropertyBinding look like?
public interface PropertyBinding { <B> void injectProperty( B bean ); }
Well, not much as you can see! It has a single method injectProperty which is the trigger for injecting a value into the appropriate property of the given bean. How this value is created and how it is injected into the bean is up to the client, but most clients will use the BeanProperty supplied in the original bindProperty method to do the actual injection:
public interface BeanProperty<T> { <A extends Annotation> A getAnnotation( Class<A> annotationType ); TypeLiteral<T> getType(); String getName(); <B> void set( B bean, T value ); }
Specifically the set method lets you inject a value into this property for a given bean instance. Notice how you can also query the generic type, name, as well as any annotations attached to the bean property. You can use any or all of this information to determine what value to inject into the property: for example by querying XML metadata, runtime annotations, maybe even pulling values out of a database.
Property names are normalized - a setter method called "setValue" has a property name of "value" |
The BeanProperty currently doesn't tell you whether it's backed by a field or setter method. This is because we don't need this information in our use-case and don't believe others will, but some sort of query method could be added in the future if necessary. |
Enough talk, let's actually try and use this extension layer in a small example!
We begin by creating an example Maven project:
mvn archetype:create "-DgroupId=example.bean.guice" "-DartifactId=guice-bean-example" cd guice-bean-example/
Remove the example App files, as we'll be adding our own files soon:
rm src/main/java/example/bean/guice/App.java rm src/test/java/example/bean/guice/AppTest.java
Now edit the POM, first we need to add a dependency to the guice-bean-inject library:
<dependencies> <!-- ... --> <dependency> <groupId>org.sonatype.spice.inject</groupId> <artifactId>guice-bean-inject</artifactId> <version>0.1.0</version> </dependency> <!-- ... --> </dependencies>
This will also pull in the guice-bean-reflect and guice-patches libraries |
We also need to tell Maven to compile at the Java5 level, because this is the minimum required by Guice:
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.5</source> <target>1.5</target> </configuration> </plugin> </plugins> </build>
We're now ready to add our example code, let's start with a very simple "do nothing" binder:
package example.bean.guice; import org.sonatype.guice.bean.inject.BeanBinder; import org.sonatype.guice.bean.inject.PropertyBinder; import com.google.inject.TypeLiteral; import com.google.inject.spi.TypeEncounter; public class SimpleBeanBinder implements BeanBinder { public <B> PropertyBinder bindBean( TypeLiteral<B> type, TypeEncounter<B> encounter ) { return null; } }
Well that's fairly useless! We should at least return some property binders which in turn return some property bindings, otherwise nothing will be injected. To save on typing we'll put our injection code in a couple of nested anonymous classes, but we could just as well have created named classes. Let's also use the bean property to do the actual injection (for the moment we will just inject null).
package example.bean.guice; import org.sonatype.guice.bean.inject.BeanBinder; import org.sonatype.guice.bean.inject.PropertyBinder; import org.sonatype.guice.bean.inject.PropertyBinding; import org.sonatype.guice.bean.reflect.BeanProperty; import com.google.inject.TypeLiteral; import com.google.inject.spi.TypeEncounter; public class SimpleBeanBinder implements BeanBinder { public <B> PropertyBinder bindBean( TypeLiteral<B> type, TypeEncounter<B> encounter ) { return new PropertyBinder() { public <T> PropertyBinding bindProperty( final BeanProperty<T> property ) { return new PropertyBinding() { public <C> void injectProperty( C bean ) { property.set( bean, null ); } }; } }; } }
We made the property parameter final so we could use it inside the anonymous class |
Next we need to provide actual values for our bean properties. We're free to get these values from anywhere we want, but to keep this example simple we'll just get them from the Guice injector. So how do we access the injector while we're still configuring bindings? Well the TypeEncounter has methods to look up the Provider for a given binding Key. But be careful - we can't use these providers during the property binding call because the injector is still being initialized. We can only use them once the injection phase starts, such as in the injectProperty method.
Our example bean binder takes the simple name of the bean class, appends the property name, and turns it into a JSR 330 binding annotation like @Named("PersonBean.name"). In practice you'll also want to include the package name to avoid collisions. The appropriate value provider is supplied by the TypeEncounter using a key based on the binding annotation and property type. We store the provider in a final variable so we can access it later on when we need to inject the property value into a bean.
package example.bean.guice; import javax.inject.Named; import javax.inject.Provider; import org.sonatype.guice.bean.inject.BeanBinder; import org.sonatype.guice.bean.inject.PropertyBinder; import org.sonatype.guice.bean.inject.PropertyBinding; import org.sonatype.guice.bean.reflect.BeanProperty; import com.google.inject.Key; import com.google.inject.TypeLiteral; import com.google.inject.spi.TypeEncounter; import com.google.inject.util.Jsr330; public class SimpleBeanBinder implements BeanBinder { public <B1> PropertyBinder bindBean( final TypeLiteral<B1> type, final TypeEncounter<B1> encounter ) { return new PropertyBinder() { public <T> PropertyBinding bindProperty( final BeanProperty<T> property ) { // simple mapping id, not guaranteed to be unique as we don't include the package namespace Named valueId = Jsr330.named( type.getRawType().getSimpleName() + "." + property.getName() ); final Provider<T> valueProvider = encounter.getProvider( Key.get( property.getType(), valueId ) ); return new PropertyBinding() { public <C> void injectProperty( C bean ) { property.set( bean, valueProvider.get()); } }; } }; } }
We now have a working bean binder that maps bean properties to JSR 330 named bindings. To finish things off, let's add a bit of logging so we can see what the binder is doing during the test:
package example.bean.guice; import java.util.logging.Level; import java.util.logging.Logger; import javax.inject.Named; import javax.inject.Provider; import org.sonatype.guice.bean.inject.BeanBinder; import org.sonatype.guice.bean.inject.PropertyBinder; import org.sonatype.guice.bean.inject.PropertyBinding; import org.sonatype.guice.bean.reflect.BeanProperty; import com.google.inject.Key; import com.google.inject.TypeLiteral; import com.google.inject.spi.TypeEncounter; import com.google.inject.util.Jsr330; public class SimpleBeanBinder implements BeanBinder { private final Logger LOGGER = Logger.getLogger( SimpleBeanBinder.class.getName() ); void log( String message ) { // blank method+class to remove clutter :) LOGGER.logp( Level.INFO, "", "", message ); } public <B1> PropertyBinder bindBean( final TypeLiteral<B1> type, final TypeEncounter<B1> encounter ) { log( "Binding bean <" + type + ">" ); return new PropertyBinder() { public <T> PropertyBinding bindProperty( final BeanProperty<T> property ) { log( "Binding property [" + property.getName() + "]" ); // simple mapping id, not guaranteed to be unique as we don't include the package namespace Named valueId = Jsr330.named( type.getRawType().getSimpleName() + "." + property.getName() ); final Provider<T> valueProvider = encounter.getProvider( Key.get( property.getType(), valueId ) ); return new PropertyBinding() { public <C> void injectProperty( C bean ) { T value = valueProvider.get(); log( "Injecting property [" + property.getName() + "] with \"" + value + "\"" ); property.set( bean, value ); } }; } }; } }
OK so we have our simple bean binder, now we need a bean to test it with. How about the following:
package example.bean.guice; public class PersonBean { private int age; private String name; public void setName( String name ) { this.name = name.toUpperCase(); } public String getName() { return name; } public int getAge() { return age; } }
This is a trivial bean that mixes field and setter injection, it is definitely not meant to represent good coding practice! |
Finally we need to write a test that exercises our bean binder. The following jUnit test bootstraps a Guice injector that uses our binder along with a couple of constant bindings for each of the bean properties. Guice will handle converting the constant strings to the right types. The injector is used to inject the example bean instance into the test, where its properties are checked against the expected values.
package example.bean.guice; import javax.inject.Inject; import junit.framework.TestCase; import org.sonatype.guice.bean.inject.BeanListener; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.TypeLiteral; import com.google.inject.matcher.Matchers; import com.google.inject.util.Jsr330; public class PersonBeanTest extends TestCase { @Inject PersonBean personBean; @Override protected void setUp() { Guice.createInjector( new AbstractModule() { @Override protected void configure() { bindListener( Matchers.only( TypeLiteral.get( PersonBean.class ) ), new BeanListener( new SimpleBeanBinder() ) ); bindConstant().annotatedWith( Jsr330.named( "PersonBean.name" ) ).to( "John Smith" ); bindConstant().annotatedWith( Jsr330.named( "PersonBean.age" ) ).to( "42" ); } } ).injectMembers( this ); } public void testNameBean() { // check the name was capitalized by the setter assertEquals( "JOHN SMITH", personBean.getName() ); assertEquals( 42, personBean.getAge() ); } }
If you are doing a lot of Guice testing, you should take a look at the atunit |
We're now ready to test our example bean binder! Type the following command to kick things off:
mvn clean install
You should see the usual Maven output followed by a successful test, which should look something like this:
------------------------------------------------------- T E S T S ------------------------------------------------------- Running example.bean.guice.PersonBeanTest Jan 8, 2010 12:27:07 AM INFO: Binding bean <example.bean.guice.PersonBean> Jan 8, 2010 12:27:08 AM INFO: Binding property [name] Jan 8, 2010 12:27:08 AM INFO: Binding property [age] Jan 8, 2010 12:27:08 AM INFO: Injecting property [age] with "42" Jan 8, 2010 12:27:08 AM INFO: Injecting property [name] with "John Smith" Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.785 sec Results : Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
Congratulations, you've just used Guice to initialize a plain old bean with no annotations at all!
Interested in how the BeanListener finds bean properties? We'll take a closer look at this in the next post in this series which will cover Generic Bean reflection.