Chapter 17. Remoting and web services using Spring

17.1. Introduction

Spring features integration classes for remoting support using various technologies. The remoting support eases the development of remote-enabled services, implemented by your usual (Spring) POJOs. Currently, Spring supports four remoting technologies:

  • Remote Method Invocation (RMI). Through the use of the RmiProxyFactoryBean and the RmiServiceExporter Spring supports both traditional RMI (with java.rmi.Remote interfaces and java.rmi.RemoteException) and transparent remoting via RMI invokers (with any Java interface).

  • Spring's HTTP invoker. Spring provides a special remoting strategy which allows for Java serialization via HTTP, supporting any Java interface (just like the RMI invoker). The corresponding support classes are HttpInvokerProxyFactoryBean and HttpInvokerServiceExporter.

  • Hessian. By using the HessianProxyFactoryBean and the HessianServiceExporter you can transparently expose your services using the lightweight binary HTTP-based protocol provided by Caucho.

  • Burlap. Burlap is Caucho's XML-based alternative for Hessian. Spring provides support classes such as BurlapProxyFactoryBean and BurlapServiceExporter.

  • JAX RPC. Spring provides remoting support for Web Services via JAX-RPC.

  • JMS (TODO).

While discussing the remoting capabilities of Spring, we'll use the following domain model and corresponding services:

// Account domain object
public class Account implements Serializable{
  private String name;

  public String getName();
  public void setName(String name) {
    this.name = name;
  }
}
			

// Account service
public interface AccountService {

  public void insertAccount(Account acc);
  
  public List getAccounts(String name);
}
			

// Remote Account service
public interface RemoteAccountService extends Remote {

  public void insertAccount(Account acc) throws RemoteException;
  
  public List getAccounts(String name) throws RemoteException;
}
			

// ... and corresponding implement doing nothing at the moment
public class AccountServiceImpl implements AccountService {

  public void insertAccount(Account acc) {
    // do something
  }
  
  public List getAccounts(String name) {
    // do something
  }
}
			

We will start exposing the service to a remote client by using RMI and talk a bit about the drawbacks of using RMI. We'll then continue to show an example for Hessian.

17.2. Exposing services using RMI

Using Spring's support for RMI, you can transparently expose your services through the RMI infrastructure. After having this set up, you basically have a configuration similar to remote EJBs, except for the fact that there is no standard support for security context propagation or remote transaction propagation. Spring does provide hooks for such additional invocation context when using the RMI invoker, so you can for example plug in security frameworks or custom security credentials here.

17.2.1. Exporting the service using the RmiServiceExporter

Using the RmiServiceExporter, we can expose the interface of our AccountService object as RMI object. The interface can be accessed by using RmiProxyFactoryBean, or via plain RMI in case of a traditional RMI service. The RmiServiceExporter explicitly supports the exposing of any non-RMI services via RMI invokers.

Of course, we first have to set up our service in the Spring BeanFactory:

<bean id="accountService" class="example.AccountServiceImpl">
    <!-- any additional properties, maybe a DAO? -->
</bean>
				

Next we'll have to expose our service using the RmiServiceExporter:

<bean class="org.springframework.remoting.rmi.RmiServiceExporter">
	<!-- does not necessarily have to be the same name as the bean to be exported -->
	<property name="serviceName"><value>AccountService</value></property>
	<property name="service"><ref bean="accountService"/></property>
	<property name="serviceInterface"><value>example.AccountService</value></property>
	<!-- defaults to 1099 -->
	<property name="registryPort"><value>1199</value></property>
</bean>
				

As you can see, we're overriding the port for the RMI registry. Often, your application server also maintains an RMI registry and it is wise to not interfere with that one. Furthermore, the service name is used to bind the service under. So right now, the service will be bound at rmi://HOST:1199/AccountService. We'll use the URL later on to link in the service at the client side.

Note: We've left out one property, i.e. the servicePort property, which is 0 by default. This means an anonymous port will be used to communicate with the service. You can specify a different port if you like.

17.2.2. Linking in the service at the client

Our client is a simple object using the AccountService to manage accounts:

public class SimpleObject {
  private AccountService accountService;
  public void setAccountService(AccountService accountService) {
    this.accountService = accountService;
  }
}
				

