|
|
 |
Integrating Apache SOAP with an EJB Server
By Billy Newport, EJB Consultant.
Part 1 |
Part 2 |
Part 3 |
Part 4
This article takes a break from the security aspect and provides more information about SOAP and using it with your
applications. SOAP basically provides an RPC mechanism that allows a client to invoke methods on a service which
resides on a server. This sounds like there is a singleton object on the server side but this is not true.
We implement our SOAP service using a Java class. The methods on the object can be static or normal methods.
Lets assume from this point on that the method are not static. This means that the SOAP dispatcher must create
an instance of the object before it can call a method on behalf of a SOAP client. If SOAP only supported
singletons then it would create a single instance of this object for the lifetime of the server and this
would not be terribly useful. You can how-ever, influence the life time of the service side object and make
it sticky with clients by specifying a scope indicator when you deploy the service.
SOAP Scope Levels
When we deploy a SOAP service we specify the scope level of the service. The scope can be one of three values:
- Request
This means that a new object is created for every SOAP request and lives for a single method invocation before it is discarded.
- Session.
This means that one object is created per client session. This means a HTTP session for us. WebSphere needs cookies to manage sessions and our new transport handles these for us thereby making this option viable. The default HTTP transport with SOAP does not do this and
hence you can't take advantage of session scope with it.
- Application.
Here one instance of the service is created and used to service all client requests for the lifetime
of the servlet engine.
The session and application scopes seem most useful for me. Application scope lets us create singleton
services. Session scope lets us create an instance of the service for each client that connects. We
can store state in the service that is client specific using session scope. We would need to manage
this our-selves with application scope.
We need to write a Java class that gets an instance of our session bean and stores the instance
in a class variable and then delegates calls to the public methods to the corresponding ones on the
session bean. We will use session level scope.
Scope Levels and session fault tolerance.
The only level that is fault tolerant will be session level scope. Almost all J2EE servers implement HTTP session fail-over. The service objects are kept in the HTTP session and so long as they can be serialized, they should survive fail-over.
Application level scope is probably not going to survive fail-over. This is because SOAP keeps the object instance in the ServletContext object. This object is usually not fail-over enabled so if the server dies then the servlet context will be lost. But, if you need client state then you should be using session scoping in any case. When using application level scope then the only state you should keep is cache for reference data etc that you can reload on restart in any case without any loss of data. You should not keep any volatile data that you can lose as state in your service when using application level scope.
Transactions with SOAP
We should point something out here. IIOP has an extension called OTS. The Java version of this is JTS.
Basically, JTS allows a client to create a transaction context that represents a single transaction.
This transaction context is sent to the server using OTS with every method call. This allows the server
to group the work done by the methods in to a single transaction even though each method called may
have been executed in a different thread on the server side.
HTTP doesn't support OTS, it's a Corba thing. Our SOAP client can send multiple requests to the
server in a single SOAP session. But, potentially each SOAP request may be processed by the server
using a different thread. This server thread may have been processing other clients SOAP requests
between your SOAP requests. This means that normally each method will run in its own transaction.
This is the case even when we use session scoping. It may be the same object each time that is
called but the point is that it is probably a different thread and hence the requests are not
executed in a single transaction. The servlet engine will rollback any running transaction when
the servlet request has been serviced in any case. Each servlet request is a new transaction.
So, for now, we need to set the transaction settings for the methods we call to none,
requires_new or required. Mandatory will fail unless we explicitly (and we could) make a
transaction surrounding the calls to the beans in our SOAP adapter method.
Why are we using Session beans for the service implementation again?
We are doing this so that we can leverage the container security of our application server. Every
method call to beans hosted in a container can be intercepted by the container. If we deploy the
bean with security ACLs (WebSphere method groups) attached to the bean or methods then the container
will limit access to these methods to authenticated clients that are authorized to invoke the method.
So, we put a canCallSOAP ACL on the rpcrouter servlet and then put more specific access controls on the
session bean implementing the server.
First, the ACL on the rpcrouter forces WebSphere to authenticate all HTTP clients that go to
that URL. Our HTTP transport takes care of this. The client is now authenticated to WebSphere and if
that client is authorized (present in the method group canCallSOAP) then the router processes
the request. It invokes the corresponding method on our SOAP service. This is simple an adapter
that forwards the call to the corresponding method on the session bean. The container will intercept
the call to the session bean and only allow it if the authenticated user is authorized to call that method.
So, the session beans allow us to have method level security by leveraging the security support
built in to the J2EE server. If you need better than method level security, for example,
the method allows all traders to call it but depending on a parameter to the method we
need further authorization in the logic. We might check if the trader is a bond trader or
an fx trader, for example. We can use the J2EE security APIs to do this. We simply define method
groups representing traders that are bond traders and then call the isCallerInRole method on the
session context in the session bean method. This, unfortunately, is not implemented at this
time (3.5.2) of WebSphere but may work in other application servers. This (if it works) is a cool
feature as you can use the security infrastructure supplied by the app server vendor to administer
these groups and query permissions. Normally, you'd need to implement this your-self
using tables and implement a GUI to administer it so it's a time saver.
Writing the Adapter
OK, we'll now write a simple adapter. This adapter is our SOAP service. It will delegate
the work to a session bean. The adapter simply creates an InitialContext, queries the home
interface and the creates a session bean in its constructor. The session bean instance is
stored in a class variable. Any exceptions that occur are wrapped in a SOAPException.
The methods on the adapter then just invoke the correct method on the session bean instance.
import javax.naming.InitialContext;
public class HelloWorldAdapter {
HelloWorld bean;
/**
* Create the adapter.
*/
public HelloWorldAdapter() {
super();
try
{
InitialContext ic = new InitialContext();
Object o = ic.lookup("com/ejbinfo/soap/ejb/HelloWorld");
HelloWorldHome hwHome = (HelloWorldHome)
javax.rmi.PortableRemoteObject.narrow(o, HelloWorldHome.class);
bean = hwHome.create();
}
catch(Exception e)
{
bean = null;
}
}
public String getTwoStrings(String i)
throws org.apache.soap.SOAPException
{
try
{
// Delegate to the method on the session bean.
return bean.getTwoStrings(i);
}
catch(Exception e)
{
throw new org.apache.soap.SOAPException("HelloWorldAdapter",
"cant invoke method", e);
}
}
}
It's worth pointing out that the above class as it is coded would also with well with application
level scope. The container will give us a good session bean each time so long as the bean is stateless.
If the bean is stateful then we need to mark the service as session level scope so that the bean instance
is associated with a specific client. It's also worth remembering that stateful session beans are not
re-entrant. If we made a mistake and gave two different clients the same instance of the bean and both
clients tried to concurrently invoke a method then the container would stop the second guy to the race
and throw an exception. Only one thread of execution is allowed at one time on a stateful session bean.
As regards performance, you may want to implement a home interface cache that is VM wide. This factory
object should be used to get a home interface for an ejb. Its pretty much always safe to cache a
home interface instance. WebSphere and WebLogic cluster these instances so that they can automatically
handle failover without you needing to look it up again if the original box falls over. InitialContext
lookups are usually expensive so it can pay to cache these in a HashMap that everybody shares.
Deploy the above service and run a test client
I deployed this using the admin page that comes with the SOAP package. I started a browser, and
told it to deploy a new service with the name urn:HelloWorld, it had session level scoping, a single
method getTwoStrings and no mappings. A new HelloWorldAdapter is created for each client that connects
but each client keeps its instance on subsequent requests thanks to the cookie management provided by
the HTTPClient package. Here is the test client that I used. It is basically the GetAddress client with
some modifications:
package com.ejbinfo.soap.ejb;
import java.io.*;
import java.util.*;
import java.net.*;
import org.w3c.dom.*;
import org.apache.soap.util.xml.*;
import org.apache.soap.*;
import org.apache.soap.encoding.*;
import org.apache.soap.encoding.soapenc.*;
import org.apache.soap.rpc.*;
import java.security.*;
public class HelloWorldSOAPClient
{
public static void main(String[] args) throws Exception
{
if (args.length != 2
&& (args.length != 3 || !args[0].startsWith("-")))
{
System.err.println("Usage:");
System.err.println(" java " + HelloWorldSOAPClient.class.getName() +
" [-encodingStyleURI] SOAP-router-URL nameToLookup");
System.exit (1);
}
// Process the arguments.
int offset = 3 - args.length;
String encodingStyleURI = args.length == 3
? args[0].substring(1)
: Constants.NS_URI_SOAP_ENC;
URL url = new URL(args[1 - offset]);
String param = args[2 - offset];
SOAPMappingRegistry smr = new SOAPMappingRegistry();
BeanSerializer beanSer = new BeanSerializer();
// Build the call.
Call call = new Call();
call.setSOAPMappingRegistry(smr);
call.setTargetObjectURI("urn:HelloWorld");
call.setMethodName("getTwoStrings");
call.setEncodingStyleURI(encodingStyleURI);
// HTTPClient.AuthorizationInfo.
// addBasicAuthorization("p300",443,"EJBINFO","bnewport","secret");
// Don't prompt whether to accept cookies.
HTTPClient.CookieModule.setCookiePolicyHandler(null);
// Use our HTTP transport.
call.setSOAPTransport(new com.ejbinfo.soap.transport.http.SOAPHTTPConnection());
Vector params = new Vector();
params.addElement(new Parameter("input", String.class,
param, null));
call.setParams(params);
// Invoke the call.
Response resp;
try
{
resp = call.invoke(url, "");
}
catch (SOAPException e)
{
System.err.println("Caught SOAPException (" +
e.getFaultCode() + "): " +
e.getMessage());
return;
}
// Check the response.
if (!resp.generatedFault())
{
Parameter ret = resp.getReturnValue();
Object value = ret.getValue();
System.out.println(value != null ? "\n" + value : "I don't know.");
}
else
{
Fault fault = resp.getFault();
System.err.println("Generated fault: ");
System.out.println (" Fault Code = " + fault.getFaultCode());
System.out.println (" Fault String = " + fault.getFaultString());
}
}
}
Stateless and Stateful session beans.
You could keep state in the adapter. We do have one per client after all. But, given that the business
logic is contained in the session bean then if we do need to keep state between calls, a stateful
session bean makes more sense, no? We just keep the EJBHandle in the service object. This
is serializable. Stateful session beans are not guaranteed to be recoverable when a failure
occurs. WebSphere and WebLogic 5.x don't support stateful session bean fail-over. WebLogic 6.0
does support this apparently. A portable solution may be to store your state in an entity bean
and keep the key as state in your service object. The business logic should then be implemented using
a stateless session bean and the adapter passes the key to the methods of the session bean. This
allows the session bean to retrieve the entity bean and recover the state. But, you will now have the
additional headache of purging entity beans from the database when a client dies unexpectedly how-ever.
It appears finally that if you need fail-over then use WebLogic 6.0 and replicated stateful session beans
or keep your state in a serializable object that you keep in the SOAP adapter and pass to the session
bean on each method call. Why use the session bean when all the state is in the adapter?
Security. If we don't use the session bean then we lose security. The SOAP adapter has no access
to the HTTPSession and therefore no access to the J2EE security APIs on that. This means it's
difficult to implement method level security. You could patch SOAP to force it to make the
HTTPSession object available to the adapter but you're probably better off just using a stateless
session bean that accepts its state as a parameter and returns a new copy to be stored in the adapter afterwards.
Conclusion
We now have an adapter that we use to forward SOAP requests to a stateless session bean. It can
leverage the J2EE security provided by the server and it is fault-tolerant as we're taking advantage
of the HTTPSession HA features of the server also. The new HTTP transport we're using makes this
possible because it supports cookies. This means that the server's session tracking works and can
associate a server side HTTPSession with our HTTPClient.
The next article should finally get around to showing how to do certificate authentication.
|