Candidate list of areas of integration.
Expression and Managed Bean Integration Approach 1
Ok folks, here's the plan:
We propose to link JSF and Spring via AOP to be independent of specific JSF implementations. Our current approach utilizes Spring AOP by providing an ApplicationFactory to JSF. Our Factory supplies JSF with a proxied Application which modifies the behaviour of the createMethodBinding() and createValueBinding() methods. These are the only methods responsible for resolving JSF bean references; for the rest of the methods, we call through to the default Application. The implementation class to be proxied is identified via a .properties file, so it can be changed to match the JSF implementation used. (Currently, we use Sun's Reference Implementation.)
The methods receive the name of the requested bean's property in JavaServer Faces expression language (JSF EL). First, they check whether the requested property can be found within JSF's managed beans. Then, they check whether it can be found within Spring's beans. If there's none of both, the name will be tokenized and evaluated starting with JSF's managed beans. If at some point, there's a property with the name of a Spring bean, the context will be changed from JSF to Spring.
An example:
faces-config.xml (partial):
<managed-bean>
<description>JSF managed bean referencing a spring bean</description>
<managed-bean-name>JsfBean</managed-bean-name>
<managed-bean-class>test.JsfBean</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
</managed-bean>
JsfBean.java:
package test;
public class JsfBean {
private SpringBean _springBean;
public JsfBean() {
super();
}
public SpringBean getSpringBean() {
return _springBean;
}
public void setSpringBean(SpringBean paramSpringBean) {
_springBean = paramSpringBean;
}
}
applicationContext.xml (partial):
<bean id="SpringBean" class="test.SpringBean">
<property name="text"><value>some example text</value></property>
</bean>
SpringBean.java:
package test;
public class SpringBean {
String _text;
public SpringBean() {
super();
}
public String getText() {
return _text;
}
public void setText(String paramText) {
_text = paramText;
}
}
showText.jsp (partial):
<h:outputText value="#{JsfBean.springBean.text}" />
In this example, the proxied Application will first call getSpringBean(). If the returned value is null, it will check whether there's a corresponding bean defined within Spring and call the setter. This way, the JSF managed bean is responsible for caching which enables a developer to use the scope settings of JSF.
Additionally, setApplicationContext() will be called upon instantiation of every JSF managed bean that implements ApplicationContextAware, so that it can interact with Spring if it needs to.
This is what we have right now. Comments and feedback are of course very welcome!
Wolfgang
Expression and Managed Bean Integration Approach 2
Wolfgang's approach is slightly different than the one I'm working on. His also has some different advantages and disadvantages. Not sure which is better but I figured I would put forward my implementation if for no other reason than to provide a different perspective.
My approach was to implement a custom VariableResolver. This new variable resolver could provide 2 features to a Spring + JSF implementation, Spring as an additional simple varialbe context and Direct spring involvement in JSF Managed beans.
Spring as an additional variable context
applicationContext.xml (partial):
<bean id="SpringBean" class="test.SpringBean">
<property name="text"><value>some example text</value></property>
</bean>
SpringBean.java:
package test;
public class SpringBean {
String _text;
public SpringBean() {
super();
}
public String getText() {
return _text;
}
public void setText(String paramText) {
_text = paramText;
}
}
showText.jsp (partial):
<h:outputText value="#{SpringBean.text}" />
Given the example above this first feature would allow a JSF binding expression to directly use spring beans from the application context without having to define a Managed bean. Beans used in this approach would not contain any state beyond the scope of the expression. But could be valuable for singletons such as utility classes and stateless event processors. I believe this feature could be added to any JSF implementation in a generic manner by a proxy similar to Wolfgang's approach to proxy the Application above.
Spring bean as a managed bean
faces-config.xml (partial):
<application>
<variable-resolver>SpringVariableResolver</variable-resolver>
</application>
<managed-bean>
<description>JSF managed spring bean</description>
<managed-bean-name>SpringBean</managed-bean-name>
<managed-bean-class>test.SpringBean</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
<managed-property>
<property-name>text</property-name>
<value>This is some text</value>
</managed-property>
</managed-bean>
This feature allows spring beans to be used directly as JSF managed beans. by doing a name match between the <managed-bean-name> tag in the faces-config.xml above and a spring bean name in the application context.
showText.jsp (partial):
<h:outputText value="#{SpringBean.text}" />
When the code sample above is executed then the VariableResolver would check to see if "SpringBean" exists in spring and if it exists as a JSF managed bean as well. If it does VariableResolver will then check to see if the bean already exists in the scope specified by <managed-bean-scope> and returns that instance if it exists. Otherwise, VariableResolver creates a new spring bean (SpringBean), performs the JSF wiring on any managed properties on top of the already wired spring bean, stores the bean in the scope specified, and returns it.
This approach allows for any JSF Managed bean to be a spring bean as well. This bean is then wired by both Spring and JSF. This approach could introduce some confusion because of the double IoC but maybe not.
With this feature there are also some interoperability problems. The main interoperability problem lies in how the JSF implementation retrieves managed bean configuration information and wires up the managed beans. I believe that can be different in each implementation. I don't know if this is a bad idea or not but one possible way around this issue would be for this custom variable resolver to provide it's own faces-context parser and processor.
Here is my VariableResolver implementation as it stands for MyFaces.
SpringVariableResolver.java
import java.util.Map;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.servlet.ServletContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import net.sourceforge.myfaces.MyFacesFactoryFinder;
import net.sourceforge.myfaces.config.FacesConfig;
import net.sourceforge.myfaces.config.FacesConfigFactory;
import net.sourceforge.myfaces.config.ManagedBeanConfig;
import net.sourceforge.myfaces.config.configure.ManagedBeanConfigurator;
import net.sourceforge.myfaces.el.VariableResolverImpl;
public class SpringVariableResolver extends VariableResolverImpl {
public static Log log = LogFactory.getLog(SpringVariableResolver.class);
public Object resolveVariable(FacesContext context, String name) {
ServletContext servletContext =
(ServletContext)context.getExternalContext().getContext();
ApplicationContext appContext =
WebApplicationContextUtils.getWebApplicationContext(servletContext);
if(!appContext.containsBean(name)) {
log.info("Spring Bean with name '"+name+"' does not exist allowing MyFaces to process.");
return super.resolveVariable(context, name);
} else {
ExternalContext externalContext = context.getExternalContext();
FacesConfigFactory fcf = MyFacesFactoryFinder.getFacesConfigFactory(externalContext);
FacesConfig facesConfig = fcf.getFacesConfig(context.getExternalContext());
ManagedBeanConfig mbc = facesConfig.getManagedBeanConfig(name);
if (mbc != null) {
String scopeKey = mbc.getManagedBeanScope().toUpperCase();
Map scope = null;
if("request".toUpperCase().equals(scopeKey)) {
scope = context.getExternalContext().getRequestMap();
} else if("session".toUpperCase().equals(scopeKey)){
scope = context.getExternalContext().getSessionMap();
} else if("application".toUpperCase().equals(scopeKey)){
scope = context.getExternalContext().getApplicationMap();
}
if(scope != null && scope.containsKey(name)) {
log.info("Found bean '"+name+"' in scope: "+scopeKey);
return scope.get(name);
} else {
ManagedBeanConfigurator configurator = new ManagedBeanConfigurator(mbc);
Object value = appContext.getBean(name);
configurator.configure(context, value);
if(scope != null) {
scope.put(name, value);
}
log.info("Returning JSF Managed spring bean."+name);
return value;
}
} else {
log.info("Returning spring bean."+name);
return appContext.getBean(name);
}
}
}
}
In the above example I'm extending MyFaces' existing VariableResolverImpl class however this could easily be changed to a proxy instead.
Anyway, here is another possible approach to a JSF and Spring integration. I must preface my work with the fact that I am very new with both JSF and Spring so there could be several problems with this approach that I'm not aware of so I look forward to comments from those who are more experienced with both.
Mike
Listener or Event Model Integration
I might as well while I'm at it offer up a preliminary and very ugly way to handle the JSF listener tags allowing the use of spring beans as component listeners.
The idea here is to basicly create a Spring-faces tag library to provide a Spring bean listener wrapper to a spring bean.
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="testBean" class="TestBean" singleton="true"/>
<bean id="testListener" class="TestListener" singleton="false">
<property name="test">
<ref local="testBean"/>
</property>
</bean>
<bean id="testSpringListener" class="test.SpringActionListener" singleton="false">
<property name="beanName">
<value>testSpringListener</value>
</property>
<property name="actionListener">
<ref local="testListener"/>
</property>
</bean>
</beans>
TestListener.java
import javax.faces.event.AbortProcessingException;
import javax.faces.event.ActionEvent;
import javax.faces.event.ActionListener;
public class TestListener implements ActionListener {
private TestBean testBean;
public void setTest(TestBean bean) {
testBean = bean;
}
public void processAction(ActionEvent actionEvent)
throws AbortProcessingException {
testBean.hello();
}
}
test.jsp
<%@ taglib uri="http: prefix="h" %>
<%@ taglib uri="http: prefix="f" %>
<%@ taglib uri="/WEB-INF/spring_jsf.tld" prefix="spring" %>
<f:view>
<h:form id="form1">
<h:commandButton action="success" binding="#{Page1.button1}" id="button1" value="Test Listener">
<spring:actionListener bean="testListener"/>
</h:commandButton>
</h:form>
</f:view>
SpringActionListenerTag.java
package test;
import javax.faces.FacesException;
import javax.faces.component.ActionSource;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.el.ValueBinding;
import javax.faces.webapp.UIComponentTag;
import javax.servlet.ServletContext;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.Tag;
import javax.servlet.jsp.tagext.TagSupport;
import javax.faces.event.ActionListener;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
public class SpringActionListenerTag extends TagSupport {
private String _bean = null;
public SpringActionListenerTag() { }
public void setBean(String bean) {
_bean = bean;
}
public int doStartTag() throws JspException {
if (_bean == null) {
throw new JspException("bean attribute not set");
}
UIComponentTag componentTag = UIComponentTag.getParentUIComponentTag(pageContext);
if (componentTag == null) {
throw new JspException("ActionListenerTag has no UIComponentTag ancestor");
}
if (componentTag.getCreated()) {
UIComponent component = componentTag.getComponentInstance();
if (component instanceof ActionSource) {
String beanName;
FacesContext facesContext = FacesContext.getCurrentInstance();
if (UIComponentTag.isValueReference(_bean)) {
ValueBinding vb = facesContext.getApplication().createValueBinding(_bean);
beanName = (String)vb.getValue(facesContext);
} else {
beanName = _bean;
}
ServletContext servletContext =
(ServletContext)facesContext.getExternalContext().getContext();
ApplicationContext appContext =
WebApplicationContextUtils.getWebApplicationContext(servletContext);
if(appContext.containsBean(beanName)) {
Object bean = appContext.getBean(beanName);
if(bean instanceof ActionListener) {
if(bean instanceof SpringActionListener) {
((ActionSource)component).addActionListener((ActionListener)bean);
} else {
SpringActionListener springListener = new SpringActionListener();
springListener.setBeanName(beanName);
springListener.setActionListener((ActionListener)bean);
((ActionSource)component).addActionListener(springListener);
}
} else {
throw new FacesException("Bean '"+beanName+"' is not of type "+
ActionListener.class.getName());
}
} else {
throw new FacesException("Bean '"+beanName+"' does not exist in " +
"Spring Application Context");
}
} else {
throw new JspException("Component " + component.getId() + " is no ActionSource");
}
}
return Tag.SKIP_BODY;
}
}
spring_jsf.tld
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN"
"http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd">
<taglib xmlns="http://java.sun.com/JSP/TagLibraryDescriptor">
<tlib-version>0.0.1</tlib-version>
<jsp-version>1.2</jsp-version>
<short-name>Spring JSF Tag Library</short-name>
<tag>
<name>actionListener</name>
<tag-class>test.SpringActionListenerTag</tag-class>
<body-content>empty</body-content>
<attribute>
<name>bean</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
</tag>
</taglib>
SpringActionListener.java
package test;
import java.util.HashMap;
import java.util.Map;
import javax.faces.component.StateHolder;
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.ActionEvent;
import javax.faces.event.ActionListener;
import javax.servlet.ServletContext;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
public class SpringActionListener implements ActionListener, StateHolder {
public static String LISTENER_NAME_KEY = "NAME";
public static String LISTENER_STATE_KEY = "STATE";
boolean isTransient = false;
String beanName;
ActionListener actionListener;
Map state;
public SpringActionListener() {
}
public void setBeanName(String beanName) {
this.beanName = beanName;
}
public String getBeanName() {
return beanName;
}
public void setActionListener(ActionListener actionListener) {
this.actionListener = actionListener;
}
public ActionListener getActionListener() {
if(actionListener != null) {
return actionListener;
}
if(state != null) {
beanName = (String)state.get(LISTENER_NAME_KEY);
}
if(beanName == null) {
throw new IllegalStateException("Listener must have been initialized by a State Manager.");
}
ServletContext context =
(ServletContext)FacesContext.getCurrentInstance().getExternalContext().getContext();
ApplicationContext appContext =
WebApplicationContextUtils.getWebApplicationContext(context);
actionListener = (ActionListener)appContext.getBean(beanName);
if(state.get(LISTENER_STATE_KEY) != null) {
if(actionListener instanceof StateHolder) {
((StateHolder)actionListener).restoreState(FacesContext.getCurrentInstance(),
state.get(LISTENER_STATE_KEY));
}
}
return actionListener;
}
public void processAction(ActionEvent actionEvent) throws AbortProcessingException {
getActionListener().processAction(actionEvent);
}
public boolean isTransient() {
return isTransient;
}
public void restoreState(FacesContext context, Object state) {
this.state = (Map)state;
}
public Object saveState(FacesContext context) {
if(state == null || actionListener != null) {
state = new HashMap();
state.put(LISTENER_NAME_KEY, beanName);
ActionListener listener = getActionListener();
if(listener instanceof StateHolder) {
state.put(LISTENER_STATE_KEY, ((StateHolder)listener).saveState(context));
}
}
return state;
}
public void setTransient(boolean newTransientValue) {
isTransient = newTransientValue;
}
}
Unfortunately I know nothing about JSF's event model but this bit of code allowed me to use Spring beans as action listeners so maybe it will help others. This approach could easily be duplicated to implement changeEvent Listeners too.
The way the SpringActionListener is implemented here it could support the wrapping of ActionListeners who implement StateHolder. SpringActionListener itself could also be a spring bean which wraps another listener. If it is then the Spring Tag will store the bean itself as the ActionListener so the following JSP code would work too.
test.jsp using SpringActionListener Bean
<h:commandButton action="success" binding="#{Page1.button1}" id="button1" title="Test" value="Test Listener">
<spring:actionListener bean="testSpringListener"/>
</h:commandButton>
This approach I believe is JSF implementation generic.
Regards,
Mike
Validator, Converter, and UI Component Integration Approach
Configuring JSF Validators, Converters, and UIComponents in Spring
Hi Mike,
I'm one of the guys who came up with "approach one" together with Wolfgang. We just switched from developing our own open source framework to using spring and JSF, so we're no experts on that field neither. I quite like your approach of double declaring one bean in order to be able to define the scope of a bean (by being JSF managed) instead of having to code two beans. As far as I can see, we should be able to combine our approach (implementation independant, JSF beans can reference spring beans) with your approach quite easily.
Concerning your listener approach: I don't really like having to code a custom tag library and having to teach JSP developers how to use it. I haven't looked into the listener concept of JSF that much, so I don't have an idea at hand yet. I'm also busy working on a project this week, so I won't be able to have a look at it before mid of next week. But I'd be glad if we could find a transparent solution so that developers don't have to bother and ideally don't have to know that there's some glue code integrating both frameworks.
Cheers,
Thomas