分享

Enterprise Java Community: Building on Struts for Java 5 Users

 jianjun0921 2006-04-19
April 2006

Discussion


Introduction

Struts is undoubtedly the most successful Java web development framework. It is the first widely adopted Model View Controller (MVC) framework, and has proven itself in thousands of projects. Struts was ground-breaking when it was introduced, but the web development landscape has changed a lot in the last few years. It is also fair to say that Struts is not universally popular, and has in many ways been overtaken by other frameworks.

Developers not satisfied with Struts have tended to embrace other frameworks, or even create their own. This is even the case within the Struts community. The two major initiatives - the integration with WebWork and the JSF-based Shale - are producing frameworks quite different from the Struts to which most users are accustomed.

A less well known fact is that the increasing adoption of Java 5 has brought in a third way forward for Struts. In this article I introduce Strecks, a set of Java 5 based extensions which offer a greatly streamlined programming model for Struts applications, without introducing any new compatibility issues. If you‘re confused about the name, think of it as a shortened, misspelled version of "Struts Extensions for Java 5".

Strecks is built on the existing Struts 1.2 code base, adding a range of productivity enhancing features, including:

  • pure POJO actions
  • action dependency injection
  • action controllers encapsulating request workflow
  • action interceptors
  • form validation using annotations
  • data conversion and binding using annotations
  • pluggable navigation

These are discussed in more detail in the rest of the article.

Features

Strecks aims to simplify, not replace, the existing Struts programming model. Familiar artifacts such as Actions and ActionForms are still present, in an enhanced form. Strecks actions and forms can be used seamlessly within existing applications. The user interface or view layer of the framework has not been altered, and neither has configuration; there are no new custom tag libraries to speak of, with only a couple of useful tags added.

Strecks enhancements chiefly affect Struts‘s controller layer, which consists of ActionForms, Actions and a controlling RequestProcessor. Let‘s look at form-handling enhancements first, starting with form validation.

Form Validation

A requirement for virtually any application which accepts user input is form validation. Few would argue that programmatic validation code is tedious to write. An example of Struts 1.2 programatic validation, taken from our example application, is shown below:

public ActionErrors validate(ActionMapping mapping, HttpServletRequest request)
{
ActionErrors errors = new ActionErrors();
if (days == null)
{
ActionMessage error = new ActionMessage("holidaybookingform.days.null");
errors.add("days", error);
hasError = true;
}
else
{
if (!GenericValidator.isInt(days))
{
ActionMessage error = new ActionMessage("holidaybookingform.days.number");
errors.add("days", error);
hasError = true;
}
}
//validation for other fields omitted
if (!hasError)
return null;
return errors;
}

All of the above code is required simply to validate a single field, that is, to determine that the number of days supplied in our holiday booking entry form has been supplied in the correct Integer format.

Struts offers an alternative XML-based validation mechanism in the form of Jakarta Commons Validator. However, the format is regarded by many as verbose, and there has been a shift in recent years away from the use of XML in this way.

Strecks offers a concise annotation-based alternative, which we see in action below:

private String days;
@ValidateRequired(order = 3, key = "holidaybookingform.days.null")
@ValidateInteger(key = "holidaybookingform.days.number")
public void setDays(String days)
{
this.days = days;
}

Your form class is still a subclass of ActionForm; it is not exposed to the framework‘s mechanism for implementing the validation. This all takes place behind the scenes. Also, you can also implement programmatic validation logic in the form‘s validate() method.

Type Conversion and Data Binding

One problem when working with domain models in Struts applications is the need to write data binding and type conversion code - the framework provides very limited support for this. A practical limitation with Struts 1.2 is that action forms generally need to use String properties. Consider a form field in which the user is expected to enter a number. If a user enters "one", conversion will fail. The problem is when the form is redisplayed, the value 0 will be displayed, and not "one", an unacceptable usability scenario. The workaround is to use String-based form properties, at the price of having to do type conversion and data binding programmatically.

