CDI and JSF aren't Best Friends Yet

Where I started banging my head into the table.

This specific issue has actually has jumped up and bit me a few times so I’ll just try explain the most recent one. Basically I have a new object that I am creating through a JSF form page, and that object needs to have a default value set at creation which is determined at runtime. For the sake of exposing this little issue lets assume that this value needs to be used multiple times during the request and thus we will make the producer @RequestScoped to avoid duplicate database calls.

To the code!

@ConversationScoped
@Named
public class CarPricerManager implements Serializable {

    private static final long serialVersionUID = 1L;

    @Inject
    @Active
    private PriceBook activePriceBook;

    private CarPricer carPricer;

    public CarPricer getCarPricer() {
        if(carPricer == null) {

            carPricer = new CarPricer();
            carPricer.setPriceBook(activePriceBook);
        }
        return carPricer;
    }
}

public class PriceBooks {

    @Produces
    @RequestScoped
    @Active
    public PriceBook findActivePriceBook() {
        return em.createQuery(.....).getSingleResult();
    }

    @Produces
    @RequestScoped
    @Named
    public List<PriceBook> priceBooks() {
        return em.createQuery(.....).getResultList();
    }
}

Now lets show the JSF snippet to select our price book when creating our new CarPricer

<h:selectOneMenu value="\#{carPricerManager.carPricer.priceBook}" required="true"
    converter="PriceBookConverter">
    <f:selectItem itemLabel="Choose" />
    <f:selectItems value="\#{priceBooks}" var="_apb" itemLabel="\#{_apb.name}" />
</h:selectOneMenu>

Where it goes wrong.

What we would expect to happen is the selectOneMenu would initially be selected with the value of our active price book. However this isn’t going to be the case because the injection of our active price book does not give us an object of PriceBook.java it gives us an proxy of PriceBook.java and the equals(...) that JSF is going to call will not return true.

Why it goes wrong.

The CDI EG has determined that there is not meaningful way to implement .equals() and .hashCode() on a proxy that can delegate to the underlying bean (See OWB-458 & WELD-695). What this means is that two proxies of the same underlying bean will be equal. However, two proxies to underlying beans which are equal according to .equals will not be equal.

How can we fix this?

One solution would be to execute the .equals() on the converted value if a converter exists. However, the JSF RI Mojarra feels (JAVASERVERFACES-2393) that any proxy must should delegate equals, hashCode and toString to the proxied instance per java.lang.reflect.Proxy. In the case of CDI the underlying bean can change during the lifecycle of the proxy and thus delegating isn’t realistic. This appears to be one of those places where the JSF and CDI expert groups need to step in and find a solution so that developers can enjoy consistent and predictable behavior in their applications.

A solutions that work now.

By making the bean with the producers @RequestScoped and storing the values in the bean after initial population we can have default dependent scoped producers which don’t produce proxies and hit the database only once.

@RequestScoped
public class PriceBooks {

    private PriceBook activeBook;
    private List<PriceBook> books;

    @Produces
    @Active
    public PriceBook findActivePriceBook() {
    if (activeBook == null)
        activeBook = em.createQuery(.....).getSingleResult();
        return activeBook;
    }

    @Produces
    @Named
    public List<PriceBook> priceBooks() {
    if (books == null) {
        books = em.createQuery(.....).getResultList();
        return books;
    }
}
tags: cdi jsf java
Cody Lerum - September 11, 2012
About outjected.com

Outjected is an infrequent blog about the frequent issues and annoyances that pop up while coding.

These are living posts and will be updated as errors or improvements are found.
About Me
Google+