To link in the service on the client, we'll create a separate bean factory, containing the simple object and the service linking configuration bits:

<bean class="example.SimpleObject">
	<property name="accountService"><ref bean="accountService"/></bean>
</bean>

<bean id="accountService" class="org.springframework.remoting.rmi.RmiProxyFactoryBean">
	<property name="serviceUrl"><value>rmi://HOST:1199/AccountService</value></property>
	<property name="serviceInterface"><value>example.AccountService</value></property>
</bean>
				

That's all we need to do to support the remote account service on the client. Spring will transparently create an invoker and remotely enable the account service through the RmiServiceExporter. At the client we're linking it in using the RmiProxyFactoryBean.

17.3. Using Hessian or Burlap to remotely call services via HTTP

Hessian offers a binary HTTP-based remoting protocol. It's created by Caucho and more information about Hessian itself can be found at http://www.caucho.com.

17.3.1. Wiring up the DispatcherServlet for Hessian

Hessian communicates via HTTP and does so using a custom servlet. Using Spring's DispatcherServlet principles, you can easily wire up such a servlet exposing your services. First we'll have to create a new servlet in your application (this an excerpt from web.xml):

<servlet>
	<servlet-name>remote</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<load-on-startup>1</load-on-startup>
</servlet>
				

You're probably familiar with Spring's DispatcherServlet principles and if so, you know that know you'll have to create an application context named remote-servlet.xml (after the name of your servlet) in the WEB-INF directory. The application context will be used in the next section.

17.3.2. Exposing your beans by using the HessianServiceExporter

In the newly created application context called remote-servlet.xml we'll create a HessianServiceExporter exporting your services:

<bean id="accountService" class="example.AccountServiceImpl">
    <!-- any additional properties, maybe a DAO? -->
</bean>

<bean name="/AccountService" class="org.springframework.remoting.caucho.HessianServiceExporter">
    <property name="service"><ref bean="accountService"/></property>
    <property name="serviceInterface">
        <value>example.AccountService</value>
    </property>
</bean>
				

Now we're ready to link in the service at the client. No handler mapping is specified mapping requests (urls) onto services and that's why the BeanNameUrlHandlerMapping will be used, hence the service will be exported at the URL http://HOST:8080/AccountService.

17.3.3. Linking in the service on the client

Using the HessianProxyFactoryBean we can link in the service at the client. The same principles apply as with the RMI example. We'll create a separate bean factory or application context and mention the following beans where the SimpleObject is using the AccountService to manage accounts:

<bean class="example.SimpleObject">
    <property name="accountService"><ref bean="accountService"/></property>
</bean>

<bean id="accountService" class="org.springframework.remoting.caucho.HessianProxyFactoryBean">
	<property name="serviceUrl"><value>http://remotehost:8080/AccountService</value></property>
	<property name="ServiceInterface"><value>example.AccountService</value></property>
</bean>
				

That's all there is to it.

17.3.4. Using Burlap

We won't discuss Burlap, the XML-based equivalent of Hessian, in detail here, since it is configured and set up in exactly the same way as the Hessian variant explained above. Just replace the word Hessian with Burlap and you're all set to go.

17.3.5. Applying HTTP basic authentication to a service exposed through Hessian or Burlap

One of the advantages of Hessian and Burlap is that we can easily apply HTTP basic authentication, because both protocols are HTTP-based. Your normal HTTP server security mechanism can easily be applied through using the web.xml security features, for example. Usually, you don't use per-user security credentials here, but rather shared credentials defined at the Hessian/BurlapProxyFactoryBean level (similar to a JDBC DataSource).

<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
	<property name="interceptors">
		<list>
			<ref bean="authorizationInterceptor"/>
		</list>
	</property>
</bean>

<bean id="authorizationInterceptor" 
	class="org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor">
	<property name="authorizedRoles">
		<list>
			<value>administrator</value>
			<value>operator</value>
		</list>
	</property>	
</bean>
				

This an example where we explicitly mention the BeanNameUrlHandlerMapping and set an interceptor allowing only administrators and operators to call the beans mentioned in this application context.