An example of a type conversion code and form property to domain model in Struts 1.2 is shown below, for just the Date field in the example application‘s form:

public void readFrom(HolidayBooking booking)
{
if (booking != null)
{
if (booking.getStartDate() != null)
this.startDate = new java.sql.Date(booking.getStartDate().getTime()).toString();
}
}
public void writeTo(HolidayBooking booking)
{
if (this.startDate != null && this.startDate.trim().length() > 0)
booking.setStartDate(java.sql.Date.valueOf(startDate));
}

Frameworks with declarative data binding and type conversion generally implement these features in the view to controller boundary. For example, JSF data conversions are defined in the view, with the target being managed bean properties. Strecks, by contrast, uses the ActionForm itself for type conversion and data binding. This has two benefits. Firstly, it allows the interface between the view and the form bean to remain very simple, no different from existing Struts applications. Also, it also allows type conversion and data binding behaviour to be unit tested very easily.

The Strecks type conversion and data binding features allow the previous example to be reduced as shown below:

private String startDate;
private HolidayBooking booking;
@BindSimple(expression = "booking.startDate")
@ConvertDate(pattern = "yyyy-MM-dd")
public String getStartDate()
{
return startDate;
}
//getters and setters omitted

The value of the form property is converted using the Converter identified using the @ConvertDate annotation, then bound the the startDate property in the domain object HolidayBooking. No extra code is needed. Specifying a converter explicitly is not necessary for most basic types, such as Integer, Long and Boolean. Also, the converter class can be set explicitly as a @BindSimple attribute, in which case the separate @ConvertDate would not be necessary.

Both converter annotations and binding mechanisms can be added without additional configuration by using the Annotation Factory pattern. The table below describes the additional artifacts required for each:

Binding Mechanism Converter
Implementation Implementation of BindHandler Implementation of Converter
Annotation Annotation annotated with @BindFactoryClass Annotation annotated with @ConverterFactoryClass
Factory Implementation of BindHandlerFactory Implementation of ConverterFactory

Dependency Injection

In the web tier of a J2EE application, dependencies take many forms, ranging from application-scoped business tier services to request-scoped parameters and attributes. Effective dependency management is about getting to just the data and services your application needs, in a simple and robust manner. The recognition of the value of Dependency Injection in achieving this is among the most important developments in Java programming practice in the last few years.

Dependency management in a typical Struts 1.2 application is somewhat out of step with these developments, for a number of reasons:

  • There is no built-in support for dependency injection. Access to services either needs to take place via programmatic lookups or superclass hooks (such as Spring‘s ActionSupport class, which is used to simplify access to a Spring ApplicationContext).
  • The requirement for thread-safe Actions which cannot hold request-specific state in instance variables is an obstacle to effective dependency management. This is because request-specific state must be held in request or session attributes or passed to other methods as arguments.
  • Struts Actions are closely bound to the Servlet API, which makes unit testing harder than for actions with no framework or Servlet API dependencies.
  • There is no support for declarative type conversion for request parameters.

Consider the example below, which uses a Spring bean to retrieve HolidayBooking data for a given ID.

public class RetrieveBookingAction extends ActionSupport
{
public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request,
HttpServletResponse response) throws Exception
{
HolidayBookingService holidayBookingService = (HolidayBookingService) getWebApplicationContext().getBean(
"holidayBookingService");
long id = Long.parseLong(request.getParameter("holidayBookingId"));
HolidayBooking holidayBookings = holidayBookingService.getHolidayBooking(id);
request.setAttribute("holidayBooking", holidayBookings);
return mapping.findForward("success");
}
}

Notice how the service bean is obtained using a programmatic hook. The request parameter ID is converted programmatically using Long.parseLong().

Strecks has a simple mechanism for dependency injection, also based on Java 5 annotations. Action beans (the replacement for Actions which discussed in more detail later) are instantiated on a per-request basis, allowing dependencies to be injected via setter method annotations.

