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.
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! :)
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.