Design a simple service-oriented J2EE application framework

Leverage Struts, Spring, Hibernate, and Axis

Today, developers are inundated with open source frameworks that help with J2EE programming: Struts, Spring, Hibernate, Tiles, Avalon, WebWorks, Tapestry, or Oracle ADF, to name a few. Many developers find that these frameworks are not the panacea to their problems. Just because they are open source doesn't mean they are easy to change and improve. When a framework falls short in a key area, addresses only a specific domain, or is just bloated and too expensive, you might need to build your own framework on top of it. Building a framework like Struts is a nontrivial task. But incrementally developing a framework that leverages Struts and other frameworks doesn't have to be.

In this article, I show you how to develop X18p (Xiangnong 18 Palm, named for a legendary powerful kung fu fighter), a sample framework that addresses two common issues ignored by most J2EE frameworks: tight coupling and bloated DAO (data access object) code. As you'll see later, X18p leverages Struts, Spring, Axis, Hibernate, and other frameworks at various layers. Hopefully, with similar steps, you can roll your own framework with ease and grow it from project to project.

The approach I take in developing this framework uses concepts from IBM's Rational Unified Process (RUP). I follow these steps:

  1. Set simple goals initially
  2. Analyze the existing J2EE application architecture and identify the issues
  3. Compare alternative frameworks and select the one that is simplest to build with
  4. Develop code incrementally and refactor often
  5. Meet with framework's end-user and collect feedback regularly
  6. Test, test, test

Step 1. Set simple goals

It is tempting to set ambitious goals and implement a cutting-edge framework that solves all problems. If you have sufficient resources, that is not a bad idea. Generally, developing a framework upfront for your project is considered overhead that fails to provide tangible business value. Starting smaller helps you lower the unforeseen risks, enjoy less development time, lower the learning curve, and get project stakeholders' buy-in. For X18p, I set only two goals based on my past encounters with J2EE code:

  1. Reduce J2EE Action code coupling
  2. Reduce code repetition at J2EE DAO layer

Overall, I want to provide better quality code and reduce the total cost of development and maintenance by increasing my productivity. With that, we go through two iterations of Steps 2 through 6 to meet those goals.

Reduce code coupling

Step 2. Analyze previous J2EE application architecture

If a J2EE application framework is in place, we first must see how it can be improved. Obviously, starting from scratch doesn't make sense. For X18p, let's look at a typical J2EE Struts application example, shown in Figure 1.

Figure 1. J2EE Struts application architecture. Click on thumbnail to view full-sized image.

Action calls XXXManager, and XXXManager calls XXXDAOs. In a typical J2EE design that incorporates Struts, we have the following items:

  • HttpServlet or a Struts Action layer that handles HttpRequest and HttpResponse
  • Business logic layer
  • Data access layer
  • Domain layer that maps to the domain entities

What's wrong with the above architecture? The answer: tight coupling. The architecture works just fine if the logic in Action is simple. But what if you need to access many EJB (Enterprise JavaBeans) components? What if you need to access Web services from various sources? What if you need to access JMX (Java Management Extensions)? Does Struts have a tool that helps you look up those resources from the struts-config.xml file? The answer is no. Struts is meant to be a Web-tier-only framework. It is possible to code Actions as various clients and call the back end via the Service Locator pattern. However, doing so will mix two different types of code in Action's execute() method.

The first type of code relates to the Web-tier HttpRequest/HttpResponse. For instance, code retrieves HTTP form data from ActionForm or HttpRequest. You also have code that sets data in an HTTP request or HTTP session and forwards it to a JSP (JavaServer Pages) page to display.

The second code type, however, relates to the business tier. In Action, you also invoke backend code such as EJBObject, a JMS (Java Message Service) topic, or even JDBC (Java Database Connectivity) datasources and retrieve the result data from the JDBC datasources. You may use the Service Locator pattern in Action to help you do the lookup. It's also possible for Action to reference only a local POJO (plain old Java object) xxxManager. Nevertheless, a backend object or xxxManager's method-level signatures are exposed to Action.

That's how Action works, right? The nature of Action is a servlet that is supposed to care about how to take data in from HTML and set data out to HTML with an HTTP request/session. It also interfaces to the business-logic layer to get or update data from that layer, but in what form or protocol, Action could care less.

As you can imagine, when a Struts application grows, you could end up with tight references between Actions (Web tier) and business managers (business tier) (see the red lines and arrows in Figure 1).

To solve this problem, we can consider the open frameworks in the market—let them inspire our own thinking before we make an impact. Spring Framework comes on my radar screen.

Step 3. Compare alternative frameworks

The core of Spring Framework is a concept called BeanFactory, which is a good lookup factory implementation. It differs from the Service Locator pattern in that it has an Inversion-of-Control (IoC) feature previously called Injection Dependency. The idea is to get an object by calling your ApplicationContext's getBean() method. This method looks up the Spring configuration file for object definitions, creates the object, and returns a java.lang.Object object. getBean() is good for object lookups. It appears that only one object reference, ApplicationContext, must be referenced in the Action. However, that is not the case if we use it directly in the Action, because we must cast getBean()'s return object type back to the EJB/JMX/JMS/Web service client. Action still must be aware of the backend object at the method level. Tight coupling still exists.

If we want to avoid an object-method-level reference, what else we can use? Naturally, service, comes to mind. Service is a ubiquitous but neutral concept. Anything can be a service, not necessarily just the so-called Web services. Action can treat a stateless session bean's method as a service as well. It can treat calling a JMS topic as consuming a service too. The way we design to consume a service can be very generic.