The Strecks equivalent to our previous example is shown below. Notice how the execute() method no longer has multiple method arguments, and is much shorter than the Struts example.

@Controller(name = BasicController.class)
public class RetrieveBookingAction implements BasicAction
{
private HolidayBookingService holidayBookingService;
private WebHelper webHelper;
private long holidayBookingId;
public String execute() { HolidayBooking holidayBookings = holidayBookingService.getHolidayBooking(holidayBookingId); webHelper.setRequestAttribute("holidayBooking", holidayBookings); return "success"; }
@InjectSpringBean(name = "holidayBookingService") public void setService(HolidayBookingService service) { this.holidayBookingService = service; } @InjectWebHelper public void setWebHelper(WebHelper webHelper) { this.webHelper = webHelper; } @InjectRequestParameter(required = true) public void setHolidayBookingId(long holidayBookingId) { this.holidayBookingId = holidayBookingId; } }

Dependency Injection in Strecks is supported through the InjectionHandler interface, which has the following form:

public interface InjectionHandler
{
public Object getValue(ActionContext actionContext);
}

ActionContext is simply an interface which simply wraps the HttpServletRequest, HttpServletResponse, ServletContext, ActionForm and ActionMapping. In other words, InjectionHandler supports retrieving data or services accessible via any one of these objects. In addition to the injectors shown in the example above, injectors are available for request, session and application context attributes, the Locale, MessageResources, the ActionForm, the ActionMapping, as well as other objects.

Adding your own injection handlers is simple, again using the Annotation Factory pattern:

  • create an InjectionHandler implementation which returns the object you wish to inject.
  • define an annotation which can be used to identify and configure the InjectionHandler.
  • create an InjectionHandlerFactory which can be used to read the annotation and create an InjectionHandler instance.

Actions

Actions are the key artifacts in a Struts application because they are the primary home for service tier invocations as well as the application‘s presentation logic. In the previous section, we discussed briefly the need for thread-safety in actions. This, together with the presence of a fairly predefined inheritance hierarchy, makes reuse of request processing logic quite difficult.

Strecks offers a simple solution to these problems. The responsibility of a traditional Struts action is divided between two objects, an action controller and an action bean.

The idea behind the action controller is that common request processing logic can be abstracted and reused across many actions. For example, simple CRUD web applications typically use three types of request handling logic:

  • read-only actions whose job is simply to retrieve data from a service tier object and make it available to the view for display.
  • form rendering actions, which may additionally need to populate a form with data from a previously persisted domain object.
  • form submission actions, which need to receive data submitted via a form and invoke the relevant service tier operations.

With request processing logic abstracted into a controller, all that remains is to implement the domain-specific aspects of each operation. This is the job of the action bean.

In Strecks, the action bean is registered in struts-config.xml exactly as if it were a regular Action. When used for the first time, an annotation on the action bean‘s class definition is used to identify the controller. An instance of the controller is created. The controller, which extends Action, is held in the action cache for subsequent requests. Each time a request is received, the controller creates a new instance of the action bean. The action controller itself holds no state, and is shared among multiple requests.

Lets take a look at an action bean implementation. SubmitEditBookingAction, shown below, is invoked when submitting an update from the "Edit Holiday Booking" form.

@Controller(name = BasicSubmitController.class)
public class SubmitEditBookingAction implements BasicSubmitAction
{
private HolidayBookingForm form;
private HolidayBookingService holidayBookingService;
private WebHelper webHelper;
public void preBind()
{
}
public String cancel()
{
webHelper.setRequestAttribute("displayMessage", "Cancelled operation");
webHelper.removeSessionAttribute("holidayBookingForm");
return "success";
}
public String execute()
{
HolidayBooking holidayBooking = form.getBooking();
holidayBookingService.updateHolidayBooking(holidayBooking);
webHelper.setRequestAttribute("displayMessage", "Successfully updated entry: " + holidayBooking.getTitle());
webHelper.removeSessionAttribute("holidayBookingForm");
return "success";
}
//dependency injection setters omitted
}

