1. Introduction

1. Introduction

When we talk about a service oriented architecture, most people think immediately about SOAP and webservices. However, the concept of a "service" can and should be interpreted much more broadly.

Services could be implemented using a wide variety of transport protocols such as HTTP , RMI , CORBA etc. Or on top of technology such as JEE 's EJB stateless session beans .

The fact that we think of SOA as SOAP, is because virtually every programming language can offer SOAP services and use them. That's not the case with all the other protocols and technologies available. And there is the issue that the first three letters are identical...

When we talk about Service Oriented Architecture , we mostly talk about an enterprise architecture i.e. a set of software components belonging to a single company - the occasional external service provider fits in without a problem.

The other approach to web services, such as mash ups, and websites using services such as Google Maps etc. are not discussed here.

1.1. Service

The service provider defines a contract, that is binding for both the provider and consumer. Think of this as a (kind of) API that has documented behavior, and that should only evolve in a backward compatible fashion.

The parameters are always passed by value, not by reference. This is mostly required by the distributed nature of the deployment and the potential use of technologies that do not support passing references.

A service should be autonomous, it should be owner of all it’s resources such as configuration artifacts (configuration file, jar libraries,…), database schema etc. This makes it possible to substitute one service for another and to have freedom to improve, change the implementation without having to redeploy and test everything again. As long as they observe the same contract.

This autonomy corresponds to the encapsulation principle of a good OO design.

If you want to evaluate whether a service is well designed and abstracted, try to imagine two wildly different implementations of the service, one using a database and the other using for instance an XML file as a persistence mechanism. If there is no impact on other systems, you have a perfectly designed service.

In real world cases, services are not always the owner of their database. Often this is because of a dependency by other software on that database like BI tools or worse, other applications depend on the database schema. This is often the most important reason why SOA doesn't deliver on its promise of loose coupling and lower overall cost.

Relying only on well designed services that are loosely coupled, provides a way to incrementally upgrade or substitute part of the system without affecting the service consumers. Such a substitution might be a migration of service per service to new technology or just the implementation of new business logic

1.2. Web Service

A web service is a service deployed with a particular technology, always involving HTTP and a text based data format such as XML or JSON. Different web service technologies are available: SOAP, XML-RPC etc.

When talking about SOAP, some related technology, should be mentioned. UDDI (registry and discovery) and WSDL (WS Description Language) are XML-schemas and documents that are primarily intended for discovering and consuming third party services.

1.2.1. Service Properties

Ideally a service should respect a few rules. These are not always evident, but when followed, the service will be loosely coupled, more reusable and robust.

  • Autonomous
  • Reusable
  • Encapsulated
  • Idempotent Operations
  • Reentrant Operations
  • ...

Autonomous . This means that everything the service needs, must be owned by the service. E.g. the database, other processes and services should not be allowed to access the data except via the service. DTO and the service interface as described in the technical discussion are also "owned" by the service. This is the single most important requirement for achieving the loosely coupled quality. Imagine replacing the current service with a completely redesigned new one, if other processes depend on the same database structure, it is impossible to replace it.

Reusable . This is a quality that is not easy to detect and promote. It is related to an insight into the business logic AND a good grasp of clean, object oriented design principles as encapsulation and coherence.

Encapsulated . This is a general design principle in OO , it enables loose coupling since the well encapsulated service can change its internal implementation without external impact. It consists of hiding internal implementation details from external agents (components, other services etc.).

Reentrant Operations Reentrant means that the service’s operation can be called recursively and - more importantly – concurrently. Reentrancy is easily achieved by not keeping conversational state . In development, it can be done by using local variables only, and accessing other variables as read-only values. Shared data must be accessed in a thread safe way - such as using database transactions to achieve ACID-ness .

Idempotent Operations . This means that sending the same message twice should not result in two executions. E.g. obtain an invoice id and then invoke the makeInvoice operation, if you invoke it twice, the service can detect this and react apropriately. This is not a required quality, it is something that depends on the concrete semantics of the operation: ordering a chair twice might just mean ordering two chairs.

