Frameworks save the day

Use an extensible, vendor-independent framework to accomplish the top tasks in server-side development

In server-side development, a number of core tasks crop up over and over again. Most developers know that such tasks can and should be pulled into a core framework, built and tested once, and reused across multiple projects. However, knowing something and doing it are two different things.

The framework concept has been kicking around in software development for a long time in one form or another. In its simplest form, a framework is simply a body of tried and tested code that is reused in multiple software development projects. Smart companies invest formally in frameworks and good developers build up a library of components that they use often. Such actions reduce development time while improving delivered software quality -- which means that developers can spend more time concentrating on the business-specific problem at hand rather than on the plumbing code behind it. A good framework also enhances the maintainability of software through API consistency, comprehensive documentation, and thorough testing.

At one level, the framework showcased in this article does the simple things you need every day: logging, exception handling, JNDI lookup, configuration, and database management. Delving deeper into the design and implementation however, you will see that the framework also provides application server independence, future hooks for adding management services, and a well-defined extension mechanism.

Note: To download the framework's complete source code in zip format, go to the Resources section below.

Goals for the framework

Before setting out to build the framework, it's worthwhile to set out some basic objectives against which we can measure success:

  • The framework should be simple. The number of objects should be minimal, with simple methods and shallow inheritance hierarchies. Furthermore, the API must be consistent across different framework modules to minimize the ramp-up time required to start effectively using the framework.
  • A developer should be able to add new services to the framework easily. You can be sure that your framework will grow over time, becoming a balkanized hodge-podge reflecting the designers' and developers' personal coding styles. If from the start you lay down a well-defined extension mechanism that is powerful enough to meet the needs of your framework providers, you've laid a sound base on which to build and extend the framework over time.
  • The framework should have solid documentation. This may sound obvious, but it rarely happens in practice. At the very least, users will expect a good level of javadoc commenting and an overall block diagram outlining the major components in the system, along with sample code showing how the components can be used.
  • The framework should be usable from any J2EE component. This includes EJBs, servlets, JMS listeners, and regular Java classes. Accomplishing this is not difficult, but it needs to be kept in mind during development.
  • Developers should be able to deploy the framework to multiple application servers. A really useful framework will offer developers the same level of functionality no matter what application server they work on. For example, if a vendor claims that its environment is EJB 1.1 compliant, then I expect certain things to be present in that environment. In the same way, if a framework is available on application server X, then it should be fully functional on that server, no ifs or buts. Above all, this complexity should be hidden from the end user wherever possible.

Framework assumptions

In order to make our framework easier to maintain, some assumptions are made:

  • Supported databases: The only component that uses a database directly is the DbConnectionService. Any database that has a JDBC driver will work.
  • Supported JDK versions: 1.2.2 or higher. JDK 1.1.x is explicitly not supported. JDK 1.3-specific APIs are not used, as the 1.3 JDK has still not gained widespread acceptance as a production VM.
  • Target application servers: BEA WebLogic 5.1 and jBoss. This point is not so much a restriction, more a declaration of what comes working (and tested) out of the box. There is no reason why the framework couldn't be extended to other application servers as required (in fact, it is designed in such a way to make this as clear and easy as possible).
  • Vendor-specific functionality: Vendor-specific functionality would clash with the stated goal of providing a vendor-independent framework. Though in some cases vendors have added proprietary extensions to their product that would enhance performance or flexibility, such extensions have been eschewed here in favor of complete vendor independence.

The framework's five basic components

Next, we look at the five core components -- logging, JNDILookup, configuration, database connection management, and exception handling -- that make up the framework. These are the hosted services that can be leveraged by business-specific code, as seen in Figure 1.

Figure 1. The current framework consists of five components. The logging service serves a core function in that the other services depend on its existence.

Logging

Logging represents the framework's single most important component. Apart from the value it adds to users, it is crucial to debugging the framework itself. Put simply, a system without a logging component and an accompanying set of logging guidelines built into the coding standards will take a long time to develop (and debug) and will be very difficult to maintain.

So what are the requirements for logging?

  1. Simplicity: A logging component must be simple to use, or developers won't use it. Instead, they'll use System.out.println, thus impairing performance. With that in mind, only one import statement and one line should be necessary to use the logging service, no more.
  2. Flexible output formatting: Systems regularly go live without any reporting features built in. It typically isn't critical to the main system functionality to have things like usage patterns, audit trails, and so on reported; such features are thus instead penciled in for phase two or even phase three releases. Though you may not think you need this functionality now, wouldn't it would help a lot if your plain old logging module could handle it already? Most third-party reporting tools can read your output logs as long as they are structured -- the logging service should be able to handle this in a configurable way.
  3. Support for output to different channels: In development, piping output to stdout as well as a file is helpful; in production, this should be turned off to improve performance.