The action bean uses BasicSubmitController, which itself mandates that its action beans implement the BasicSubmitAction interface. SubmitEditBookingAction does so by implementing the preBind(), cancel() and execute() methods. Lets consider cancel(), which is invoked when a user clicks on a button rendered using the <html:cancel/>; tag.

The key point here is that the logic for determining whether a form has been cancelled is implemented in the controller, not the action bean. We see how this happens by looking at the controller implementation below:

@ActionInterface(name = BasicSubmitAction.class)
public class BasicSubmitController extends BaseBasicController
{
@Override
protected ViewAdapter executeAction(Object actionBean, ActionContext context)
{
BasicSubmitAction action = (BasicSubmitAction) actionBean;
ActionForm form = context.getForm();
HttpServletRequest request = context.getRequest();
boolean cancelled = false;
if (request.getAttribute(Globals.CANCEL_KEY) != null)
{
cancelled = true;
}
if (form instanceof BindingForm && !cancelled)
{
action.preBind();
BindingForm bindingForm = (BindingForm) form;
bindingForm.bindInwards();
}
String result = cancelled ? action.cancel() : action.execute();
return getActionForward(context, result);
}
}

In a traditional Struts application, code for identifying a cancel event would need to be present in the Action class.

Use of an action controller in this way simplifies development of actions and allows for effective reuse of request processing logic. A number of action controllers are available out of the box, including form handling controllers as well as dispatch controllers which mimic the behaviour of DispatchAction, MappingDispatchAction and LookupDispatchAction. Creating controllers customised for AJAX operations, for example, would not be difficult.

Action Bean annotations

One of the most powerful Strecks features is the ability to add custom annotations to action beans. This feature allows the contract between the action bean and the controller action to be extended without changing the interface that the action bean must implement.

The mechanisms behind custom action bean annotations are already used by the framework for a number of purposes:

  • reading action bean dependency injectors
  • allowing pluggable navigation
  • support for dispatch method lookup for Streck‘s LookupDispatchAction equivalent
  • configuring the action bean source, that is, the mechanism used to instantiate an action bean

The same mechanism could be used to features currently not present, such as action bean-specific interceptors and action bean validate() methods.

Lets consider action bean source configuration. Using a single annotation class level annotation, it is possible to identify an action bean as "Spring-managed", as shown in the example below:

@Controller(name = BasicController.class)
@SpringBean(name = "springActionBean")
public class SpringControlledAction implements BasicAction
{
private String message;
private SpringService springService;
public String execute()
{
int count = springService.getCount();
message = "Executed Spring controlled action, count result obtained: " + count;
return "success";
}
public String getMessage()
{
return message;
}
//Don‘t need dependency injection annotation, because
public void setSpringService(SpringService service)
{
this.springService = service;
}
}

Its hard to imagine how providing this level of integration with Spring could be more simple. In the example, the action bean is instantiated per request by obtaining the bean named springActionBean from the Spring application context. Note the absense of the @InjectSpringBean annotation - the service dependency injection is handled within the Spring startup process.

A second example, shown below, involves an action bean handling submission of a form with multiple submit buttons. It in turn uses BasicLookupDispatchController, which supports the @DispatchMethod annotation for determining which method to execute.

@Controller(name = BasicLookupDispatchController.class)
public class ExampleBasicLookupSubmitAction implements BasicSubmitAction
{
private String message;
private MessageResources resources;
public void preBind()
{
}
public String execute()
{
message = "Ran execute() method in " + ExampleBasicLookupSubmitAction.class;
return "success";
}
@DispatchMethod(key = "button.add")
public String insert()
{
message = "Ran insert() method linked to key ‘" + resources.getMessage("button.add") + "‘ from "
+ ExampleBasicLookupSubmitAction.class;
return "success";
}
public String cancel()
{
message = "Ran cancel() method";
return "success";
}
@InjectMessageResources
public void setResources(MessageResources resources)
{
this.resources = resources;
}
}

