Why move from Struts to JSF at all
Struts has been the workhorse for years. Action classes, forwards, Tiles, and a pile of taglibs got many of us to production. It still ships on every team server I visit.
JSF is arriving with vendor love and a component story that feels closer to how users click. Backing beans, validators, converters, and view state can clean up a lot of glue code we wrote by hand.
What did we expect to gain
Fewer custom tags. Fewer ActionForm beans. Better reuse through UI components. A view story that plays nice with tools. And a path into Ajax without bolting scripts onto every page.
The short version. Struts to JSF buys us a cleaner UI layer and a simpler way to wire UI to services.
Start small and run both frameworks
Do not flip the whole app at once. Run Struts and JSF side by side. Keep the Struts controller for what already works and route new pages to the Faces servlet.
How do we wire that in web xml
<web-app>
<servlet>
<servlet-name>action</servlet-name>
<servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>action</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.jsf</url-pattern>
</servlet-mapping>
</web-app>Old flows keep using .do. New screens ride .jsf. No big bang. No extra pain.
Map Action to backing bean
In Struts we post to an Action, set stuff on a request, and forward. In JSF we bind to a managed bean and return an outcome string that picks a view.
What does that look like
// Struts action
public class LoginAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest req, HttpServletResponse res) {
LoginForm f = (LoginForm) form;
boolean ok = authService.login(f.getUser(), f.getPass());
if (ok) {
req.getSession().setAttribute("user", f.getUser());
return mapping.findForward("home");
}
req.setAttribute("error", "Bad login");
return mapping.getInputForward();
}
}// JSF managed bean
public class LoginBean {
private String user;
private String pass;
private AuthService authService;
public String doLogin() {
boolean ok = authService.login(user, pass);
if (ok) {
FacesContext.getCurrentInstance()
.getExternalContext().getSessionMap().put("user", user);
return "home";
}
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage("Bad login"));
return null; // stay on page
}
// getters and setters
}<faces-config>
<managed-bean>
<managed-bean-name>login</managed-bean-name>
<managed-bean-class>com.app.ui.LoginBean</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
</managed-bean>
<navigation-rule>
<from-view-id>/login.jsp</from-view-id>
<navigation-case>
<from-outcome>home</from-outcome>
<to-view-id>/home.jsp</to-view-id>
<redirect/>
</navigation-case>
</navigation-rule>
</faces-config>Swap tags screen by screen
Struts tags move to h and f tags. Keep JSP for now if that is what you ship. Facelets is tempting and already more pleasant than Tiles. More on that in a bit.
How do tags line up
<!-- Struts -->
<html:form action="/login.do">
<html:text property="user"/>
<html:password property="pass"/>
<html:submit value="Login"/>
</html:form>
<!-- JSF -->
<h:form>
<h:inputText value="#{login.user}"/>
<h:inputSecret value="#{login.pass}"/>
<h:commandButton value="Login" action="#{login.doLogin}"/>
<h:messages />
</h:form>Validation and conversion
Struts Validator has those xml rules. In JSF you wire Validator and Converter classes or use built ins.
What is the simplest example
public class EmailValidator implements javax.faces.validator.Validator {
public void validate(FacesContext ctx, UIComponent comp, Object value)
throws ValidatorException {
String email = String.valueOf(value);
if (!email.matches(".+@.+\\..+")) {
throw new ValidatorException(
new FacesMessage("Please enter a valid email"));
}
}
}<h:inputText value="#{profile.email}">
<f:validator validatorId="emailValidator"/>
</h:inputText>Register the validator id in faces config and you are done. The page stays clean and the error shows up via h:message.
Tiles to Facelets
Tiles got us common layouts with definitions. Facelets does the same with less ceremony and much better templating.
How do we express a layout
<!-- template.xhtml -->
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets">
<h:panelGroup layout="block"><ui:insert name="header"/></h:panelGroup>
<h:panelGroup layout="block"><ui:insert name="content"/></h:panelGroup>
<h:panelGroup layout="block"><ui:insert name="footer"/></h:panelGroup>
</ui:composition>
<!-- page.xhtml -->
<ui:composition template="/WEB-INF/templates/template.xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets">
<ui:define name="content">
<h:outputText value="Hello Facelets"/>
</ui:define>
</ui:composition>If you are not ready for Facelets yet, you can still use JSP includes and keep moving. Do not block the migration waiting for the perfect view engine.
EL, JSTL, and when things run
JSTL runs before JSF. That surprises folks in the first week. Use the rendered attribute and keep JSTL for static bits.
What mistake bit us
<%-- This fails because c:if runs too early --%>
<c:if test="#{bean.show}">
<h:inputText value="#{bean.name}"/>
</c:if>
<%-- Do this instead --%>
<h:inputText value="#{bean.name}" rendered="#{bean.show}"/>State and redirects
JSF keeps a view state across requests. That reduces boilerplate but grows the page size. On big forms set state to server.
How do we flip the switch
<context-param>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<param-value>server</param-value>
</context-param>To avoid resubmits, return an outcome with redirect in faces config as shown above. It behaves like the Struts post redirect approach we used for years.
Messages and i18n
JSF can load a bundle once and make it available as bundle on every page. That cuts down on fussy includes.
What is the quick setup
<application>
<message-bundle>com.app.i18n.Messages</message-bundle>
</application><h:outputText value="#{bundle.welcome}"/>Libs and servers that worked
We had good results with MyFaces 1.1 on Tomcat 5.5 and with the Sun JSF RI as well. Tomahawk gives handy components like file upload and calendar.
What jars did we actually ship
faces api, faces impl or myfaces core, tomahawk if you want extra components, commons collections and friends. Keep your classpath tidy or you will chase class not found errors all night.
Ajax on top
The Ajax wave is real. You can sprinkle Prototype or Scriptaculous on JSF pages or try Ajax4jsf to bind events to server actions with less JavaScript.
Where is the safe line
Keep rules simple. Submit small areas. Avoid full page rerenders. If the form is huge, regular posts are still faster and less fragile.
Testing the move
For Struts we used StrutsTestCase. For JSF today we lean on Selenium or HtmlUnit to click and assert. Backing beans should be thin and push logic into services that you can unit test with JUnit.
What do we watch in CI
Page weight with client state. Bundle keys missing. Navigation cases that trap users on the same page with no message. These show up early if you put smoke tests in the build.
Migration flow that keeps sanity
Peel and replace one slice of UI at a time. Keep URLs stable. Keep Struts filters and security in place while JSF grows in a new folder like /faces or with the .jsf suffix.
Which screens should go first
Pick forms with repeatable controls and validation pain. Search forms, wizards, and admin pages usually pay back fast with JSF components.
Pitfalls we hit
Action classes that hide real business rules. Move that code into services before you migrate or you will just rename the mess. EL conflicts with JSP taglibs if the same prefix is reused. Keep namespaces clear.
What about SEO and bookmarks
If URLs matter, keep the same path and return redirect outcomes so the address bar shows the target page. Avoid view ids with random query strings. JSF can be friendly to crawlers if you keep links as real anchors and not only buttons.
Quick checklist to ship
Routing Struts and JSF side by side with clean mappings.
Beans Request scope by default. Session only for user stuff.
State Server state for big forms. Redirect after post.
Tags Replace Struts tags with h and f tags screen by screen.
Templates Move Tiles to Facelets when the team is ready.
Validation Use JSF validators and converters, keep messages in a bundle.
Testing Selenium for flows, JUnit for services.
Where does this leave Struts
Still fine for straight forms. But if you need components, richer widgets, and less glue code, JSF is a solid next step. The tool support that Sun and IDEs are putting out makes it feel like the center of gravity is moving.
Compact take away
Migrating Struts to JSF works best as a steady walk, not a sprint. Run both, swap tags, move logic into services, manage state on the server, and test the flows that make money.
Do that and you get cleaner pages, fewer custom tags, and a UI stack that fits the way we build web apps now. The path is clear and the lessons will age well.