1.2.2. Service and Transactions

One of the big issues when designing services is to determine how fine or coarse grained the scope of an operation should be. Other technologies such as EJB 's provide a a notion of connections and can maintain conversational state over several, consecutive method invocations. JEE application servers provide distributed transactions that can span multiple method invocations.

Typical web services do not provide a notion of session or transactions. The service interface should reflect that by offering operations that coincide with a typical transaction.

Fine grained - two method invocations in single transaction
Fine grained - two method invocations in single transaction

This example shows how the client can remotely interact with an EJB using several method invocations. In order to do this the client code has to explicitely open and close a distributed transaction. In return it can commit or roll back all the changes with a simple commit or roll back.

Coarse grained - single operation invocations represents a single transaction
Coarse grained - single operation invocations represents a single transaction

A service consumer should invoke an operation with enough data for the service to handle everything in a single transaction. In this example the two method invocations - updateBalance and orderProduct - are turned into one operation - orderProduct . But all the parameters - price and stock number - are now passed together.

It is a very simplified example to illustrate the principle. In general the operations therefore require typically a lot more data compared to other local and remote technologies.

Another more concrete example is how a typical web shop application should interact as a service consumer with the service:

The consumer application maintains the conversational state with the browser/user
The consumer application maintains the conversational state with the browser/user

The web application assembles user data in a ShoppingCart object until the user wants to 'commit' his order by checking out. Only at that point does the application invoke the order operation, passing all the necessary product information such as sku -number, quantity and probably customer identification to the service.

1.3. SOA Example

This example is mainly provided as an illustration on how different components in a SOA work together. It is not a complete solution...

1.3.1. Introduction

We have a typical architecture that provides different sets of functionality to different sets of users, leverages existing investments in code and guarantees different non-functional requirements which we will ignore for the moment.

We have several applications

  • A Dashboard application that provides management with BI to support taking insightful decisions.
  • A CRM application heavily used by the sales department.
  • A Helpdesk application, providing information about products and customers. Used by a call center.
  • A Financial application is used by financial managers and accountants to do monthly billing and invoicing.
  • An Infrastructure application is used by the IT department to install, upgrade and configure services and applications.

If we look at the CRM and Financial applications, you will see that both need data about customers, such as addresses.

In a well managed enterprise SOA, an architect will influence the design of services in such a way that they can be more easily reused later.

Example of an SOA
Example of an SOA

For the sake of argument, we assume that all services and consumers are implemented in Java technologies. This is of course not required, services can be implemented and used in a lot of programming languages.

1.3.2. Service Consumer

Our consumers in the enterprise setting are typically web applications.

In this example we have a CRM application build on JSP technology, hosted on company servers and that can be accessed to enter new customer data from desktops and laptops, used by sales reps.

A subset of this application is available for browsing on cell phones with limited capabilities - for a quick lookup of alternate contacts, phone numbers etc.

Workflow (orchestration) in this kind of applications is simple, so it would be probably handled implicitly from an MVC framework such as Spring MVC

Both applications use AXIS infrastructure to access the services over SOAP and HTTP.

Example of a SOA - Consumer
Example of a SOA - Consumer

The WSDL-knowledge input symbolically shows that this service consumer implicitly needs to know a lot of about the actual service: parameter types, data types, return values,... In an ESB architecture, this is externalized by the mediation function of the ESB

The consumer must also "understand" the semantics of the service.

Each consumer also needs to know where to find the service on the network. This is solved by the routing function when using an ESB architecture.

1.3.3. Service Provider

The service itself is plain java, using hibernate or jdbc to map objects to the database - in this example each service owns its own database.

The AXIS instance is hosted on a server and provides the business logic to service consumers such as our CRM application.

Example of a SOA - Provider
Example of a SOA - Provider

1.3.4. Some Implementation Details

In this section we will take a look at some implementation details when providing and consuming web services with Java.

