Tuesday, May 04, 2010

JSR-330 and Qualifiers in the JBoss Microcontainer

The latest JBoss Microcontainer jboss-kernel 2.2.0.Alphas contain support for the @javax.inject.Inject and @javax.inject.Qualifier annotations from JSR-330.

While most of the use of the JBoss Microcontainer in JBoss AS uses explicitly named injection, e.g.

<?xml version="1.0" encoding="UTF-8"?>
<deployment xmlns="urn:jboss:bean-deployer:2.0">
   <bean name="MyDependency" class="org.jboss.test.Dependency"/>
   <bean name="MyBean" class="org.jboss.test.Bean">
      <property name="dependency"><inject bean="MyDependency"/></property>
   </bean>
</deployment>

The above means that when deploying the MyBean bean bean it will look up the MyDependency bean by name and inject that instance into MyBean's dependency property.

Contextual injection
We can also look up things by type, first let us see how to do this using xml. This has been supported by the Microcontainer for a while, so this might be familiar:

<?xml version="1.0" encoding="UTF-8"?>
<deployment xmlns="urn:jboss:bean-deployer:2.0">
   <bean name="MyDependency" class="org.jboss.test.Dependency"/>
   <bean name="MyBean" class="org.jboss.test.Bean">
      <property name="dependency"><inject/></property>
   </bean>
</deployment>

Now if the definition of MyBean's class is: package org.jboss.test;
public class Bean{
   Dependency dependency;

   public void setDependency(Dependency dependency){
      this.dependency = dependency;
   }
}

When creating MyBean this time, we have not given the name of the bean to use when configuring the dependency property. Instead the Microcontainer will check the type of the property, and find which beans are installed in the Microcontainer that can be cast to that type. In this case it finds MyDependency and uses that. If there had been more beans that could be cast to this type, an error would have been thrown. We'll see later how qualifiers can be used to resolve the ambiguities.

Using @javax.inject.Inject 
Now let us do the same using annotations. We first need to install the beans into the MC, the simplest way is to use an xml file to define the beans:

<?xml version="1.0" encoding="UTF-8"?>
<deployment xmlns="urn:jboss:bean-deployer:2.0">
   <bean name="MyDependency" class="org.jboss.test.Dependency"/>
   <bean name="MyBean" class="org.jboss.test.Bean"/> 
</deployment>

Notice that we did not define any injection for the dependency property in this case, instead we use @javax.inject.Inject on MyBean's class to define the injection:

package org.jboss.test; 

import javax.inject.Inject; 

public class Bean{

   Dependency dependency;

   @Inject
   public void setDependency(Dependency dependency){ 
      this.dependency = dependency;
   } 
} 
What happens now is that when MyBean is deployed we find the @javax.inject.Inject annotation on the dependency property and this causes a lookup of the bean to be found. As in the last example it looks up the bean by type, and throws an error if more that one can be found.

Qualifiers to the rescue
If we deploy the following beans:
<?xml version="1.0" encoding="UTF-8"?>
<deployment xmlns="urn:jboss:bean-deployer:2.0">
   <bean name="SomeDependency" class="org.jboss.test.SomeDependency"/>
   <bean name="TestDependency" class="org.jboss.test.TestDependency"/>
   <bean name="MyBean" class="org.jboss.test.Bean"/>
</deployment>

with the following classes
package org.jboss.test;

public class SomeDependency extends Dependency{
}
- - - - - - - - -
package org.jboss.test;

public class TestDependency extends Dependency{
}
- - - - - - - - -
package org.jboss.test;
import javax.inject.Inject;

public class Bean{
   Dependency dependency;

   @Inject
   public void setDependency(Dependency dependency){
      this.dependency = dependency;
   }
}

Now we have an ambiguity since both TestDependency and SomeDependency can be cast to org.jboss.test.Dependency. To give the Microcontainer a hint about which of these instances should be used we can use a qualifier. A qualifier is any annotation that has been annotated with the @javax.inject.Qualifier annotation. For example:
package org.jboss.test;

import javax.inject.Qualifier;

@Qualifier
@Retention(RUNTIME)
public @interface Test{
}

Now we can use this qualifier on our beans by adding it to TestDependency's class and to the Bean.dependency injection point:

package org.jboss.test;

@Test
public class TestDependency extends Dependency{
}
- - - - - - - - -
package org.jboss.test;

import javax.inject.Inject;

public class Bean{
   Dependency dependency;

   @Inject @Test
   public void setDependency(Dependency dependency){
      this.dependency = dependency;
   }
}

Now the Microcontainer will look up all beans that can be cast to org.jboss.test.Dependency AND that have the @org.jboss.test.Test annotation. In this case only TestDependency satisfies those criteria and is used for injection.

Qualifiers via xml
As part of implementing qualifiers in the Microcontainer we built in native support for qualifiers and came up with something somewhat more powerful that what is indicated by JSR-330. First let's deploy the classes from the previous example without any annotations:

package org.jboss.test;

public class SomeDependency extends Dependency{
}
- - - - - - - - -
package org.jboss.test;

public class TestDependency extends Dependency{
}
- - - - - - - - -
package org.jboss.test;

import javax.inject.Inject;

public class Bean{
   Dependency dependency;

   public void setDependency(Dependency dependency){
      this.dependency = dependency;
   }
}

This time the injection and qualifier annotations are driven by xml:

<?xml version="1.0" encoding="UTF-8"?>
<deployment xmlns="urn:jboss:bean-deployer:2.0">
   <bean name="SomeDependency" class="org.jboss.test.SomeDependency"/>
   <bean name="TestDependency" class="org.jboss.test.TestDependency">
      <qualifier content="Annotation">@org.jboss.test.Test</qualifier>
   </bean>
   <bean name="MyBean" class="org.jboss.test.Bean">
      <property name="dependency">
         <inject>
            <qualifier content="Annotation" type="Required">@org.jboss.Test</qualifier>
         </inject>
      </property>
   </bean>
</deployment>

Now, we're saying that TestDependency provides the @org.jboss.test.Test qualifier, and when injecting Bean.dependency we want to inject any bean that is of type Dependency and that has the @org.jboss.test.Test qualifier. As when using "real" annotations it will pick out the TestDependency bean. The qualifier content is Annotation which means the parser will try to create an annotation from the body of the qualifier annotation. When qualifier annotations are applied in this way, they do not need to be annotated with @javax.inject.Inject.

A qualifier does not have to be an annotation, simple strings can be used by omitting the content attribute:
<?xml version="1.0" encoding="UTF-8"?>
<deployment xmlns="urn:jboss:bean-deployer:2.0">
   <bean name="SomeDependency" class="org.jboss.test.SomeDependency"/>
   <bean name="TestDependency" class="org.jboss.test.TestDependency">
      <qualifier>test</qualifier>
   </bean>
   <bean name="MyBean" class="org.jboss.test.Bean">
      <property name="dependency">
         <inject>
            <qualifier type="Required">test</qualifier>
         </inject>
      </property>
   </bean>
</deployment>

Qualifiers can also be used in other places supporting the inject element, such as parameters to constructors and install methods.

Bean level qualifiers
In the previous example we applied qualifiers to a particular property. As a convenience you can choose default qualifiers that should be applied when doing injection at bean level. This is useful when you have a lot of properties that need injecting, here is an example:
<?xml version="1.0" encoding="UTF-8"?>
<deployment xmlns="urn:jboss:bean-deployer:2.0">
   <bean name="SomeDependency" class="org.jboss.test.SomeDependency"/>
   <bean name="TestDependency" class="org.jboss.test.TestDependency">
      <qualifier>test</qualifier>
   </bean>
   <bean name="SomeOtherDependency" class="org.jboss.test.SomeOtherDependency">
      <qualifier>test</qualifier>
   </bean>
   <bean name="TestOtherDependency" class="org.jboss.test.TestOtherDependency">
      <qualifier>test</qualifier>
      <qualifier>other</qualifier>
   </bean>
   <bean name="MyBean" class="org.jboss.test.Bean">
      <qualifier type="Required">test</qualifier>
      <qualifier type="Optional">other</qualifier>
      <property name="dependency"><inject/></property>
      <property name="otherDependency"><inject/></property>
   </bean>
</deployment>

Now we are saying that when doing contextual injection into MyBean's properties, the candidate beans MUST have the test qualifier, and that the other qualifier can be used to further strip out ambiguities. If we have the following bean classes:

package org.jboss.test;

public class SomeDependency extends Dependency{
}
- - - - - - - - -
package org.jboss.test;

public class TestDependency extends Dependency{
}
- - - - - - - - -
package org.jboss.test;

public class SomeOtherDependency extends OtherDependency{
}
- - - - - - - - -
package org.jboss.test;

public class TestOtherDependency extends OtherDependency{
}
- - - - - - - - -
package org.jboss.test;

import javax.inject.Inject;

public class Bean{
   Dependency dependency;

   OtherDependency otherDependency;

   public void setDependency(Dependency dependency){
      this.dependency = dependency;
   }

   public void setOtherDependency(OtherDependency otherDependency){
      this.otherDependency = otherDependency;
   }
}

then TestDependency is used for MyBean.dependency and TestOtherDependency is used for injection into MyBean.otherDependency.