Note: Of course, this example doesn't show a flexible kind of security infrastructure. For more options as far as security is concerned, have a look at the Acegi Security System for Spring, to be found at http://acegisecurity.sourceforge.net.

17.4. Exposing services using HTTP invokers

As opposed to Burlap and Hessian, which are both lightweight protocols using their own slim serialization mechanisms, Spring Http invokers use the standard Java serialization mechanism to expose services through HTTP. This has a huge advantage if your arguments and return types are complex types that cannot be serialized using the serialization mechanisms Hessian and Burlap use (refer to the next section for more considerations when choosing a remoting technology).

Under the hood, Spring uses either the standard facilities provided by J2SE to perform HTTP calls or Commons HttpClient. Use the latter if you need more advanced and easy-to-use functionality. Refer to jakarta.apache.org/commons/httpclient for more info.

17.4.1. Exposing the service object

Setting up the HTTP invoker infrastructure for a service objects much resembles the way you would do using Hessian or Burlap. Just as Hessian support provides the HessianServiceExporter, Spring Http invoker support provides the so-called org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter. To expose the AccountService (mentioned above), the following configuration needs to be in place:

    <bean name="/AccountService" class="org.sprfr.remoting.httpinvoker.HttpInvokerServiceExporter">
        <property name="service"><ref bean="accountService"/></property>
        <property name="serviceInterface">
            <value>example.AccountService</value>
        </property>
	</bean>

17.4.2. Linking in the service at the client

Again, linking in the service from the client much resembles the way you would do it when using Hessian or Burlap. Using a proxy, Spring will be able to translate your calls to HTTP POST requests to the URL pointing to the exported service.

	
	<bean id="httpInvokerProxy" class="org.sprfr.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
		<property name="serviceUrl">
			<value>http://remotehost:8080/AccountService</value>
		</property>
		<property name="serviceInterface">
			<value>example.AccountService</value>
		</property>
	</bean>

As mentioned before, you can choose what HTTP client you want to use. By default, the HttpInvokerProxy uses the J2SE HTTP functionality, but you can also use the Commons HttpClient by setting the httpInvokerRequestExecutor property:

<property name="httpInvokerRequestExecutor">
	<bean class="org.springframework.remoting.httpinvoker.CommonsHttpInvokerRequestExecutor"/>
</property>

17.5. Web Services

Spring has support for:

  • Exposing services using JAX-RPC
  • Accessing Web Services

17.5.1. Exposing services using JAX-RPC

Spring has a convenience base class for JAX-RPC servlet endpoint implementations - ServletEndpointSupport. To expose our AccountService we extend Spring's ServletEndpointSupport class and implement our business logic here, usually delegating the call to the business layer.

/**
 * JAX-RPC compliant RemoteAccountService implementation that simply delegates
 * to the AccountService implementation in the root web application context.
 *
 * This wrapper class is necessary because JAX-RPC requires working with
 * RMI interfaces. If an existing service needs to be exported, a wrapper that
 * extends ServletEndpointSupport for simple application context access is
 * the simplest JAX-RPC compliant way.
 *
 * This is the class registered with the server-side JAX-RPC implementation.
 * In the case of Axis, this happens in "server-config.wsdd" respectively via
 * deployment calls. The Web Service tool manages the life-cycle of instances
 * of this class: A Spring application context can just be accessed here.
 */
public class AccountServiceEndpoint extends ServletEndpointSupport implements RemoteAccountService {
    
    private AccountService biz;
	
    protected void onInit() {
        this.biz = (AccountService) getWebApplicationContext().getBean("accountService");
    }

    public void insertAccount(Account acc) throws RemoteException {
        biz.insertAccount(acc);
    }
  
    public Account[] getAccounts(String name) throws RemoteException {
        return biz.getAccounts(name);
    }
  
}	

Our AccountServletEndpoint needs to run in the same web application as the Spring context to allow for access to Spring's facilities. In case of Axis, copy the AxisServlet definition into your web.xml, and set up the endpoint in "server-config.wsdd" (or use the deploy tool). See the sample application JPetStore where the OrderService is exposed as a Web Service using Axis.

17.5.2. Accessing Web Services

