views:

96

answers:

2

I'm not sure if what I'm doing is wrong, or if I just missed an annotation or configuration item somewhere. Here's the situation:

I have a JSF application with a session-scoped bean named SessionData. This bean has an application-scoped bean reference (of type ApplicationData) injected into it at creation time. This works ok when the session is first created. The dependency injection is done with <managed-bean> elements in the faces-config.xml file as shown here:

<managed-bean>
    <managed-bean-name>sessionData</managed-bean-name>
    <managed-bean-class>my.package.SessionData</managed-bean-class>
    <managed-bean-scope>session</managed-bean-scope>
    <managed-property>
        <property-name>applicationData</property-name>
        <property-class>my.package.ApplicationData</property-class>
        <value>#{applicationData}</value>
    </managed-property>
</managed-bean>
<managed-bean>
    <managed-bean-name>applicationData</managed-bean-name>
    <managed-bean-class>my.package.ApplicationData</managed-bean-class>
    <managed-bean-scope>application</managed-bean-scope>
</managed-bean>

Because it doesn't make sense to have my SessionData object include the ApplicationData object when it is serialized, I have marked the ApplicationData reference as transient in my SessionData object:

transient private ApplicationData applicationData;

All is good until the web application is stopped (in my Tomcat 6.x container) and the sessions are serialized. When I restart the application and the sessions are deserialized, my reference to ApplicationData is not re-injected by JSF. I know that deserialization is supposed to leave transient fields without a value. Is there a way to signal JSF that this session-scoped object requires its dependencies be set again after deserialization?

I am using MyFaces JSF 1.2 and Tomcat 6.0.26 as my web application container.

+1  A: 

you can add a method:

private void readObject(java.io.ObjectInputStream in)
 throws IOException, ClassNotFoundException {
  in.defaultReadObject();
  applicationData = initializeApplicationData();
}

And in initializeApplicationData you can use a dynamic proxy object. Using CGLIB or javassist create a proxy that, before each method invocation sets an internal field - the real ApplicationData. If it is null, then get the current FacesContext (which will be accessible at that point) and get the managed bean from there by:

FacesContext facesContext = FacesContext.getCurrentInstance();
originalApplicationData = (ApplicationData)facesContext.getApplication()
  .createValueBinding("#{applicationData}").getValue(facesContext);

and delegate to its methods.

This is an ugly workaround, but I think it will work.

Bozho
@Bozho This will be my first time using javassist, so I'm going to need some time to digest that part of your solution. Thank you.
Jim Tough
it's quite easy. Just follow the tutorial - your proxy case is the simplest :)
Bozho
+2  A: 

Although the solution offered by Bozho could work, I don't want to introduce proxy objects into an application that isn't currently using them. My solution is less than ideal, but it gets the job done.

I left the transient field in place:

transient private ApplicationData _applicationData;

I also left the setter in place so JSF can initially set the reference when the SessionData object is created the first time:

public void setApplicationData(ApplicationData applicationData) {
    _applicationData = applicationData;
}

The change that I made was in the getter method. Methods in the SessionData object now need to stop directly accessing the _applicationData field and instead get the reference via the getter. The getter will first check for a null reference. If it is null, then the managed bean is obtained via the FacesContext. The constraint here is that the FacesContext is only available during the lifespan of a request.

/**
 * Get a reference to the ApplicationData object
 * @return ApplicationData
 * @throws IllegalStateException May be thrown if this method is called
 *  outside of a request and the ApplicationData object needs to be
 *  obtained via the FacesContext
 */
private ApplicationData getApplicationData() {
    if (_applicationData == null) {
        _applicationData = JSFUtilities.getManagedBean(
            "applicationData",  // name of managed bean
            ApplicationData.class);
        if (_applicationData == null) {
            throw new IllegalStateException(
                "Cannot get reference to ApplicationData object");
        }
    }
    return _applicationData;
}

If anyone cares, here is the code for my getManagedBean() method:

/**
 * <p>Retrieve a JSF managed bean instance by name.  If the bean has
 * never been accessed before then it will likely be instantiated by
 * the JSF framework during the execution of this method.</p>
 * 
 * @param managedBeanKey String containing the name of the managed bean
 * @param clazz Class object that corresponds to the managed bean type
 * @return T
 * @throws IllegalArgumentException Thrown when the supplied key does
 *  not resolve to any managed bean or when a managed bean is found but
 *  the object is not of type T
 */
public static <T> T getManagedBean(String managedBeanKey, Class<T> clazz)
        throws IllegalArgumentException {
    Validate.notNull(managedBeanKey);
    Validate.isTrue(!managedBeanKey.isEmpty());
    Validate.notNull(clazz);
    FacesContext facesContext = FacesContext.getCurrentInstance();
    if (facesContext == null) {
        return null;
    }
    Validate.notNull(facesContext.getApplication());
    ELResolver resolver = facesContext.getApplication().getELResolver();
    Validate.notNull(resolver);
    ELContext elContext = facesContext.getELContext();
    Validate.notNull(elContext);
    Object managedBean = resolver.getValue(
        elContext, null, managedBeanKey);
    if (!elContext.isPropertyResolved()) {
        throw new IllegalArgumentException(
            "No managed bean found for key: " + managedBeanKey);
    }
    if (managedBean == null) {
        return null;
    } else {
        if (clazz.isInstance(managedBean)) {
            return clazz.cast(managedBean);
        } else {
            throw new IllegalArgumentException(
                "Managed bean is not of type [" + clazz.getName() +
                "] | Actual type is: [" + managedBean.getClass().getName()+
                "]");
        }
    }
}

And don't pick on my Validate calls. I'll take them out after I'm done with development! :)

Jim Tough
There's a shortcut in [`Application#evaluateExpressionGet()`](http://download.oracle.com/javaee/6/api/javax/faces/application/Application.html#evaluateExpressionGet%28javax.faces.context.FacesContext,%20java.lang.String,%20java.lang.Class%29). See also [this answer](http://stackoverflow.com/questions/2633112/jsf-get-managed-bean-by-name/2633733#2633733).
BalusC