Custom qualifiers
If the above qualifiers are not enough to narrow down what you want to inject, you can create custom qualifiers as shown in this simple example. If we deploy the following beans we end up with Dependency3 being injected into MyBean.dependency:
<?xml version="1.0" encoding="UTF-8"?>
<deployment xmlns="urn:jboss:bean-deployer:2.0">
   <bean name="TestMatcherAndParser"
      class="org.jboss.test.kernel.qualifiers.support.TestMatcherAndParser"/>
      <bean name="QualifierMatchers" class="org.jboss.kernel.spi.qualifier.QualifierMatchers">
         <constructor factoryMethod="getInstance">
      </constructor>
      <install method="addParser">
         <parameter><inject bean="TestMatcherAndParser"/></parameter>
      </install>
      <install method="addMatcher">
         <parameter><inject bean="TestMatcherAndParser"/></parameter>
      </install>
      </bean> 
      <bean name="Dependency1" class="org.jboss.test.SomeDependency">
         <qualifier>test</qualifier>
      <qualifier content="Test">Hello-xxx</qualifier>
      </bean>
      <bean name="Dependency2" class="org.jboss.test.Dependency">
         <qualifier>test</qualifier>
      </bean>
      <bean name="Dependency3" class="org.jboss.test.TestDependency">
         <qualifier>test</qualifier>
         <qualifier content="Test">Hola-xxx</qualifier>
      </bean>
      <bean name="MyBean" class="org.jboss.test.Bean">
         <qualifier type="Required">test</qualifier>
      <qualifier type="Required" content="Test">xxx-Hola</qualifier>
    </bean>
</deployment>

The magic behind understanding content="Test" lives in the TestMatcherAndParser bean which is installed via the QualifierParser singleton:
package org.jboss.test.kernel.qualifiers.support;

import java.util.Set;
import org.jboss.beans.metadata.api.model.QualifierContent;
import org.jboss.dependency.spi.ControllerContext;
import org.jboss.kernel.spi.qualifier.QualifierMatcher;
import org.jboss.kernel.spi.qualifier.QualifierParser;

public class TestMatcherAndParser implements QualifierMatcher, QualifierParser{
   public TestMatcherAndParser(){
   }

   //QualifierParser methods
   public QualifierContent getHandledContent(){
      return QualifierContent.getContent("Test");
   }

   public Object parseSupplied(ClassLoader cl, Object rawQualifier){
      return new Supplied((String)rawQualifier);
   }

   public Object parseWanted(ClassLoader cl, Object object){
      return new Wanted((String)object);
   }

   //QualifierMatcher methods
   public Class getHandledType(){
      return Wanted.class;
   }

   public boolean matches(ControllerContext context, Set<Object> suppliedQualifiers, Wanted qualifier){
      for (Object supplied : suppliedQualifiers){
         if (supplied instanceof Supplied)
            return qualifier.getString().equals(((Supplied)supplied).getString());
      }
      return false;
   }

   private static class Supplied{
      String string;

      public Supplied(String string){
         int i = string.indexOf("-xxx");
         if (i >= 0)
            string = string.substring(0, i);
            this.string = string;
      }

      public String getString(){
         return string;
      }
   }
}
- - - - - - - - -
package org.jboss.test.kernel.qualifiers.support;

public class Wanted{
   String string;
   public Wanted(String string){
      this.string = string;
      if (string.startsWith("xxx-"))
         string = string.substring(4);
         this.string = string;
      }

   public String getString(){
      return string;
   }
}

TestMatcherAndParser implements the QualifierParser and QualifierMatcher interfaces. The getHandledContent() method is what links this parser to content="Test". When parsing a qualifier with that content type supplied from from a bean (e.g. the qualifier entries in Dependency1 and Dependency3) we end up in the parseSupplied() method which creates an instance of Supplied. Similarly when parsing a qualifier with that content type from a bean wanting to inject something we end up in the parseWanted() method which creates an instance of Wanted.

So for Dependency1 we end up with a Supplied containing the string Hello and for Dependency3 we end up with a Supplied containing the string Hola. We end up with a Wanted containing the string Hola for MyBean.
Next when checking the qualifiers when doing injection into MyBean and checking

<qualifier type="Required" content="Test">xxx-Hola</qualifier>

behind the scenes the microcontainer finds all the contexts that are of the correct type and which have supplied qualifiers where type content="Test", i.e. the contexts Dependency1 and Dependency3. Then for each of these contexts it calls matches() with all the qualifiers for the candidate bean along with the parsed Wanted qualifier for MyBean.

When checking Dependency1 it calls matches() with the Supplied containing Hello and the Wanted containing Hola which does not match, and for Dependency2 it calls matches() with the Supplied containing Hello and the Wanted containing Hola which matches, meaning that only Dependency1 matches all the criteria. When calling matches() the ControllerContext (with information about the candidate bean's name, type, instance and other things) is passed in as well.

In the provided example we are only doing a silly little check where the Supplied gets rid of the final -xxx and the Wanted gets rid of the leading -xxx. The point here is that it is simple to implement more advanced qualifier checking than is provided out-of-the-box, and this mechanism has already been used to provide support for matching OSGi properties.
Post a Comment