The design and construction of a logging service could take up a complete article in itself. Instead of spending time building one, I have picked one off the shelf that I consider to be best of the breed: log4j (see Resources for more details). Although most vendors (including jBoss and WebLogic) provide a logging service as part of their products, their services aren't used here because they would affect the cross-platform portability of the framework.

In the framework lifecycle, two incarnations of the LogService exist. Initially, the FrameworkManager and the logging service itself use the BootLogger, as neither component can assume that the fully-fledged logging service has been located and bootstrapped. Once the main logging service has been initialized, the other framework components use it in preference to the BootLogger, which possesses a subset of the main service's functionality.

Finding objects/references stored on a JNDI tree

Next, let's look at the JNDILookup component. JNDI trees serve as the telephone directories of the enterprise Java world. Looking for that hot new bean in town or the latest connection pool? You will find them in the JNDI environment as named and configured by the application assembler/deployer. The framework provides a window into this world that hides the vendor- and location-specific details from developers when they don't need to or want to be aware of them. This service also serves as an example of how to use the framework's ability to detect the current application server to configure a client service appropriately. See the JNDIService javadoc for more details on this functionality.

Using a configuration lookup to avoid hardcoding variables

Our third component is the configuration lookup. Although EJBs can use the java:comp/env JNDI context to store information that should live outside the codebase, this is not so easy to do for non-EJB components. With the ConfigService, all J2EE components can retrieve values from a central file-based repository. Thus, you won't have to hardwire these values or use J2EE component-specific mechanisms.

Database connection pooling

We next turn our attention to the framework's fourth component, the database connection manager. Databases are a commodity in the enterprise Java world. Most of the time, developers don't need or want to know where a database is; they simply want a connection to talk to it. As the relational world becomes more in tune with the object world, eventually a service like this will filter completely behind the scenes; knowing that your objects are persisted to a database would be like knowing where a spool file for email lives on a server -- you don't care, you just want to use email. Until that happy day, however, we need a database-connection finder service.

Exception handling

A consistent exception handling strategy is a core requirement for any distributed system. Put simply, each framework components should be honest in all its dealings. In other words, if I ask the JNDILookup to find a bean for me, I don't want to get a null reference in return! In a better world, the framework would either return a valid object or explicitly inform the caller that the service was unable to fulfill its request. Indeed, the framework provides a base exception class that developers can use to follow this rule consistently. If you choose not to follow this rule, your reasons why should be clearly outlined and well documented. Also, the JNDILookup service is provided as an example of how to subclass the base exception and use it in the public API.

Now that the individual components have been detailed, we turn our attention to the larger strategy of framework configuration and management.

The framework brain

Let's recap the progress to date. So far, we've built up the five core modules in the base framework. Where do we go from here? What doesn't the framework do that it should? Well, it can't handle a hybridized deployment environment yet -- it can't morph to suit the system it finds itself in. Also, as we add new services, how will the framework know about them? Can we add some brains without killing performance? The answer is yes! (For what follows, I presume you understand design patterns along with their advantages and disadvantages; if not, see Resources for a good link).

In the following sections, we'll first examine the main "command and control" component -- the FrameworkManager. Next, we'll look at the mechanism used to interrogate the framework host, and examine our deployment strategy for the framework. Finally, we'll touch briefly on the process used to build the framework.

The framework factory/manager

Figure 2 shows the clear separation of responsibilities promoted by the framework. Because of the well-defined extension mechanism and FrameworkManager, business-specific code does not need to initialize a framework service before using it. The business-specific code is also completely unaware of the specifics in configuring the framework to the current host environment. Indeed, at the business-specific layer, only the service's public APIs are visible.

Figure 2. The high-level architecture

In addition to the usual factory tasks of component creation/initialization and location, the FrameworkManager figures out what application server it has been deployed to and makes that information available to its client components. As it boasts more intelligence than a regular factory, it's called FrameworkManager rather than FrameworkFactory.

I used Ant (a Java-based build automation tool that removes many of the pitfalls associated with building large Java codebases; see Resources for more information) to build the framework. I've also taken advantage of a very powerful Ant Task in the framework itself, the MatchingTask. This class searches a specified directory structure for files that meet a predefined filter pattern; such functionality plays a key role in the autodiscovery of framework components.

1 2 3 Page 1
Page 1 of 3