Little did anyone know, when the first few lines of code were committed to the Struts CVS repository at Apache in June 2000, that a revolution was brewing. Prior to that time, there were few useful models for best practices for the architecture of web based applications. The best we could do was handwaving about the relative advantages of "Model 1" versus "Model 2" approaches.
The original implementation of Struts, which was released as a 1.0 product approximately one year later, changed all that. As more and more people came to understand the advantages of building on top of a stable and supported framework, and as more and more developers adopted it for their own application development, and as more than more books helped everyone understand how to use the framework correctly, and as more and more development tools provided support for building Struts based applications, the world changed. A small open source project became a defacto industry standard that, even today, gets downloaded many tens of thousands of times per month.
But, that was then ... and this is now. In the four years that Struts has been around (a relatively infinite amount of "Internet time"), vastly improved technologies have become available from many talented architects and designers. Moore's Law has continued its seemingly inexhaustible progress. Developers have grown in their ability to understand less monolithic architectures -- as well as developing preferences towards agility, the ability to create unit tests, and to build applications by composition rather than inheritance.
One of the critical success factors for Struts has been, and continues to be, an obvious commitment (on the part of Struts developers) to maintain and enhance the framework in a way that remains fundamentally backwards compatible, while embracing new technologies as they have become availalble. This has led to Struts being both praised (for protecting the investment of developers with thousands of applications critically dependent on the framework) and dissed (for being a dinosaur compared to all the "latest and greatest" favorite technological approaches). History has shown (in terms of continued popularity) that this is a good strategic approach.
Indeed, Struts will continue to evolve in a manner that protects the investments of existing users. For example, the recent discussions around using the "chain of responsibility" design pattern inside the Struts controller to radically simplify customization and extension of the framework, while at the same time providing historically consistent default functionality, is just one example of this trend. But ... it's also time for something else.
It is time to harvest many of the great ideas that have matured in the last four years. It is time to focus Struts on solving the unsolved problems, while leveraging existing solutions instead of reinventing them. It is time to answer the question "if we knew then what we know now, what would Struts have looked like?"
Thus, this is a proposal to fundamentally reinvent Struts ... to bring the framework up to date. To focus on the key value add of a framework that focuses on the "controller" tier of the traditional Model-View-Controller design pattern, while expanding the areas of responsibility that the controller assumes, but at the same time radically simplifying the use of the overall framework -- and lets you choose the portions that you care about, rather than being monolithic. To bring forth ... a proposal for a future of Struts, code named the "Shale" proposal.
EDITOR'S NOTE: Why "Shale"? As others have pointed out, the cultural rules of engagement at Apache encourage both evolution and revolution in software designs. Revolutions are typically assigned code names while they are still under discussion, and only gain access to the branding of the overall project once they are accepted. Other proposals for Struts 2.x have talked about tearing down the walls inside the framework, and those are Good Things. This proposal, on the other hand, suggests that we fundamentally divide the notion of a web application framework into solid individual layers, much as we see geologically in shale deposits around our volcanoes and coastlines. Each layer of the framework should focus on the specific requirements that are relevant to that layer -- and use of one layer should not necessarily require the use of all the rest (although it's certainly reasonable for synergies to exist if all the layers are chosen :-). Hence, "shale."
But first, let's examine some problems with the current architectures of web application frameworks in general, and Struts 1.x in particular.
Most current implementations of a Model-View-Controller architecture for web applications have focused on providing a single monolithic controller that is responsible for the entire processing of every incoming request. While this paradigm is very powerful, it creates usability and understandability problems for some application developers, because it requires artificial separation of portions of the application logic that (from the viewpoint of the developer) belong together. The most common scenario in a Struts-based application, for example, is the following pattern:
A key issue is that, from the point of view of the person supporting a particular business transaction, the following pieces of functionality are tightly coupled:
Yet in Struts 1.x, for example, the setup logic and processing logic end up in two different Actions, requiring multiple action mappings, and a tendency for people to implement action chaining and be surprised by the results. A more developer-friendly approach would be to combine the tightly coupled processing actions into a single Java class, implicitly associated in a 1:1 relationship by a (pluggable) mapper, so that the entire combination has a single "identity" from the point of view of the application framework -- and so that this entire combination can be treated as a unit by development tools that choose to support it.
One of the most significant technological challenges of developing applications based on HTTP is dealing with the stateless aspect of the protocol. The servlet API provides a relatively simple mechanism for identifying requests that are coming from the same user (sessions). In turn, this allows convenient state saving of information particular to an individual user's interactions with the application. But it doesn't solve all of the problems.
In particular, it is common for individual transactions to require more than one interaction with a user. The servlet API offers no direct support for maintaining state across these requests that is specific to that transaction -- let alone deal with realities like the back button on a browser, which can lead to incorrect interactions. Some frameworks do more than others to manage this problem. In Struts 1.x, for example, you can use the transaction token mechanism to catch the "duplicate submit" scenario. But a more comprehensive solution go "guided dialog" transactions, with the associated state management, would substantially improve the productivity of developers utilizing the framework.
It may seem, from the above problem descriptions, that an application-level controller has no reason for being any longer. Yet, nothing could be further from the truth. There are still useful features that can be enabled by processing on every incoming request. In Struts 1.x, we have required all transactions to flow through a processing servlet -- because we have used Servlet 2.2 as the base platform, we were not able to implement these features using servlet filters.
Moving to a Servlet 2.4 baseline provides us the opportunity to invoke only the required per-every-request features that a particular application requires, without requring a monolithic implementation. In addition, event listeners (at the request, session, and application) levels will be important, for example, for implementing end-of-request cleanup operations, or detect when request attributes of a particular type are registered, so that value add services may be provided transparently.
Beyond this, however, modern application frameworks are expected to provide standard services (or supported plugins to standard services) out of the box. Struts needs to offer such solutions in areas that are particularly important, yet currently require individual integration by application developers.
The briefest way to state the proposed architecture is to fulfill the following goal statement:
Divide the responsibilities of the monolithic Struts 1.x controller into individual layers, whose features may be composed in appropriate combinations based on the requirements of a particular application.
To that end, the following primary layers are proposed:
The features provided by each of these layers are further described in the following sections.
There continue to be framework responsibilities that are best discharged
by preprocessing (and/or postprocessing) every request, or every request that
meets certain criteria. Shale will leverage the Filter
APIs of
the servlet container to provide mechanisms to plug in individual application
level functionality for purposes such as:
application/x-www-form-urlencoded
)Depending upon the nature of the individual pieces being integrated, it
might be more user friendly to configure a single Filter
that
used a customizable "chain of responsibility" design pattern (like that
provided by Commons Chain) to implement the actual behavior performed for
each incoming request.
In the specific case of authentication and authorization, Shale must
interoperate both with applications wishing to utilize container managed
security, as well as those providing their own technology. In both cases,
it is expected that the outcome of performing authentication and
authorization activities will be made visible to the application via return
values of the (potentially wrapped) servlet/portlet request methods
getRemoteUser()
, getUserPrincipal()
, and
isUserInRole()
.
Separate from the core of Shale, we should consider supporting a subproject that provides functionally complete plug-ins that implement functionalities of the types described above. In particular, a complete user administration system (with support for "remember me" cookies) would be a popular feature useful in many application environments.
Nearly all modern MVC-based frameworks for web application development provide some level of support for the "application controller" functionality described in the previous section, as well as the "view controller" capabilities described next. However, relatively few of them deal with an intermediate requirement to manage a "dialog" or "conversation" with the application user that spans multiple HTTP requests. Dealing with this complexity is left as an "exercise left for the reader" -- a state of affairs that Struts 2.x should change.
Although this portion of the "Shale" proposal is the least fleshed out in terms of an thinking through a possible implementation approach, it offers an opportunity for substantially improving the productivity of developers building applications based on Struts. Therefore, let us define the requirements that must be met by an implementation of a controller for dialogs:
Shale will support a mechanism that provides a 1:1 relationship between a view tier presentation technology responsible for creating an HTTP response (such as a JSP page), and a corresponding Java class containing event handling logic, (optionally) values used in the dynamic rendering of the response, and (optionally) bindings to the individual user interface components included in the response page. Such a Java class is known (in JSF terminology) as a "backing bean," and will in most circumstances be registered as a managed bean in the JSF configuration resources.
JSF does not require that a backing bean implement any particular interface, or extend any particular base class. Therefore, Shale should not impose any such restriction either. It should merely promise to ensure that a bean of the appropriate class (selected by a pluggable mapper that translates the JSF view identifier into a class name) will be present -- normally in request scope -- when needed to process an incoming form submit, or to render a newly created view.
However, if an application's backing bean happens to implement a lightweight
ViewController
interface defined by Shale, several lifecycle
related methods will be called by the framework:
DialogController
instance
for the dialog that this view is a participant in, if any.encode()
methods of the components have been called). This method is only
called for a view that is actually going to be rendered, in the
case where the view being rendered is different from the view that
responded to a form submit. As such, it is a useful place to acquire
references to model data that is required in the rendering of
this view.ViewController
s that have had their
init()
method called during this request.Other than implementing a Shale-defined interface, a
ViewController
need not have any other dependence on the
framework. Thus, it remains easy to unit test such a bean outside of
a servlet container environment.
When coding the action method corresponding to a form's submit button, Shale should encourage best practices in terms of delegating business logic to the model tier, rather than implementing it directly in the action method. One approach to this might be to provide helper methods that make it easy to encapsulate the current request state (including the current instance of the backing bean, which would include all the incoming input field values) into a context that is then passed to a "chain of responsibility" implementation such as Commons Chain for performing the actual business logic processing.
Best practices for JSF include storing backing beans in request scope, rather than in page scope. Because such beans are instantiated anew for each request, and are accessed only by a single thread, there are no thread safety concerns (as there are with Struts 1.x Actions) related to instance variables. However, this also means that state required across more than one request needs to be stored elsewhere (because the backing bean will be thrown away at the end of the request).
Compared to Struts 1.x, support for client side validation and layout management (Tiles) are explicitly excluded from the core controller framework. This is not based on any belief that the functionality is not valuable -- it belongs properly in the view tier, and should be managed there as a separate subproject.
A key aspect of designing a framework is to choose which features it implements itself, and which capabilities it imports as dependencies. The "Shale" proposal contemplates the following dependency choices, with individual implementations proposed for some of them. Shale will then provide its own APIs in the controller tier that leverage these capabilities.
Proposal: JDK Version 1.4
Picking a base J2SE platform is a tradeoff between wanting to use all the latest and greatest features of the most recent platform (for the purposes of this discussion, JDK 5.0) and the realities of how many developers will have the possibility of deploying applications based on that platform when we release the initial version of Shale. Currently, Struts 1.x requires a minimum of JDK 1.2 (because it uses the collection classes extensively).
JDK 1.4 represents a slightly aggressive choice given the current state of the market, but relies on a reasonable assumption that JDK 1.4 will become widely deployed over the next 12-18 months (in particular because JDK 1.4 is a required baseline for the upcoming generation of J2EE 1.4 platform application servers). The key technological reasons for this choice (over a more conservative choice of JDK 1.3) include:
Proposal: J2EE Version 1.4 Platform APIs
For Struts, the most important platform API is the Servlet Specification. Although Servlet 2.4 (the version included in J2EE 1.4) was fairly modest in the scope of its changes, several of them are important to the architecture of a web application framework:
Proposal:: JavaServer Faces 1.1
Prior to the development of JavaServer Faces, web application frameworks were on their own in terms of supporting mechaisms for user interface components and the corresponding request processing lifecycle. The standardization of JSF provides an opportunity to build on top of a functionally capable API that is slated to become part of the J2EE 5.0 platform, and which provides useful (from the viewpoint of a framework) functionality in many areas beyond just the user interface components themselves, such as:
Without JSF, we would likely have to reinvent much of this functionality (or leverage the similar, but more primitive, implementations of these ideas in existing Struts code or other framworks). With JSF, we can leverage the extensibility of the basic framework to enable the division of controller responsibilities at the view (page), dialog, and application level that is the key feature of the entire proposal.
Struts should remain agnostic about technologies to be employed for representing business logic, model data, and persistence. However, functional examples should be provided that encourage the use of best practices in application design.
Inversion of Control (IoC) containers (the techniques are also referred to as Dependency Injection) are becoming a popular mechanism for assembling the required services and logic of an application. If Struts included such a framework, it would provide a solid basis for building maintainable apps, as well as allowing the framework to configure itself using the same capabilities.
Rather than building such a container ourselves, we should seek to incorporate an existing one that is license-compatible and which can be integrated into the JSF managed beans facilities (so that value binding and method binding expressions can leverage the facilities of this container transparently). From my research so far, I like Spring's capabilities in this area the best, but am open to other suggestions.
In order to support reasonably complete solutions for applications that wish to provide their own authentication and authorization services (as well as interact with container managed security), we need APIs available for performing user registration, implementing "remember me" features, and represent the results via a wrapped request (so that apps depending on getRemoteUser(), getUserPrincipal(), and isUserInRole() will still work). Using JDK 1.4 as a base platform would allow us to integrate mechanisms like JAAS. Other alternatives include plugins like SecurityFilter.
The proposed solutions for dialog management require a framework for managing state across multiple HTTP requests. This is likely to be an area where considerable investigation is needed before picking a fundamental technology. Things that seem feasible based on my initial review are:
Proposal: Commons Logging
Logging is an important feature of both an application framework itself (to help developers understand the dynamic behavior of the framework) and for building high quality applications. If the proposal for JDK 1.4 is accepted, we could potentially mandate using the java.util.logging APIs; however, many Struts users prefer to use alternative logging implementations such as Log4J. Commons Logging provides a portable adapter layer that allows the use of different logging implementations under the covers.