Sunday, September 30, 2007

Integrating Guice and JSF

Special thanks to my teammate Çağatay Çivici for inspiring me on this one. I will never be able to pronounce his name correctly, but that certainly doesn't stop me from taking one of his good ideas and running with it.

Çağatay approached JSF/Guice integration by using a ServletContextListener to initialize Guice while annotating each managed bean with @PostConstruct. For my spike, I have kept the ServletContextListener but replaced @PostConstruct with a custom VariableResolver. It goes like this ...

The Model

public class ShoppingCart {
private Order order;
@Inject
public void setOrder(Order order) { this.order = order; }
public Order getOrder() { return order; }
}
public interface Order { }
public class BulkOrder implements Order {
public String toString() { return "I am a Bulk Order"; }
}

The View

<html>
<body>
<f:view>
<h:outputText value="#{shoppingCart.order}" />
</f:view>
</body>
</html>
This page renders "I am a Bulk Order", even though the shopping cart has no notion of a bulk order. The bulk order is injected into the shopping cart instance, a regular managed bean.
<managed-bean>
<managed-bean-name>shoppingCart</managed-bean-name>
<managed-bean-class>com.thoughtworks.guice.ShoppingCart
</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
</managed-bean>
This isn't an escape from XML hell, but it will put your configuration files on a diet. You still have to "name" each managed bean, Guice handles everything else.

How it Works

Once JSF has created the ShoppingCart instance, Guice is allowed to intercept before it is received by the view. (In faces-config.xml )
<application>
<variable-resolver>
com.thoughtworks.guice.GuiceVariableResolver
</variable-resolver>
</application>

public class GuiceVariableResolver extends VariableResolver {
private final VariableResolver wrapped;
public GuiceVariableResolver(VariableResolver wrapped) {
if(wrapped == null)
throw new NullPointerException("wrapped "
+ VariableResolver.class.getName());
this.wrapped = wrapped;
}

@Override
public Object resolveVariable(FacesContext fctx, String name)
throws EvaluationException {
Object resolved = wrapped.resolveVariable(fctx, name);
if(resolved != null) {
Map map = fctx.getExternalContext().getApplicationMap();
Injector injector = (Injector) map.get(Injector.class.getName());
if(injector == null)
throw new NullPointerException("Could not locate "
+ "Guice Injector in application scope using"
+ " key '" + Injector.class.getName() + "'");
injector.injectMembers(resolved);
}
return resolved;
}
}

The GuiceVariableResolver relies on the GuiceServletContextListener, which isn't any different from Çağatay's ServletContextListener (In web.xml)
<listener>
<listener-class>
com.thoughtworks.guice.GuiceServletContextListener
</listener-class>
</listener>

public class GuiceServletContextListener
implements ServletContextListener {
public void contextInitialized(ServletContextEvent event) {
ServletContext ctx = event.getServletContext();
Injector inject = Guice.createInjector(new ShoppingModule());
ctx.setAttribute(Injector.class.getName(), inject);
}
public void contextDestroyed(ServletContextEvent event) {
ServletContext ctx = event.getServletContext();
ctx.removeAttribute(Injector.class.getName());
}
}

public class ShoppingModule implements Module {
public void configure(Binder binder) {
binder.bind(Order.class).to(BulkOrder.class);
}
}

5 comments:

Cagatay said...

Nice hack Dennis, better jsf 1.1 support for sure:)

Not Dennis Byrne said...

Thanks man, here is part 2 ... http://notdennisbyrne.blogspot.com/2007/10/integrating-guice-and-jsf-part-2.html

name said...

7Lp3tH Magnific!

Volnei said...

I thing this is a good solution...

http://code.google.com/p/guicesf/downloads/list

Anonymous said...

Implementing GuiceVariableResolver + adding it to faces-config.xml was enough to make it work for us! We use Guice 3.0 / MyFaces 1.1. We didn't touch any other class. Thank you!!!