Spring has two factory beans to create web service proxies LocalJaxRpcServiceFactoryBean and JaxRpcPortProxyFactoryBean. The former can only return a JAX-RPC Service class for us to work with. The latter is the full fledged version that can return a proxy that implements our business service interface. In this example we use the later to create a proxy for the AccountService Endpoint we exposed in the previous paragraph. You will see that Spring has great support for Web Services requiring little coding efforts - most of the magic is done in the spring configuration file as usual:

    <bean id="accountWebService" class="org.springframework.remoting.jaxrpc.JaxRpcPortProxyFactoryBean">
        <property name="serviceInterface">
            <value>example.RemoteAccountService</value>
        </property>
        <property name="wsdlDocumentUrl">
            <value>http://localhost:8080/account/services/accountService?WSDL</value>
        </property>
        <property name="namespaceUri">
            <value>http://localhost:8080/account/services/accountService</value>
        </property>
        <property name="serviceName">
            <value>AccountService</value>
        </property>
        <property name="portName">
            <value>AccountPort</value>
        </property>
    </bean>

Where serviceInterface is our remote business interface the clients will use. wsdlDocumentUrl is the URL for the WSDL file. Spring needs this a startup time to create the JAX-RPC Service. namespaceUri corresponds to the targetNamespace in the .wsdl file. serviceName corresponds to the serivce name in the .wsdl file. portName corresponds to the port name in the .wsdl file.

Accessing the Web Service is now very easy as we have a bean factory for it that will expose it as RemoteAccountService interface. We can wire this up in Spring:

    <bean id="client" class="example.AccountClientImpl">
        ...
        <property name="service">
            <ref bean="accountWebService"/>
        </property>
    </bean>

And from the client code we can access the Web Service just as if it was a normal class, except that it throws RemoteException.

public class AccountClientImpl {

    private RemoteAccountService service;
    
    public void setService(RemoteAccountService service) {
        this.service = service;
    }
    
    public void foo() {
       try {
           service.insertAccount(...);
        } catch (RemoteException e) {
           // ouch
           ...
        }
     }
     
}

We can get rid of the checked RemoteException since Spring supports automatic conversion to its corresponding unchecked RemoteAccessException. This requires that we provide a non RMI interface also. Our configuration is now:

    <bean id="accountWebService" class="org.springframework.remoting.jaxrpc.JaxRpcPortProxyFactoryBean">
        <property name="serviceInterface">
            <value>example.AccountService</value>
        </property>
        <property name="portInterface">
            <value>example.RemoteAccountService</value>
        </property>
        ...
    </bean>

Where serviceInterface is changed to our non RMI interface. Our RMI interface is now defined using the property portInterface. Our client code can now avoid handling java.rmi.RemoteException:

public class AccountClientImpl {

    private AccountService service;
    
    public void setService(AccountService service) {
        this.service = service;
    }
    
    public void foo() {
        service.insertAccount(...);
     }
     
}

17.5.3. Register Bean Mappings

To transfer complex objects over the wire such as Account we must register bean mappings on the client side.

[Note]Note

On the server side using Axis registering bean mappings is usually done in server-config.wsdd.

We will use Axis to register bean mappings on the client side. To do this we need to subclass Spring Bean factory and register the bean mappings programmatic:

public class AxisPortProxyFactoryBean extends JaxRpcPortProxyFactoryBean {

	protected void postProcessJaxRpcService(Service service) {
		TypeMappingRegistry registry = service.getTypeMappingRegistry();
		TypeMapping mapping = registry.createTypeMapping();
		registerBeanMapping(mapping, Account.class, "Account");
		registry.register("http://schemas.xmlsoap.org/soap/encoding/", mapping);
	}

	protected void registerBeanMapping(TypeMapping mapping, Class type, String name) {
		QName qName = new QName("http://localhost:8080/account/services/accountService", name);
		mapping.register(type, qName,
		    new BeanSerializerFactory(type, qName),
		    new BeanDeserializerFactory(type, qName));
	}

}

17.5.4. Registering our own Handler

In this section we will register our own javax.rpc.xml.handler.Handler to the Web Service Proxy where we can do custom code before the SOAP message is sent over the wire. The javax.rpc.xml.handler.Handler is a callback interface. There is a convenience base class provided in jaxrpc.jar - javax.rpc.xml.handler.GenericHandler that we will extend:

public class AccountHandler extends GenericHandler {

    public QName[] getHeaders() {
        return null;
    }

    public boolean handleRequest(MessageContext context) {
        SOAPMessageContext smc = (SOAPMessageContext) context;
        SOAPMessage msg = smc.getMessage();

        try {
            SOAPEnvelope envelope = msg.getSOAPPart().getEnvelope();
            SOAPHeader header = envelope.getHeader();
            ...
		
        } catch (SOAPException e) {
            throw new JAXRPCException(e);
        }

        return true;
    }

}

What we need to do now is to register our AccountHandler to JAX-RPC Service so it would invoke handleRequest before the message is sent over the wire. Spring has at this time of writing no declarative support for registering handlers. So we must use the programmatic approach. However Spring has made it very easy for us to do this as we can extend its bean factory and override its postProcessJaxRpcService method that is designed for this:

public class AccountHandlerJaxRpcPortProxyFactoryBean extends JaxRpcPortProxyFactoryBean {

    protected void postProcessJaxRpcService(Service service) {
        QName port = new QName(this.getNamespaceUri(), this.getPortName());
        List list = service.getHandlerRegistry().getHandlerChain(port);
        list.add(new HandlerInfo(AccountHandler.class, null, null));

        logger.info("Registered JAX-RPC Handler [" + AccountHandler.class.getName() + "] on port " + port);
    }

}

And the last thing we must remember to do is to change the Spring configuration to use our factory bean:

    <bean id="accountWebService" class="example.AccountHandlerJaxRpcPortProxyFactoryBean">
        ...
    </bean>    

17.6. Auto-detection is not implemented for remote interfaces

The main reason why auto-detection of implemented interfaces does not occur for remote interfaces is to avoid opening too many doors to remote callers. The target object might implement internal callback interfaces like InitializingBean or DisposableBean which one would not want to expose to callers.

Offering a proxy with all interfaces implemented by the target usually does not matter in the local case. But when exporting a remote service, you should expose a specific service interface, with specific operations intended for remote usage. Besides internal callback interfaces, the target might implement multiple business interfaces, with just one of them intended for remote exposure. For these reasons, we require such a service interface to be specified.

This is a trade-off between configuration convenience and the risk of accidental exposure of internal methods. Always specifying a service interface is not too much effort, and puts you on the safe side regarding controlled exposure of specific methods.

17.7. Considerations when choosing a technology

Each and every technology presented here has its drawbacks. You should carefully consider you needs, the services your exposing and the objects you'll be sending over the wire when choosing a technology.

When using RMI, it's not possible to access the objects through the HTTP protocol, unless you're tunneling the RMI traffic. RMI is a fairly heavy-weight protocol in that it support full-object serialization which is important when using a complex data model that needs serialization over the wire. However, RMI-JRMP is tied to Java clients: It is a Java-to-Java remoting solution.

Spring's HTTP invoker is a good choice if you need HTTP-based remoting but also rely on Java serialization. It shares the basic infrastructure with RMI invokers, just using HTTP as transport. Note that HTTP invokers are not only limited to Java-to-Java remoting but also to Spring on both the client and server side. (The latter also applies to Spring's RMI invoker for non-RMI interfaces.)

Hessian and/or Burlap might provide significant value when operating in a heterogeneous environment, because they explicitly allow for non-Java clients. However, non-Java support is still limited. Known problems include the serialization of Hibernate objects in combination with lazily initializing collections. If you have such a data model, consider using RMI or HTTP invokers instead of Hessian.

JMS can be useful for providing clusters of services and allowing the JMS broker to take care of load balancing, discovery and auto-failover. By default Java serialization is used when using JMS remoting but the JMS provider could use a different mechanism for the wire formatting, such as XStream to allow servers to be implemented in other technologies.

Last but not least, EJB has an advantage over RMI in that it supports standard role-based authentication and authorization and remote transaction propagation. It is possible to get RMI invokers or HTTP invokers to support security context propagation as well, although this is not provided by core Spring: There are just appropriate hooks for plugging in third-party or custom solutions here.