The plain Struts equivalent would require implementation of a method returning a java.util.Map containing the message key to method name mappings.

Custom action bean annotations are supported through the use of the Annotation Decorator pattern, which works in a similar way to the Annotation Factory pattern.

Specifically, the controller must include a class level annotation which can be used to identify an ActionBeanAnnotationReader implementation. For example, BasicLookupDispatchController has the class definition which includes the @ReadDispatchLookups.

@ActionInterface(name = BasicSubmitAction.class)
@ReadDispatchLookups
public class BasicLookupDispatchController
extends BasicDispatchController
implements LookupDispatchActionController
{
// rest of class definition
}

The @ReadDispatchLookups identifies the DispatchMethodLookupReader as the annotation reader, which in turn simply looks for @DispatchMethod annotations in order to populate the message key to method name map. The implementation of ActionBeanAnnotationReader for handling pluggable navigation is more complex but uses the same basic mechanism.

As with validators, converters and dependency injectors, no XML is required to add custom annotations. All that is required are controller annotations to identify the ActionBeanAnnotationReaders, and the necessary annotations in the action bean implementation itself.

Interceptors

Most web applications require operations which are common to many actions, such logging, authentication and customized state management. In Struts 1.2 it is relatively difficult to accomodate these requirements in an elegant way. A Struts developer typically has two choices:

  • implement the common behaviour in RequestProcessor subclass
  • create a common base Action superclass which all of the application‘s actions should extend

While allowing duplication to be factored out, both solutions interfere with an inheritance hierarchy. Adding behaviour typically requires changing existing classes rather than simply adding a new class and appropriate configuration entry.

The solution, evident in frameworks such as WebWork and Spring MVC and also Strecks, is interceptors. Strecks defines two interceptor interfaces, BeforeInterceptor, and AfterInterceptor. A class which provides a simple implementation of both is shown below:

public class ActionLoggingInterceptor implements BeforeInterceptor, AfterInterceptor
{
private static Log log = LogFactory.getLog(ActionLoggingInterceptor.class);
public void beforeExecute(Object actionBean, ActionContext context)
{
HttpServletRequest request = context.getRequest();
log.info("Starting process action perform " + request.getRequestURI());
log.info("Using " + context.getMapping().getType());
}
public void afterExecute(Object actionBean, ActionContext context, Exception e)
{
HttpServletRequest request = context.getRequest();
log.info("Ended action perform of " + request.getRequestURI() + StringUtils.LINE_SEPARATOR);
}
}

Interceptors have access to the ActionContextand the same injected state as the action bean itself. BeforeInterceptors are run before any of the action bean methods execute but after action bean dependency injection has occurred. Neither BeforeInterceptors nor AfterInterceptor can affect navigation - this is the responsibility of the action bean and controller. BeforeInterceptors can interrupt action execution by throwing an exception. In this case, the neither the action bean methods nor the beforeExecute() method of remaining BeforeInterceptors will be executed.

By contrast, the afterExecute() of each AfterInterceptor is always executed. If an exception has been thrown during execution, this will be passed in to the afterExecute() method.

Each interceptor operates across all actions in a given Struts module. Action-specific interceptors are not present, although adding them is planned.

Navigation and View Rendering

Navigation and view rendering is another area in which Strecks provides subtle but powerful improvements over traditional Struts capabilities.

In Struts 1.2, the mechanism for navigation involves the action returning an ActionForward instance from the Action‘s execute() method. This works fine for simple JSP-based views.

Rendering using alternative view technologies (e.g. outputting of binary data, use of Velocity, etc.) is typically done in one of two ways in a Struts 1.2 application:

  • within the action itself, using calls to the ServletOutputStream or PrintWriter. In this case the action returns null to indicate that the response is complete.
  • by setting up a separate servlet to handle view rendering. This technique is used, for example, for rendering with Velocity or Freemarker.

