February 2002
Discuss this article
Welcome to the fourth installment in our series of articles explaining how to
use and consume Web services. In previous articles we've learned how to develop,
deploy, and use simple Web services. We also looked at some advanced Web
services concepts including stateful Web services, remote references, and Web
services security. In this article we'll begin to explore how Web services work
within the J2EE environment.
Understanding Web Services & J2EE Integration Basics
Today, many applications implement third-tier business logic as standard J2EE
components. Exposing these components as SOAP Web services makes them almost
universally accessible - and provides a simple mechanism for integrating these
components. The modular J2EE architecture makes this process relatively easy.
In this article we'll show how to expose J2EE components as Web services and
how to use the Java Message Service (JMS) to send SOAP messages reliably. We
will mainly speak about Enterprise Java Beans (EJBs), because they are the most
widely used J2EE components for business logic implementation, but all
demonstrated techniques are also applicable to other J2EE components, such as
JDBC data sources and JMS queues.
Standard J2EE Processing
Let's first summarize some important facts about the J2EE platform.
Traditionally, the J2EE client application uses JNDI to find J2EE components on
the server-side. For example, the client application looks up the EJB reference
in JNDI and receives an EJB client proxy in return, which the client later uses
to access the EJB component. All J2EE communication normally occurs over RMI.
Figure 1: Standard
JNDI usage
J2EE - Basic Approaches
There are two basic approaches to accessing J2EE resources via SOAP.
We'll start with the most obvious approach, which is to create a Web service
wrapper around the EJB. This approach is particularly appropriate in situations
where the Web service application doesn't map directly to the capabilities of an
individual EJB and requires some additional orchestration of the J2EE
components.
n our second example we will introduce a code-less, transparent integration
approach. Its main goal is to expose existing J2EE applications as Web services
as quickly and as dynamically as possible. This approach allows us to
effectively access existing J2EE applications over SOAP without writing or
modifying any code.
The Simple Stock Quote EJB Wrapper Demo
In this demo we will introduce the EJB wrapper Web service approach to access
a simple stateless session bean: the stock quote EJB. The wrapper approach is
very simple, and it is widely used by many SOAP frameworks. There are only
slight differences among the various implementations, which generally pertain to
the level of automation of the development process. The wrapper approach
requires development of a Web service that wraps one or more existing J2EE
components. This wrapper acts as a bridge between the SOAP world and the RMI
world. Clients send SOAP requests to the wrapper, and the wrapper translates
them into RMI requests to the EJB components. This approach is recommended for
use mainly with stateless J2EE resources, such as stateless session beans. To
access stateful resources using this technique you would need to set up
additional lifecycle services to manage the proper removal of orphaned stateful
resources.
We first need to perform some simple installation and configuration steps.
NOTE: If you haven't already downloaded the software used to create
the tutorial examples, please refer to the installation
chapter in Part
One. You'll also need to download the demo
sources. We assume that you've unpacked this archive into the
c:\wasp_demo directory. All Java sources mentioned in the tutorial
examples can be found in the src subdirectory of the unpacked demo sources
archive. They all reside in the com.systinet.demos package. Similarly
all scripts used in the demo are located in the bin subdirectory. You don't need
to download and use the software to understand these articles, but we strongly
recommend it.
ADDITIONAL INSTALLATION STEPS: We will use the Sun J2EE 1.3 reference
implementation for our J2EE environment. It's available for download from Sun's Java website. After
you've installed the J2EE 1.3 RI, you have to configure the WASP Web service
runtime to use the Sun J2EE RI by making a couple of changes in the
env.bat script that is located in the bin subdirectory of your WASP
Advanced installation. First comment out the following line (place rem at the
beginning of the line): set INSTALLATION_TYPE=standalone
Then uncomment the following line in the same script file (remove rem): set INSTALLATION_TYPE=j2ee
You also need to modify the env.bat script located in the
c:\wasp_demo\bin directory. Please specify the correct values for the
J2EE_HOME, WASP_HOME and WASP_DEMO environment variables.
Once you have completed the installation and configuration steps outlined
above, start the J2EE server and WASP Web services runtime using the
startJ2EE and startserver scripts. Next run the
deploy_j2ee script to compile the Java sources and deploy the EJBs that
we will use in our demos.
NOTE: You'll need to restart the J2EE server after you deploy the EJBs.
You can view the Java sources in the com.systinet.demos.stock
package to find that the StockQuote, StockQuoteHome and
StockQuoteBean classes implement a fairly simple stateless session bean
with one simple getQuote method. We've already deployed this EJB by
invoking the deploy script. You can make sure that all EJBs are
correctly deployed using the J2EE administration tool. Invoke the
J2EEAdmin script from the demo bin directory to start the
administration tool.
Now let's concentrate on the wrapper Web service implementation, listed in
Figure 2. It implements a getQuote method, which contains a simple EJB
invocation. First it retrieves the EJB home reference from JNDI and creates an
EJB instance. Then it invokes the getQuote method on the EJB, and
removes the EJB. Finally the invocation result is returned back to the Web
service client. You can see these steps in the code below:
package com.systinet.demos.stock;
import javax.naming.InitialContext;
import javax.naming.Context;
import javax.naming.NamingException;
import java.rmi.RemoteException;
public class StockQuoteService {
public double getQuote(String symbol) throws Exception {
// get the JNDI initial context
System.err.println("Getting J2EE initial context");
Context jndiContext = new InitialContext();
// lookup the EJB home
System.err.println("Looking up EJB Home");
Object homeRef = jndiContext.lookup("Stock");
StockQuoteHome home =
(StockQuoteHome)javax.rmi.PortableRemoteObject.narrow(
homeRef, StockQuoteHome.class);
// create the EJB instance
System.err.println("Creating EJB");
StockQuote ejb = home.create();
// call the getQuote method
System.err.println("Calling getQuote");
double quote = ejb.getQuote("SUNW");
System.err.println("SUNW "+quote);
// remove the EJB
System.err.println("Removing EJB");
ejb.remove();
return quote;
}
}
Figure 2: Simple Web service EJB wrapper
(StockQuoteService.java)
We can now deploy the EJB wrapper Web service by running the
deploy_service script. Then run the run_wrapper script to
start the Web service client that invokes the EJB through the wrapper Web
service.
NOTE: We've made this demo simple to illustrate the basic principles
of the wrapper approach; however, real-life applications are usually a bit more
complex. A wrapper service is often used to assemble functionality from a number
of EJBs and other J2EE resources. In such cases, the wrapper service usually
exposes different programmatic interfaces than the original beans.
Transparent J2EE integration
Another way to access J2EE resources is to use a transparent integration
framework. By transparent we mean that it isn't necessary to write a wrapper
service or to change the original J2EE code. This approach is most useful if you
have existing J2EE resources that you want to make available to SOAP clients or
if you have J2EE clients that need to access J2EE resources across the Internet.
The transparent J2EE integration framework described below exploits the
strengths of the JNDI architecture, which provides an abstract mechanism to
access J2EE resources. As we said earlier, in normal J2EE processing, a J2EE
client calls the JNDI lookup method, and the client's JNDI provider
passes this request over RMI to the JNDI service within the J2EE server. JNDI
returns a J2EE proxy to the client, which uses this proxy to invoke methods on
the remote J2EE resource over RMI. In this example, we will use a JNDI provider
on the client side that speaks SOAP rather than RMI. As you can see in Figure 3,
when the client issues a JNDI call using this provider, the request is sent over
SOAP to a JNDI web service. This JNDI web service performs the actual
lookup in the application server JNDI, obtaining the J2EE proxy. The JNDI web
service then returns to the client a SOAP-based remote
reference to the J2EE proxy. The client application can then use this remote
reference to invoke methods on the J2EE resource. Each method invocation is
transported over SOAP to the J2EE proxy, which redirects the request to the
actual J2EE resource. You'll notice that no code modifications are required in
either the J2EE resource or in the client code. Only a configuration change is
required in the client to point to the SOAP-based JNDI provider.
Figure 3: Web service
access to JNDI
NOTE: Most Web service runtime servers operate in the same context as
the application server, so the redirected method invocation is very fast and
won't degrade performance.
This approach also works for non-Java clients. Since the JNDI Web service is
a standard Web service, any SOAP client can take advantage of its transparent
invocation framework. For example, a Microsoft Visual Basic client can call the
lookup method on the JNDI Web service and obtain a Web service proxy to
the requested J2EE resource.
The JNDI Web service performs automatic remote garbage collection of all
components created in the Web service runtime. Most of those components are
discarded on demand when the client application discards the remote component
explicitly, but there is no guarantee of proper removal in the loosely coupled
world of Web services. That's why all dynamically created resources are tracked
and managed by a LifeCycle service.
The major advantage of this approach is that it provides immediate and
transparent SOAP access to any J2EE resource registered in JNDI, including all
types of EJB components (stateless and stateful session beans, entity beans, and
message-driven beans), plus JMS, JDBC, and other J2EE resources. No
modifications or wrappers need to be made for the J2EE resources. This approach
is obviously very useful to provide quick and easy access to existing systems
via SOAP. Let's look at an example.
Transparent J2EE Integration Demo
This demo shows a Web service calling an EJB running in Sun's J2EE 1.3
Reference Implementation engine.
Let's first check out the server-side EJB code. As you can see in the
following code listing, it's a standard stateful session bean that keeps the
state of a simple counter.
package com.systinet.demos.counter;
import javax.ejb.CreateException;
import javax.ejb.SessionBean;
import javax.ejb.SessionContext;
import javax.ejb.SessionSynchronization;
public class CounterEJB implements SessionBean {
private SessionContext context;
private int count = 0;
/**
* No argument constructor required by container.
*/
public CounterEJB() {
}
/**
* Create method specified in EJB 1.1 section 6.10.3
*/
public void ejbCreate() {
}
/* Methods required by SessionBean Interface. EJB 1.1 section 6.5.1. */
/**
* @see javax.ejb.SessionBean#setContext(javax.ejb.SessionContext)
*/
public void setSessionContext(SessionContext context){
this.context = context;
}
/**
* @see javax.ejb.SessionBean#ejbActivate()
*/
public void ejbActivate() {
}
/**
* @see javax.ejb.SessionBean#ejbPassivate()
*/
public void ejbPassivate() {
}
/**
* @see javax.ejb.SessionBean#ejbRemove()
*/
public void ejbRemove() {
}
public long getCount() {
return count++;
}
}
Figure 4: Simple Counter EJB (CounterEJB.java) - the server-side EJB
code
The other EJB sources are fairly obvious. You can see the code of the
Counter remote and CounterHome home interfaces. We've already
deployed the EJB in the first demo by invoking the deploy_j2ee command.
Please note that we don't need to deploy any specific Web service to the Web
service runtime.
The client application is a standard EJB client with the exception that it
uses different JNDI properties in the getInitialContext method.
package com.systinet.demos.counter;
import javax.naming.InitialContext;
import javax.naming.Context;
import javax.naming.NamingException;
import java.rmi.RemoteException;
public class CounterClient {
public CounterClient() {
}
public static void main(String [] args){
try {
// get the JNDI initial context
System.err.println("Getting J2EE initial context");
Context jndiContext = getInitialContext();
// lookup the EJB home
System.err.println("Looking up EJB Home");
Object homeRef = jndiContext.lookup("Counter");
CounterHome home = (CounterHome)javax.rmi.PortableRemoteObject.narrow(homeRef, CounterHome.class);
// create the EJB instance
System.err.println("Creating EJB");
Counter ejb = home.create();
System.out.println("Calling count "+ejb.getCount());
System.out.println("Calling count "+ejb.getCount());
System.out.println("Calling count "+ejb.getCount());
// remove the EJB
System.err.println("Removing EJB");
ejb.remove();
}
catch(java.rmi.RemoteException re) {
re.printStackTrace();
}
catch(Throwable t) {
t.printStackTrace();
}
}
static public Context getInitialContext() throws javax.naming.NamingException {
java.util.Properties jndiProperties = new java.util.Properties();
jndiProperties.put("java.naming.factory.initial","com.idoox.jndi.InitialContextFactoryImpl");
jndiProperties.put("java.naming.provider.url","http://localhost:6060");
return new InitialContext(jndiProperties);
}
}
Figure 5: Simple Counter EJB Client
(CounterClient.java)
Please notice that for the sake of simplicity we've hard-coded all JNDI
specific parameters. There are two parameters that need to be defined in order
to redirect the JNDI query to the JNDI Web service:
java.naming.factory.initial and java.naming.provider.url.
Those parameters are usually stored in an application .properties file
rather than hardcoded in the client application. In such case, recompilation of
the code would not be necessary.
The next step is to compile and run the J2EE client application using the
run script. You should see the EJB's getCount method called
three times. All communication between the client application and the
server-side is through SOAP messages. You can also see that the state (counter
value) is properly maintained.
Sending Reliable SOAP Messages over JMS
Today most Web services use the HTTP transport protocol for communications.
HTTP is very suitable for many applications. Its main advantage is flexibility
of application integration through the HTTP proxy and firewalls. But for some
applications, HTTP might not be sufficient. HTTP is unidirectional and lacks
many enterprise-class features, such as reliability, persistence, and
transactions. Also, its support for asynchronous message routing scenarios isn't
ideal. One approach to addressing these issues is to use JMS to transport SOAP
messages. JMS can provide substantial benefits for enterprise application
communication since it supports guaranteed message delivery, transactional
enqueueing and dequeueing of messages, and synchronous and asynchronous
messaging semantics. JMS also offers much better performance and scalability
than the HTTP protocol.
This next and final example demonstrates that SOAP is truly transport
protocol independent. We'll access the same Web service that we developed in the
first example, but this time we'll send the SOAP messages over JMS. The
implementation is very simple. We only need to change the lookup url passed to
the the client-side lookup method:
package com.systinet.demos.jms;
import org.idoox.wasp.Context;
import org.idoox.webservice.client.WebServiceLookup;
public final class StockClient {
/**
* @param args not used.
*/
public static void main( String[] args ) throws Exception {
System.setProperty("java.naming.factory.initial","com.idoox.jndi.InitialContextFactoryImpl");
System.setProperty("java.naming.provider.url","http://localhost:6060");
System.setProperty("idoox.demo.transport.j2ee", "true");
// lookup service
WebServiceLookup lookup = (WebServiceLookup)Context.getInstance(Context.WEBSERVICE_LOOKUP);
// bind to StockQuoteService
StockQuoteServiceProxy quoteService = (StockQuoteServiceProxy)lookup.lookup(
"jms://jms/Queue@jms/QueueConnectionFactory~/StockEJBService/",StockQuoteServiceProxy.class);
// use StockQuoteService
System.out.println("Getting SUNW quote");
System.out.println("------------------------");
System.out.println("SUNW "+quoteService.getQuote("SUNW"));
System.out.println("");
}
}
Figure 6: Simple JMS client (StockClient.java)
We'll compile and run the JMS client example by running the run_jms
script. The SOAP framework provides a completely transparent mapping to the
underlying transport protocol, so all previously mentioned features (e.g
stateful Web services, remote references, SOAP faults, etc.) will work over JMS
without any Java code changes.
NOTE: The Web service runtime is pre-configured to listen for SOAP
messages on the jms/Queue JMS queue.
Cleanup
Now that we've completed our demos, use the undeploy_j2ee and
undeploy_service scripts to remove the EJB from the application server
and the Web service from the Web service runtime.
Review
In this part of the Web services tutorial we learned about two ways to
integrate Web services with J2EE. We introduced the basic wrapper Web service
and the transparent integration framework and explained the situations in which
each approach provides substantial advantages. We've also shown that SOAP
messages can be sent over the JMS protocol.
In the next installment we'll demonstrate one of the major strengths of Web
services, interoperability between various Web services technologies. See you
then.
PRINTER FRIENDLY VERSION
|