This section is meant for developers, curious about implementation.

JEE 5 provides means to offer access to session bean methods via the JAX-WS API.

Axis provides means to offer access to methods of POJOs as web services. Since this is the cleanest way to implement web services, we will use that as an example.

1.3.4.1. WS with Apache Axis

It is not my intention to provide a complete WS with Axis tutorial, but a few quick highlights of the vital components when deploying a simple WS.

Axis consists of a servlet that uses the service class - CatalogServiceMemoryBased in the example - and a configuration file - deployCatalog.wsdd to make your service available over the network. It contains an administrative servlet AdminServlet that helps deploying services too.

1.3.4.1.1. Providing the Service

We have here a very simple service that returns a list of products. Our implementation class creates the same list from scratch, but you could get it from a database instead. That's left as an exercise for the reader.

We abstract this with an java interface, always a good idea when using something over the network.

package be.ooxs.examples.wsaxis.common;

import java.util.List;

public interface CatalogService {
	public List<Product> getAvailableProducts();
}

We also have some helper classes that encapsulate data, you might know this as the DTO pattern.

Here is the Price

package be.ooxs.examples.wsaxis.common;

import java.math.BigDecimal;

public class Price {
	private BigDecimal value;

	public BigDecimal getValue() {
		return value;
	}

	public void setValue(BigDecimal value) {
		this.value = value;
	}
	
}

Here is the product:

package be.ooxs.examples.wsaxis.common;

public class Product {
	private String name;
	private String sku;
	private Price unitPrice;
	
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getSku() {
		return sku;
	}
	public void setSku(String sku) {
		this.sku = sku;
	}
	public Price getUnitPrice() {
		return unitPrice;
	}
	public void setUnitPrice(Price unitPrice) {
		this.unitPrice = unitPrice;
	}
	public String toPlainString(){
		return "Product "+name+" "+sku+" price: "+unitPrice.getValue();
	}
	
}
	

Here we have the Web Service Deployment Descriptor (wsdd) , an AXIS specific file.

<?xml version="1.0" encoding="UTF-8"?>

<deployment xmlns="http://xml.apache.org/axis/wsdd/"
	xmlns:java="http://xml.apache.org/axis/wsdd/providers/java">

	<service name="cat" provider="java:RPC">
		<parameter name="className"
			value="be.ooxs.examples.wsaxis.provider.CatalogServiceMemoryBased" />
		<parameter name="interfaceName"
			value="be.ooxs.example.wsaxis.common.CatalogService" />
		<parameter name="allowedMethods" value="*" />
		<namespace>http://catalog.ooxs.be/</namespace>

		<beanMapping qname="ns:Price" xmlns:ns="http://example.ooxs.be/"
			languageSpecificType="java:be.ooxs.examples.wsaxis.common.Price" />

		<beanMapping qname="ns:Product" xmlns:ns="http://example.ooxs.be/"
			languageSpecificType="java:be.ooxs.examples.wsaxis.common.Product" />

	</service>
</deployment>
	

When the AXIS servlet is running, we can use the AdminClient to deploy our service.

Although you can deploy the service at run time, you still need to provide the classes to the AXIS instance. This means in practice restarting that AXIS instance.

This is a Linux script file (bash), but it is a plain java invocation. It should be trivial to do this on other platforms...

	
export AXIS= your/path/to/axis/lib

export CLASSPATH="../classes":
export CLASSPATH=$CLASSPATH"$AXIS/axis-ant.jar":"$AXIS/axis.jar":
export CLASSPATH=$CLASSPATH"$AXIS/commons-discovery-0.2.jar":"$AXIS/commons-logging-1.0.4.jar":
export CLASSPATH=$CLASSPATH"$AXIS/jaxrpc.jar":"$AXIS/log4j-1.2.8.jar":"$AXIS/log4j.properties":
export CLASSPATH=$CLASSPATH"$AXIS/saaj.jar":"$AXIS/wsdl4j-1.5.1.jar"

