April 2006 DiscussionIntroductionStruts 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:
These are discussed in more detail in the rest of the article. FeaturesStrecks aims to simplify, not replace, the existing Struts programming model. Familiar artifacts such as Strecks enhancements chiefly affect Struts‘s controller layer, which consists of Form ValidationA 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 Type Conversion and Data BindingOne 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 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 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:
Dependency InjectionIn 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:
Consider the example below, which uses a Spring bean to retrieve 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 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 @Controller(name = BasicController.class) public class RetrieveBookingAction implements BasicAction { private HolidayBookingService holidayBookingService; private WebHelper webHelper; private long holidayBookingId; Dependency Injection in Strecks is supported through the public interface InjectionHandler { public Object getValue(ActionContext actionContext); }
Adding your own injection handlers is simple, again using the Annotation Factory pattern:
ActionsActions 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:
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 Lets take a look at an action bean implementation. @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 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 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 Action Bean annotationsOne 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:
The same mechanism could be used to features currently not present, such as action bean-specific interceptors and action bean 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 A second example, shown below, involves an action bean handling submission of a form with multiple submit buttons. It in turn uses @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 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 @ActionInterface(name = BasicSubmitAction.class)
@ReadDispatchLookups
public class BasicLookupDispatchController
extends BasicDispatchController
implements LookupDispatchActionController
{
// rest of class definition
}
The 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 InterceptorsMost 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:
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, 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 By contrast, the 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 RenderingNavigation 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 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:
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 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 @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 @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 @Controller(name = NavigableController.class) public class ExampleOutcomeAction implements NavigableAction { public void execute() { //add processing here } In other words, the navigation mechanism is independent of the This brings us back to why the controller returns public void render(ActionContext actionContext); Of course, Strecks also provides additional navigation handling utilities in two areas:
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:
ConclusionStrecks‘s aim has been to:
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. |
|
來(lái)自: jianjun0921 > 《Struts》