With strategy formulated, danger spotted, and risk mitigated from the above analysis and comparison, we can spur our creativity and add a thin service broker layer to demonstrate the service-oriented concept.

Step 4. Develop and refactor

To implement the service-oriented concept thinking into code, we must consider the following:

  • The service broker layer will be added between the Web tier and the business tier.
  • Conceptually, an Action calls a business service request only, which passes the request to a service router. The service router knows how to hook up business service requests to different service provider controllers or adapters by looking up a service mapping XML file, X18p-config.xml.
  • The service provider controller has specific knowledge of finding and invoking the underlying business services. Here, business services could be anything from POJO, LDAP (lightweight directory access protocol), EJB, JMX, COM, and Web services to COTS (commercial off the shelf) product APIs. X18p-config.xml should supply sufficient data to help the service provider controller get the job done.
  • Leverage Spring for X18p's internal object lookup and references.
  • Build service provider controllers incrementally. As you will see, the more service provider controllers implemented, the more integration power X18p has.
  • Protect existing knowledge such as Struts, but keep eyes open for new things coming up.

Now, we compare the Action code before and after applying the service-oriented X18p framework:

Struts Action without X18p

   public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response)throws IOException, ServletException {
      ...
      UserManager userManager = new UserManager();
      String userIDRetured = userManager.addUser("John Smith")
      ...
}

Struts Action with X18p

public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response)
      throws IOException, ServletException {
      ...
      ServiceRequest bsr = this.getApplicationContext().getBean("businessServiceRequest");   
      
      bsr.setServiceName("User Services");
      bsr.setOperation("addUser");
      bsr.addRequestInput("param1", "addUser");
      String userIDRetured = (String) bsr.service();
      ...
}

Spring supports lookups to the business service request and other objects, including POJO managers, if any.

Figure 2 shows how the Spring configuration file, applicationContext.xml, supports the lookup of businessServiceRequest and serviceRouter.

Figure 2. Spring framework configuration. Click on thumbnail to view full-sized image.

In ServiceRequest.java, the service() method simply calls Spring to find the service router and passes itself to the router:

   public Object service() {
      return ((ServiceRouter) this.serviceContext.getBean("service router")).route(this);
   }

The service router in X18p routes user services to the business logic layer with X18p-config.xml's help. The key point is that the Action code doesn't need to know where or how user services are implemented. It only needs to be aware of the rules for consuming the service, such as pushing the parameters in the correct order and casting the right return type.

Figure 3 shows the segment of X18p-config.xml that provides the service mapping information, which ServiceRouter will look up in X18p.

Figure 3. X18p service mapping configuration. Click on thumbnail to view full-sized image.

For user services, the service type is POJO. ServiceRouter creates a POJO service provider controller to handle the service request. This POJO's springObjectId is userServiceManager. The POJO service provider controller uses Spring to look up this POJO with springObjectId. Since userServiceManager points to class type X18p.framework.UserPOJOManager, the UserPOJOManager class is the application-specific logic code.

Examine ServiceRouter.java:

   public Object route(ServiceRequest serviceRequest) throws Exception {
      //        /1. Read all the mapping from XML file or retrieve it from Factory
      //         Config config = xxxx;
      //        2. Get service's type from config.
      String businessServiceType = Config.getBusinessServiceType(serviceRequest.getServiceName());
      //        3. Select the corresponding Router/Handler/Controller to deal with it.
      if (businessServiceType.equalsIgnoreCase("LOCAL-POJO")) {
         POJOController pojoController = (POJOController) Config.getBean("POJOController");
         pojoController.process(serviceRequest);
      }
      else if (businessServiceType.equalsIgnoreCase("WebServices")) {
         String endpoint = Config.getWebServiceEndpoint(serviceRequest.getServiceName());
         WebServicesController ws = (WebServicesController) Config.getBean("WebServicesController");
         ws.setEndpointUrl(endpoint);
         ws.process(serviceRequest);
      }
      else if (businessServiceType.equalsIgnoreCase("EJB")) {
         EJBController ejbController = (EJBController) Config.getBean("EJBController");
         ejbController.process(serviceRequest);
      }
      else {
         //TODO
         System.out.println("Unknown types, it's up to you how to handle it in the framework");
      }
      //        That's it, it is your framework, you can add any new ServiceProvider for your next project.
      return null;
   }

The above routing if-else block could be refactored into a Command pattern. The Config object provides the Spring and X18p XML configuration lookup. As long as valid data can be retrieved, it's up to you how to implement the lookup mechanism.

Assuming a POJO manager, TestPOJOBusinessManager, is implemented, the POJO service provider controller (POJOServiceController.java) then looks for the addUser() method from the TestPOJOBusinessManager and invokes it with reflection (see the code available from Resources).

By introducing three classes (BusinessServiceRequester, ServiceRouter, and ServiceProviderController) plus one XML configuration file, we have a service-oriented framework as a proof-of-concept. Here Action has no knowledge regarding how a service is implemented. It cares about only input and output.

The complexity of using various APIs and programming models to integrate various service providers is shielded from Struts developers working on the Web tier. If X18p-config.xml is designed upfront as a service contract, Struts and backend developers can work concurrently by contract.

Figure 4 shows the architecture's new look.

Figure 4. X18p service-oriented architecture. Click on thumbnail to view full-sized image.

I summed up the common service provider controllers and implementation strategies in Table 1. You can easily add more.

Table 1. Implementation strategies for common service provider controllers

1 2 3 Page
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more