java -cp $CLASSPATH org.apache.axis.client.AdminClient deployCatalog.wsdd -lhttp://localhost:8080/axis/servlet/AxisServlet
	

This is the WSDL generated by AXIS, formatted for your comfort

<wsdl:definitions targetNamespace="http://catalog.ooxs.be/">

<wsdl:definitions targetNamespace="http://catalog.ooxs.be/">
<!--
WSDL created by Apache Axis version: 1.4
-->

	<wsdl:types>

		<schema targetNamespace="http://catalog.ooxs.be/">
			<import namespace="http://xml.apache.org/xml-soap"/>
			<import namespace="http://schemas.xmlsoap.org/soap/encoding/"/>

			<complexType name="ArrayOf_xsd_anyType">

				<complexContent>

					<restriction base="soapenc:Array">
						<attribute ref="soapenc:arrayType" wsdl:arrayType="xsd:anyType[]"/>
					</restriction>
				</complexContent>
			</complexType>
		</schema>

		<schema targetNamespace="http://xml.apache.org/xml-soap">
			<import namespace="http://catalog.ooxs.be/"/>
			<import namespace="http://schemas.xmlsoap.org/soap/encoding/"/>

			<complexType name="Vector">

				<sequence>
					<element maxOccurs="unbounded" minOccurs="0" name="item" type="xsd:anyType"/>
				</sequence>
			</complexType>
		</schema>
	</wsdl:types>

	<wsdl:message name="getAvailableProductsResponse">
		<wsdl:part name="getAvailableProductsReturn" type="impl:ArrayOf_xsd_anyType"/>
	</wsdl:message>
	<wsdl:message name="getAvailableProductsRequest">

	</wsdl:message>

	<wsdl:portType name="CatalogServiceMemoryBased">

		<wsdl:operation name="getAvailableProducts">
			<wsdl:input message="impl:getAvailableProductsRequest" name="getAvailableProductsRequest"/>
			<wsdl:output message="impl:getAvailableProductsResponse" name="getAvailableProductsResponse"/>
		</wsdl:operation>
	</wsdl:portType>

	<wsdl:binding name="catSoapBinding" type="impl:CatalogServiceMemoryBased">
		<wsdlsoap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>

		<wsdl:operation name="getAvailableProducts">
			<wsdlsoap:operation soapAction=""/>

			<wsdl:input name="getAvailableProductsRequest">
				<wsdlsoap:body encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" namespace="http://catalog.ooxs.be/" use="encoded"/>
			</wsdl:input>

			<wsdl:output name="getAvailableProductsResponse">
				<wsdlsoap:body encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" namespace="http://catalog.ooxs.be/" use="encoded"/>
			</wsdl:output>
		</wsdl:operation>
	</wsdl:binding>

	<wsdl:service name="CatalogServiceMemoryBasedService">

		<wsdl:port binding="impl:catSoapBinding" name="cat">
			<wsdlsoap:address location="http://localhost:8080/ExampleWSWithAxis2/services/cat"/>
		</wsdl:port>
	</wsdl:service>
</wsdl:definitions>
1.3.4.1.2. Consuming the Service

Although one of the big selling points for web services is that it is loosely coupled, a client is still bound (coupled) to the service by its interface - that is the operations with their arguments and return values. That is not necessarily the same thing as a Java interface.

In an environment where both end points - consumer and provider - are implemented in Java and under your control, it is a good idea to share a common set of interfaces and classes. In our example those shared, common classes are

  • CatalogService
  • Product
  • Price

We need to a little bit of tedious repetitive coding:

import javax.xml.namespace.QName;
import javax.xml.rpc.ServiceException;

import org.apache.axis.client.Call;
import org.apache.axis.client.Service;
import org.apache.axis.encoding.ser.BeanDeserializerFactory;
import org.apache.axis.encoding.ser.BeanSerializerFactory;
...

