Java frameworks span everything from the simplistic to the entirely comprehensive. Some frameworks, for example frameworks concentrating on user interface development, web applications, or persistence, are tightly focused. Others take a more “holistic” approach, providing benefit to many different areas of an application’s development efforts. It is these “whole application” frameworks I’ll concentrate on here.
The best frameworks do exactly what a “framework” is defined to do: Provide a basic conceptional structure (as of ideas) or a skeletal, openwork, or structural frame on which you can construct your application.
This article will touch on a few frameworks available as open source, and observe some of the techniques and patterns used when choosing frameworks and putting frameworks to use.
An application framework is a tool that provides both structure and services to your application. Unlike an IDE or compiler, a framework usually will be deployed along with your application. Exactly what the structure looks like, or what the services are, depends on the particular framework. Some concentrate more in one area than in others; some provide a “soup to nuts” array of services ranging from user interface connectivity through to persistence and database access.
A good application framework provides proven and reliable design patterns and code architecture to an application, increasing development productivity manyfold. Frameworks grew out of a realization that many common design patterns could have a single implementation, and that implementation could be used on multiple projects.
As open source Java grew in popularity, many application problems turned out to have common solutions. These solutions began to be abstracted into design patterns—ways of doing certain things that had been tried and found to be successful. As implementations of these patterns were saved and used again, code re-use began to grow between projects, and the most common solutions were soon grouped together into the first frameworks.
Although an API library provides for a level of re-use, frameworks provide more structure than an API library does, and usually incorporate a number of different APIs, each providing a specific set of services to your application.
As component-based development began its rise in popularity, new frameworks were written (and existing frameworks were extended) to become the component execution environment and the inter-component communication mechanism—in a word, the component “container” of choice.
Many such frameworks were created; a few were released as open source, and gained popularity slowly. A number of reasons led to the slow initial adoption of frameworks. Many development shops had a bad case of “not invented here” syndrome. Many more were slow to understand the correct use of design patterns and components. Both attitudes impeded the early adoption of frameworks, often causing an organization to re-invent services and functionality already freely available.
Paradoxically, reductions in the budgets available for development has speeded up the growth of frameworks. As developers were forced to do more with less, they looked to re-use and application of standard components as solutions—and frameworks were there to provide them. No longer did developers have the luxury of re-inventing solutions to common problems many times—they were forced to consider re-use as a higher priority, and frameworks are ideal examples of re-use at its best.
Frameworks and Open Source
Frameworks are a good fit for the open source model in that the more they are used, the more flexible they tend to become, assuming the initial design was good.
Often, a framework is built as the result of an iterative process: As projects that have some common elements are developed, reusable components are created to reduce the need to continually re-create typical functionality. As these components are re-used and made more generic and flexible, they become the backbone of a framework (or become components to be run in an existing framework).
Many frameworks evolve to encompass services for the entire range of needs of an application. Open source means that a framework of this type will be extended and new services added by a number of different developers, creating services and capabilities that a single group might not ever have time to build for themselves, and increasing the utility of the framework for everyone.
Types of Frameworks
Frameworks designed for general application development often provide more capability in particular areas of the application development process. For example, the Apache Struts framework provides a lot of functionality in the area of user-interface building, whereas a framework like Spring doesn’t touch the user-interface area much at all.
Some frameworks take a more pattern-oriented approach, supplying little of their own code; instead, they concentrate on the structure and overall architecture of an application, providing in essence a “backbone” into which other components can be connected. Often, a framework is broken into several sub-projects, each providing functionality for a particular area, such as user interface, persistence, scheduling, and so forth. Ideally, these sub-projects should be both optional and replaceable, giving the maximum flexibility to the developer.
The exact line between a framework and a project that provides functionality such as persistence or user-interface building is hard to draw sometimes. Generally, a framework will provide functionality able to build an entire application, even if it is on one area.
Some frameworks are created for industry-specific purposes, such as frameworks for building accounting solutions. These tend to be less widely re-usable, and are less often released in open source.
Another distinctive attribute of frameworks is the way they are incorporated into your own projects. Many other tools provide support to your project during development, but are generally not a part of your deployed application. Even the virtual machine, although of course essential to the deployed application, is not tied to it—your application should be able to be moved to any other compliant virtual machine and run just as well. This isolation is not necessarily the case with frameworks: A framework’s code is usually deployed as an integral part of your finished application. This makes the choice of framework all the more important because your dependence on it does not end when the development is complete; you must be able to rely on its continued operation throughout the lifetime of your application. As with any relationship requiring a high level of commitment, it is best to proceed with your eyes open!
Some developers question the usefulness of frameworks in a J2EE environment, believing instead that J2EE itself provides all of the services required. Although it is true that the J2EE platform provides a highly valuable set of tools, services, and APIs, it is not a substitute for a well-crafted framework. Most modern frameworks work should allow the construction of applications with or without the full J2EE infrastructure, particularly the EJB portion of the specification. EJBs are not the only distributed component model, nor are they in all circumstances the best choice.
A framework, then, should augment and make use of J2EE, building on its strengths and addressing its weaknesses, thereby allowing the developer to make the appropriate choices of technologies for any given project.
A framework flows over into your deployment and maintenance phases of your project; it also is involved in design. Given the reliance of frameworks on design patterns, it makes sense that those same patterns can be used even in the design stage, while your application is still being worked out. Indeed, it makes little sense to settle on a framework and not use its design patterns in your project; it will make implementing in that framework more difficult when the time comes. A good framework can lend a certain amount of “paint-by-numbers” to your design process: You know what’s available, and can work from there to decide how those capabilities are best applied to your specific needs. Of course, there’s no reason you cannot also develop custom functionality, if there is something not supplied by the framework, but there should be an using existing functionality. If you find yourself having to code too much from scratch, you may have the wrong framework, or may be applying it incorrectly.
Many frameworks began by attempting to address one portion of application development—database access, for example—and then expanded to encompass other areas, such as presentation. Frameworks initially implied a certain amount of lock-in by providing such end-to-end services: Once your application was using the framework, it was difficult to change your mind and plug in services or elements from other projects. As component-based development became more popular and effective, however, the better frameworks reduced this tie in, as you will see.
Frameworks fit into virtually every phase of a development project’s life cycle. Although they are not a design tool per se, their capabilities are definitely taken into account and can substantially simplify application design. They cut down on effort considerably in the development phase, and virtually every framework contains provisions for assisting in testing. The framework becomes an essential part of your application for production, and its facilities substantially reduce the maintenance burden.
Components and Services
Every framework provides services. These services are used either directly in the creation or management of your application code, or are used as part of your application to support its functionality, like Avalon. Either way, if you identify the services being provided, you have made a start towards isolating your application logic from those services, while still taking advantage of them.
The goal of component-based development is to assemble applications, not create them from scratch every time. By determining what services a project offers you, you then can determine the components you need to access from those services to use them. For example, if you look at the example of the Turbine framework, you see a caching service you want to take advantage of. In Turbine’s case, it’s easy to identify the services: The framework is built around services. For other projects, it will not be as simple, but the services are there nonetheless. Once you have identified a service you want to take advantage of, you need to identify and isolate the components you have to use to access that service. Again using Turbine’s Cache as an example, the TurbineGlobalCacheService object is your top-level interface to the Turbine Cache.
At this point, you have identified an implementation of a service, and the primary component (or components) that you need to access this service. This is where you bring into play the separation of a service into a role and an implementation of that role. Just like in a play, a role is an abstract, not a concrete person. When you say “get Hamlet up here”, you don’t get out a shovel; you are referring to the person who is playing the role Hamlet—whose name actually might be Frank. You might have an understudy, Joe, who also is perfectly capable of playing Hamlet; in case your original Hamlet falls ill, this person is a second implementation of the role “Hamlet.” In the script, however (the application logic in your analogy) you don’t read “Joe says this,” you read “Hamlet says this,” and it is understood that you are referring to the current implementation of Hamlet.
Just so in your application, you create a role, say “Cache,” that describes (via an interface) the function of a service that provides caching. In actual use, configuration associates the abstract “Cache” with the concrete “TurbineGlobalCacheService”. It is the implementation that does all the work, but your code never accesses TurbineGlobalCacheService directly; in fact, it never even imports the Java package that contains TurbineGlobalCacheService. You deal only with “Cache”, and if the actor playing “Cache” is changed for another capable actor, there is no difference to your “script.” The play’s the thing, after all!
By decomposing your design into services, and then defining the requirements of our services in terms of replaceable components, you remain independent of implementation. By keeping the individual elements of your application logic small and well-focused, you also maintain the ability to drive the application flow from an external source (such as workflow).
Dependencies and Classpath Conflicts
A common difficulty when combining multiple projects is a clash or conflict in the dependencies of the projects. Cocoon and Struts are good examples: Both projects depend on another Apache project from the Jakarta Commons group of projects, called “commons collections.” Ordinarily, the commons collections package is bundled into a binary .jar file, and placed in the appropriate directory (WEB-INF/lib) where the remainder of the package’s classes can find and utilize it. Struts, however, needs a different (newer) version of this jar file than does Cocoon. Attempting to run Cocoon with the newer file, in an effort to satisfy Struts’ requirements, causes errors in Cocoon. The reverse is true if we use the older file: Struts won’t operate correctly.
There are two solutions to this problem: the first is to modify and recompile either Cocoon to use the newer library, or Struts to use the older one. The problem with this solution is that it results in an “orphan” customized version of either Struts or Cocoon. You can no longer just download new versions of the two frameworks; you must concern yourself with the version of a particular library they are using—and each framework uses dozens of libraries.
Another, possibly better, solution is to let each framework run in its own environment, using its own copy of the library. This can be achieved either through running each in a separate virtual machine, or with a different classloader within the same virtual machine. Because both Struts and Cocoon can be run as Web applications, leaving them in two separate Web application contexts will use (under most servlet containers) a separate and distinct classloader for each of them, solving the problem. Of course, then you have no means to communicate between the two, and have failed to integrate the two projects.
The solution to this is communication: Rather than bundling the two frameworks or projects together, let them remain separate, but provide for a means for them to communicate. If they can transfer information and method requests between themselves, you can achieve the desired integration without conflict. This is exactly the technique that the Keel framework does; it provides a communication layer between frameworks (depending on configuration, JMS is the default choice for this layer, but Axis and several others are possible). This layer has the added advantage of providing a basis for distributed systems and clustering, and allows the overall application to scale to handle large user loads easily.
A seemingly minor problem can crop up when combining projects: Each project reads its configuration information in a different way, from a different source. In the case of a Turbine service, for example, configuration is supplied by means of a Configuration object. When you use Turbine’s cache service via, say, Avalon, you must deal with the fact that Avalon uses a different mechanism for providing its configuration to components (a life-cycle method, specifically). Because of the component-oriented design of both frameworks, however, the Configuration object passed to the service is independent from the method used to populate the object. It is therefore a simple matter to “map” from one configuration object to another, allowing you to plug the Turbine service into Avalon with very little work. This is a good example of separation of concerns—the actual configuration information is read in one component, and used in another.
For some less flexible projects, the code that supplies a service directly accesses configuration information from the source (reading it from a file or a resource on the classpath, for instance), making it difficult to “uncouple” the service from its configuration source. The solution in this case is either to supply the configuration the way the underlying service requires it, or to re-factor the service to read its configuration from a generic object (like Configuration) that can be populated elsewhere.
Separation of Concerns
The concept of separation of concerns provides many of the attributes you need when integrating multiple projects. If your application code concentrates on the business logic at hand, it is easier to integrate with multiple service implementations. If your services concentrate only on their own service, they are more easily re-used in other projects. The principal of “do one thing and do it well” is not a new one, but before the advent of frameworks and component development, it was hard to achieve in software engineering. If an application wanted a particular service, it would either build it in directly, or create its own API library for the service. Re-use was rare, and it was common for service functionality to be interlaced with application logic.
Choosing a Framework
As I’ve touched on above, choosing a framework is an important choice, and has long-term ramifications for your development project. It often is beneficial to choose a framework with an active and diverse community using it because this community becomes an important element of ongoing support.
Certain frameworks have unique advantages in special situations, though, and must not be discarded just because they are not widely popular. For example, Echo is ideal for Swing-based application developers who need to write a Web-based UI to look and work as much like their desktop application as possible.
The availability of professional services and consulting can also be an important element to look for.
Picking a framework to build on may be the most important step of your next application project—choose well!
About the Author
Michael Nash is the president of JGlobal Limited, a software development, consulting, training and support company specializing in open source Java technologies. He is also a core developer of the Keel meta-framework, the author of two books and a number of articles and papers about next-generation web-application development with Java, and a member of the JSR-127 (JavaServer Faces) Expert Group. He can be reached at [email protected]