8

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.

Jim Tough
  • 14,843
  • 23
  • 75
  • 96
  • 1
    It was suggested that I supply a readObject() method and manually set the ApplicationData object in there during deserialization by using the FacesContext. I don't think that will work since the FacesContext is only available during the lifespan of a request. The deserialization is happening at application startup. – Jim Tough Sep 23 '10 at 12:47
  • 2
    correct, that's why I deleted my answer. It appears more complicated (hence +1 for the question) – Bozho Sep 23 '10 at 12:56

2 Answers2

6

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
  • 14,843
  • 23
  • 75
  • 96
  • 4
    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 Sep 23 '10 at 15:57
  • Hi @Jim. In case of `FacesContext` what should i do on deserialization(manually get it or there is an annotation or something else)? – Arash Sep 19 '22 at 09:06
1

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
  • 588,226
  • 146
  • 1,060
  • 1,140
  • 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 Sep 23 '10 at 13:31
  • it's quite easy. Just follow the tutorial - your proxy case is the simplest :) – Bozho Sep 23 '10 at 13:37