public static void main(String[] args){
	Service service = new Service();
	Call call = (Call) service.createCall();
	
	//Configure address, operation name and carefully map each type used in the service
	call.setTargetEndpointAddress(new java.net.URL("http://localhost:8080/ExampleWSWithAxis2/services/cat"));
	call.setOperationName(new QName("http://soapinterop.org/", "getAvailableProducts"));
	call.registerTypeMapping(Price.class, 
		new QName("http://example.ooxs.be/", "Price"), 
		BeanSerializerFactory.class, 
		BeanDeserializerFactory.class);
	call.registerTypeMapping(Product.class, 
		new QName("http://example.ooxs.be/", "Product"), 
		BeanSerializerFactory.class, 
		BeanDeserializerFactory.class);
	
	Object o = call.invoke(new Object[] {});
	
	Object[] array = (Object[]) o;
	for (Object object : array) {
		System.out.println(((Product) object).toPlainString());
	}
	System.out.println("Done");
}
...	

OK that is the basic stuff.

But I would feel bad about giving bad examples. Here is a slightly refactored version that is a better starting point and example.

First an abstract base class that can be extended for each separate service you will use.

package be.ooxs.examples.wsaxis.consumer;

import java.net.MalformedURLException;
import java.rmi.RemoteException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import javax.xml.namespace.QName;
import javax.xml.rpc.ServiceException;

import org.apache.axis.client.Call;
import org.apache.axis.client.Service;
import org.apache.axis.encoding.ser.BeanDeserializerFactory;
import org.apache.axis.encoding.ser.BeanSerializerFactory;

import be.ooxs.examples.wsaxis.common.Price;
import be.ooxs.examples.wsaxis.common.Product;

public abstract class AbstractServiceConsumer {
	private Map<QName, Class<?>> registeredParameterTypes;
	{
		//in production environment, use Spring IOC instead
		registeredParameterTypes = new HashMap<QName, Class<?>>();
		registeredParameterTypes.put(new QName("http://example.ooxs.be/", "Price"), Price.class);
		registeredParameterTypes.put(new QName("http://example.ooxs.be/", "Product"), Product.class);
	}

	private void configureParameterTypes(Call call) {
		for (Entry<QName, Class<?>> entry : registeredParameterTypes.entrySet()) {
			registerSOAPType(call, entry.getValue(), entry.getKey());
		}
	}

	private void registerSOAPType(Call call, Class<?> javaClass, QName qName) {
		call.registerTypeMapping(javaClass, qName, BeanSerializerFactory.class, BeanDeserializerFactory.class);
	}

	protected Object invoke(String methodName) throws RemoteException, MalformedURLException, ServiceException {
		String endpoint = "http://localhost:8080/ExampleWSWithAxis2/services/cat";

		Service service = new Service();
		Call call = (Call) service.createCall();

		configureParameterTypes(call);
		call.setTargetEndpointAddress(new java.net.URL(endpoint));
		call.setOperationName(new QName("http://soapinterop.org/", "getAvailableProducts"));

		Object o = call.invoke(new Object[] {});
		return o;
	}

	protected <T> List<T> convert(Object[] array, Class<T> type) {
		List<T> list = new ArrayList<T>();
		for (Object object : array) {
			list.add(type.cast(object));
		}
		return list;
	}

}

Next, a concrete implementation for our CatalogService

package be.ooxs.examples.wsaxis.consumer;

import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import be.ooxs.examples.wsaxis.common.CatalogService;
import be.ooxs.examples.wsaxis.common.Product;

public class SimpleCatalogConsumer extends AbstractServiceConsumer implements CatalogService {

	@Override
	public List<Product> getAvailableProducts() {
		try {
			Object o = invoke("getAvailableProducts");
			Object[] array = (Object[]) o;
			return convert(array, Product.class);
		} catch (Exception exception) {
			Logger.getLogger(SimpleCatalogConsumer.class.getName()).log(Level.SEVERE, "getAvailableProducts threw Exception", exception);
			throw new RuntimeException(exception);
		}
	}

}