In the former case, the mechanism is arguably rather crude, while the latter requires extra configuration, since the solution is not "built-in".

Instead of returning ActionForward, Strecks controller actions return a ViewAdapter instance. This is the starting point for more flexible navigation, because it allows for alternative view rendering implementations. For ActionForward-based navigation, the controller simply returns an ActionForwardViewAdapter, which wraps an ActionForward instance.

Strecks makes it particularly easy to handle what Craig McLanahan terms "outcome-based navigation", which is implemented in Struts using navigation mapping entries in struts-config.xml, retrieved using ActionMapping.findForward(result). In the action bean example below, ActionMapping.findForward() will automatically be called with the success passed in as the argument.

@Controller(name = BasicController.class)
public class ExampleOutcomeAction implements BasicAction
{
public String execute()
{
//add processing here
return "success";
}
}

The same result can also be achieved with the @NavigateForward annotation:

@Controller(name = NavigableController.class)
public class ExampleOutcomeAction implements NavigableAction
{
public void execute()
{
//add processing here
}
@NavigateForward
public String getResult()
{
return "success";
}
}

So why there are two navigation mechanisms available for the same end result? The answer is the first gives you convenience if you want to use outcome-based navigation and are happy for this to be built into the action bean‘s interface. The second gives you flexibility. The second example could be changed to handle navigation by returning an ActionForward from the action bean itself:

@Controller(name = NavigableController.class)
public class ExampleOutcomeAction implements NavigableAction
{
public void execute()
{
//add processing here
}
@NavigateForward public ActionForward getResult() { return new ActionForward("/result.jsp"); }
}

In other words, the navigation mechanism is independent of the NavigableAction interface which the controller uses to talk to the action bean. A different navigation reader and annotation could easily be used, again with the help of the Annotation Factory pattern, to plug in a completely different view rendering mechanism.

This brings us back to why the controller returns ViewAdapter, and not ActionForward. Instead of returning an ActionForwardViewAdapter, the controller could return a RenderingViewAdapter instance. RenderingViewAdapter extends ViewAdapter and defines the method:

public void render(ActionContext actionContext);

Of course, render() could be implemented in lots of different ways. One rather tempting option (not implemented yet) is to use this interface as an adapter for the Spring MVC View and ViewResolver interfaces. In a simple stroke, we could tap in the full functionality of Spring MVC‘s support for alternative view technologies, probably the most powerful feature of this framework.

Strecks also provides additional navigation handling utilities in two areas:

  • it provides extra APIs which makes it very easy to implement the Redirect After Post pattern
  • it explicitly supports the use of a page class. A page class in Strecks is a Java class which is directly coupled to a JSP. Its sole purpose is in assisting with the rendering of the page. Using a page class can make JSPs much more maintainable, because markup which would otherwise need to be generated using nested and conditional tags can be generated more simply in a Java class and accessed via simple JavaBean properties.

Who Should Use Strecks?

The article has hopefully shown you that Strecks is a powerful framework which can transform the way you write Struts applications. It has potential to be an excellent solution for organisations which have:

  • invested in Struts, either in existing applications and developer training
  • have moved to Java 5 and want to take advantage of its powerful features
  • want a framework which supports modern software development best practices, such as ease of testability, use of design patterns, good OOP and dependency injection
  • want these features now, without the pain of a major migration

Conclusion

Strecks‘s aim has been to:

  • match the simplicity of Struts
  • match the validation and type conversion power of JSF and Tapestry
  • match the flexibility of Spring MVC and WebWork

Strecks has been open sourced under the Apache License, version 2. API changes between now and the first 1.0 release are not expected to be significant. We encourage you to download the Strecks and try it out today. Feedback would be welcome.

About the Author:

Phil Zoio is an independent Java and J2EE developer and consultant based in Suffolk in the UK. His interests include agile development techniques, Java open source frameworks and persistence. He can be contacted at philzoio@realsolve.co.uk.

PRINTER FRIENDLY VERSION

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多