SOAP Services
SOAP (Simple Object Access Protocol) is a protocol for exchanging structured information in web services using XML. It provides a standardized way to communicate between applications over HTTP, HTTPS, and other protocols, making it ideal for enterprise-level distributed systems.
SOAP Message Structure
Basic SOAP Envelope
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Header>
<!-- Optional header information -->
<authentication xmlns="http://example.com/auth">
<username>user123</username>
<token>abc123def456</token>
</authentication>
</soap:Header>
<soap:Body>
<!-- Required body containing the actual message -->
<getProduct xmlns="http://example.com/catalog">
<productId>12345</productId>
</getProduct>
</soap:Body>
</soap:Envelope>
SOAP Response Message
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<getProductResponse xmlns="http://example.com/catalog">
<product>
<id>12345</id>
<name>Wireless Headphones</name>
<category>Electronics</category>
<price currency="USD">99.99</price>
<description>High-quality wireless headphones</description>
<inStock>true</inStock>
</product>
</getProductResponse>
</soap:Body>
</soap:Envelope>
SOAP Fault Messages
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<soap:Fault>
<faultcode>Client</faultcode>
<faultstring>Invalid product ID</faultstring>
<faultactor>http://example.com/catalog/service</faultactor>
<detail>
<error xmlns="http://example.com/errors">
<code>PRODUCT_NOT_FOUND</code>
<message>Product with ID 12345 does not exist</message>
<timestamp>2023-07-10T14:30:00Z</timestamp>
</error>
</detail>
</soap:Fault>
</soap:Body>
</soap:Envelope>
WSDL (Web Service Description Language)
Complete WSDL Example
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:tns="http://example.com/catalog"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://example.com/catalog">
<!-- Data Types -->
<types>
<xsd:schema targetNamespace="http://example.com/catalog">
<!-- Product Type -->
<xsd:complexType name="Product">
<xsd:sequence>
<xsd:element name="id" type="xsd:string"/>
<xsd:element name="name" type="xsd:string"/>
<xsd:element name="category" type="xsd:string"/>
<xsd:element name="price" type="tns:Price"/>
<xsd:element name="description" type="xsd:string" minOccurs="0"/>
<xsd:element name="inStock" type="xsd:boolean"/>
</xsd:sequence>
</xsd:complexType>
<!-- Price Type -->
<xsd:complexType name="Price">
<xsd:simpleContent>
<xsd:extension base="xsd:decimal">
<xsd:attribute name="currency" type="xsd:string" use="required"/>
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
<!-- Request/Response Elements -->
<xsd:element name="getProduct">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="productId" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="getProductResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="product" type="tns:Product"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="searchProducts">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="query" type="xsd:string"/>
<xsd:element name="category" type="xsd:string" minOccurs="0"/>
<xsd:element name="maxResults" type="xsd:int" default="10"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="searchProductsResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="products" type="tns:Product" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:schema>
</types>
<!-- Messages -->
<message name="getProductRequest">
<part name="parameters" element="tns:getProduct"/>
</message>
<message name="getProductResponse">
<part name="parameters" element="tns:getProductResponse"/>
</message>
<message name="searchProductsRequest">
<part name="parameters" element="tns:searchProducts"/>
</message>
<message name="searchProductsResponse">
<part name="parameters" element="tns:searchProductsResponse"/>
</message>
<!-- Port Type (Interface) -->
<portType name="CatalogServicePortType">
<operation name="getProduct">
<documentation>Retrieve a product by ID</documentation>
<input message="tns:getProductRequest"/>
<output message="tns:getProductResponse"/>
</operation>
<operation name="searchProducts">
<documentation>Search for products</documentation>
<input message="tns:searchProductsRequest"/>
<output message="tns:searchProductsResponse"/>
</operation>
</portType>
<!-- Binding -->
<binding name="CatalogServiceBinding" type="tns:CatalogServicePortType">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<operation name="getProduct">
<soap:operation soapAction="http://example.com/catalog/getProduct"/>
<input>
<soap:body use="literal"/>
</input>
<output>
<soap:body use="literal"/>
</output>
</operation>
<operation name="searchProducts">
<soap:operation soapAction="http://example.com/catalog/searchProducts"/>
<input>
<soap:body use="literal"/>
</input>
<output>
<soap:body use="literal"/>
</output>
</operation>
</binding>
<!-- Service -->
<service name="CatalogService">
<documentation>Product catalog web service</documentation>
<port name="CatalogServicePort" binding="tns:CatalogServiceBinding">
<soap:address location="http://example.com/services/catalog"/>
</port>
</service>
</definitions>
Java SOAP Implementation
JAX-WS Server Implementation
@WebService(
serviceName = "CatalogService",
portName = "CatalogServicePort",
targetNamespace = "http://example.com/catalog",
wsdlLocation = "CatalogService.wsdl"
)
@SOAPBinding(style = SOAPBinding.Style.DOCUMENT)
public class CatalogServiceImpl implements CatalogService {
private final ProductRepository productRepository;
public CatalogServiceImpl() {
this.productRepository = new ProductRepository();
}
@WebMethod(operationName = "getProduct")
@WebResult(name = "product")
public Product getProduct(
@WebParam(name = "productId") String productId) throws ServiceException {
try {
Product product = productRepository.findById(productId);
if (product == null) {
throw new ServiceException("PRODUCT_NOT_FOUND",
"Product with ID " + productId + " does not exist");
}
return product;
} catch (Exception e) {
throw new ServiceException("INTERNAL_ERROR",
"Error retrieving product: " + e.getMessage());
}
}
@WebMethod(operationName = "searchProducts")
@WebResult(name = "products")
public List<Product> searchProducts(
@WebParam(name = "query") String query,
@WebParam(name = "category") String category,
@WebParam(name = "maxResults") Integer maxResults) throws ServiceException {
try {
SearchCriteria criteria = new SearchCriteria();
criteria.setQuery(query);
criteria.setCategory(category);
criteria.setMaxResults(maxResults != null ? maxResults : 10);
return productRepository.search(criteria);
} catch (Exception e) {
throw new ServiceException("SEARCH_ERROR",
"Error searching products: " + e.getMessage());
}
}
}
@WebFault(name = "ServiceFault", targetNamespace = "http://example.com/catalog")
public class ServiceException extends Exception {
private final ServiceFault faultInfo;
public ServiceException(String faultCode, String message) {
super(message);
this.faultInfo = new ServiceFault();
this.faultInfo.setFaultCode(faultCode);
this.faultInfo.setFaultString(message);
this.faultInfo.setTimestamp(new Date());
}
public ServiceFault getFaultInfo() {
return faultInfo;
}
}
// Data classes
@XmlRootElement(name = "product")
@XmlType(propOrder = {"id", "name", "category", "price", "description", "inStock"})
public class Product {
@XmlElement(required = true)
private String id;
@XmlElement(required = true)
private String name;
@XmlElement(required = true)
private String category;
@XmlElement(required = true)
private Price price;
@XmlElement
private String description;
@XmlElement(required = true)
private Boolean inStock;
// Constructors, getters, and setters
public Product() {}
public Product(String id, String name, String category, Price price,
String description, Boolean inStock) {
this.id = id;
this.name = name;
this.category = category;
this.price = price;
this.description = description;
this.inStock = inStock;
}
// Getters and setters...
}
@XmlType
public class Price {
@XmlValue
private BigDecimal amount;
@XmlAttribute(required = true)
private String currency;
public Price() {}
public Price(BigDecimal amount, String currency) {
this.amount = amount;
this.currency = currency;
}
// Getters and setters...
}
SOAP Client Implementation
public class CatalogServiceClient {
private final CatalogService catalogService;
public CatalogServiceClient(String serviceUrl) {
try {
URL wsdlUrl = new URL(serviceUrl + "?wsdl");
QName serviceName = new QName("http://example.com/catalog", "CatalogService");
Service service = Service.create(wsdlUrl, serviceName);
this.catalogService = service.getPort(CatalogService.class);
// Configure timeouts
BindingProvider bindingProvider = (BindingProvider) catalogService;
Map<String, Object> requestContext = bindingProvider.getRequestContext();
requestContext.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, serviceUrl);
requestContext.put("com.sun.xml.ws.connect.timeout", 30000);
requestContext.put("com.sun.xml.ws.request.timeout", 60000);
} catch (Exception e) {
throw new RuntimeException("Failed to initialize SOAP client", e);
}
}
public Product getProduct(String productId) throws ServiceException {
try {
return catalogService.getProduct(productId);
} catch (ServiceException e) {
System.err.println("Service error: " + e.getFaultInfo().getFaultString());
throw e;
} catch (Exception e) {
throw new RuntimeException("Client error", e);
}
}
public List<Product> searchProducts(String query, String category, Integer maxResults)
throws ServiceException {
try {
return catalogService.searchProducts(query, category, maxResults);
} catch (ServiceException e) {
System.err.println("Service error: " + e.getFaultInfo().getFaultString());
throw e;
} catch (Exception e) {
throw new RuntimeException("Client error", e);
}
}
// Example usage
public static void main(String[] args) {
CatalogServiceClient client = new CatalogServiceClient(
"http://localhost:8080/services/catalog");
try {
// Get a specific product
Product product = client.getProduct("12345");
System.out.println("Product: " + product.getName() +
" - $" + product.getPrice().getAmount());
// Search products
List<Product> products = client.searchProducts("wireless", "Electronics", 5);
System.out.println("Found " + products.size() + " products");
for (Product p : products) {
System.out.println("- " + p.getName() + ": $" + p.getPrice().getAmount());
}
} catch (ServiceException e) {
System.err.println("Service error: " + e.getMessage());
}
}
}
Advanced SOAP Features
SOAP Headers and Security
@WebService
public class SecureCatalogService {
@WebMethod
public Product getProduct(
@WebParam(name = "productId") String productId,
@WebParam(header = true, name = "AuthHeader") AuthenticationHeader auth)
throws ServiceException {
// Validate authentication
if (!isValidAuth(auth)) {
throw new ServiceException("AUTH_FAILED", "Invalid authentication credentials");
}
// Check authorization
if (!hasPermission(auth.getUsername(), "READ_PRODUCT")) {
throw new ServiceException("INSUFFICIENT_PRIVILEGES",
"User does not have permission to read products");
}
return getProductInternal(productId);
}
private boolean isValidAuth(AuthenticationHeader auth) {
// Implement authentication logic
return auth != null &&
auth.getUsername() != null &&
auth.getToken() != null &&
validateToken(auth.getUsername(), auth.getToken());
}
private boolean hasPermission(String username, String permission) {
// Implement authorization logic
return true; // Simplified
}
}
@XmlRootElement(name = "AuthHeader", namespace = "http://example.com/auth")
public class AuthenticationHeader {
@XmlElement(required = true)
private String username;
@XmlElement(required = true)
private String token;
@XmlElement
private String sessionId;
// Constructors, getters, and setters
}
SOAP Handlers
@HandlerChain(file = "handler-chain.xml")
@WebService
public class CatalogServiceWithHandlers implements CatalogService {
// Service implementation
}
<!-- handler-chain.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<handler-chains xmlns="http://java.sun.com/xml/ns/javaee">
<handler-chain>
<handler>
<handler-name>LoggingHandler</handler-name>
<handler-class>com.example.handlers.LoggingHandler</handler-class>
</handler>
<handler>
<handler-name>AuthenticationHandler</handler-name>
<handler-class>com.example.handlers.AuthenticationHandler</handler-class>
</handler>
<handler>
<handler-name>ValidationHandler</handler-name>
<handler-class>com.example.handlers.ValidationHandler</handler-class>
</handler>
</handler-chain>
</handler-chains>
public class LoggingHandler implements SOAPHandler<SOAPMessageContext> {
@Override
public boolean handleMessage(SOAPMessageContext context) {
Boolean outbound = (Boolean) context.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
try {
SOAPMessage message = context.getMessage();
ByteArrayOutputStream out = new ByteArrayOutputStream();
message.writeTo(out);
String direction = outbound ? "OUTBOUND" : "INBOUND";
System.out.println(direction + " SOAP Message:");
System.out.println(out.toString());
} catch (Exception e) {
System.err.println("Error logging SOAP message: " + e.getMessage());
}
return true; // Continue processing
}
@Override
public boolean handleFault(SOAPMessageContext context) {
System.err.println("SOAP Fault occurred");
return true;
}
@Override
public void close(MessageContext context) {
// Cleanup if needed
}
@Override
public Set<QName> getHeaders() {
return Collections.emptySet();
}
}
public class AuthenticationHandler implements SOAPHandler<SOAPMessageContext> {
@Override
public boolean handleMessage(SOAPMessageContext context) {
Boolean outbound = (Boolean) context.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
if (!outbound) { // Inbound request
try {
SOAPMessage message = context.getMessage();
SOAPHeader header = message.getSOAPHeader();
if (header == null) {
throw new SOAPException("Missing authentication header");
}
// Extract authentication information
Iterator<?> headerElements = header.examineAllHeaderElements();
AuthenticationHeader authHeader = null;
while (headerElements.hasNext()) {
SOAPHeaderElement element = (SOAPHeaderElement) headerElements.next();
if ("AuthHeader".equals(element.getLocalName())) {
authHeader = parseAuthHeader(element);
break;
}
}
if (authHeader == null || !validateAuth(authHeader)) {
SOAPFault fault = message.getSOAPBody().addFault();
fault.setFaultCode("Client");
fault.setFaultString("Authentication failed");
throw new SOAPFaultException(fault);
}
// Store authentication info for use by service
context.put("authenticated.user", authHeader.getUsername());
} catch (SOAPException e) {
throw new RuntimeException("Authentication error", e);
}
}
return true;
}
private AuthenticationHeader parseAuthHeader(SOAPHeaderElement element) {
// Parse authentication header from SOAP element
// Implementation depends on header structure
return new AuthenticationHeader();
}
private boolean validateAuth(AuthenticationHeader authHeader) {
// Implement authentication validation
return true; // Simplified
}
@Override
public boolean handleFault(SOAPMessageContext context) {
return true;
}
@Override
public void close(MessageContext context) {}
@Override
public Set<QName> getHeaders() {
return Set.of(new QName("http://example.com/auth", "AuthHeader"));
}
}
SOAP Testing and Debugging
SoapUI Integration
<!-- Sample SoapUI Test Case -->
<soapui:testSuite xmlns:soapui="http://www.soapui.org/testsuites">
<soapui:testCase name="GetProduct Test">
<soapui:testStep type="request" name="Valid Product Request">
<soapui:config>
<request>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:cat="http://example.com/catalog">
<soap:Header/>
<soap:Body>
<cat:getProduct>
<cat:productId>12345</cat:productId>
</cat:getProduct>
</soap:Body>
</soap:Envelope>
</request>
</soapui:config>
<soapui:assertions>
<soapui:assertion type="SOAP Response"/>
<soapui:assertion type="XPath Match">
<soapui:xpath>//cat:product/cat:id</soapui:xpath>
<soapui:expectedValue>12345</soapui:expectedValue>
</soapui:assertion>
</soapui:assertions>
</soapui:testStep>
<soapui:testStep type="request" name="Invalid Product Request">
<soapui:config>
<request>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:cat="http://example.com/catalog">
<soap:Body>
<cat:getProduct>
<cat:productId>INVALID</cat:productId>
</cat:getProduct>
</soap:Body>
</soap:Envelope>
</request>
</soapui:config>
<soapui:assertions>
<soapui:assertion type="SOAP Fault"/>
<soapui:assertion type="XPath Match">
<soapui:xpath>//soap:Fault/faultcode</soapui:xpath>
<soapui:expectedValue>Client</soapui:expectedValue>
</soapui:assertion>
</soapui:assertions>
</soapui:testStep>
</soapui:testCase>
</soapui:testSuite>
Performance Testing
public class SOAPPerformanceTest {
private CatalogServiceClient client;
private ExecutorService executor;
@Before
public void setUp() {
client = new CatalogServiceClient("http://localhost:8080/services/catalog");
executor = Executors.newFixedThreadPool(10);
}
@Test
public void testConcurrentRequests() throws InterruptedException {
int numberOfRequests = 100;
CountDownLatch latch = new CountDownLatch(numberOfRequests);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger errorCount = new AtomicInteger(0);
long startTime = System.currentTimeMillis();
for (int i = 0; i < numberOfRequests; i++) {
final String productId = "PROD_" + (i % 10); // Simulate different products
executor.submit(() -> {
try {
Product product = client.getProduct(productId);
if (product != null) {
successCount.incrementAndGet();
}
} catch (Exception e) {
errorCount.incrementAndGet();
System.err.println("Request failed: " + e.getMessage());
} finally {
latch.countDown();
}
});
}
latch.await(60, TimeUnit.SECONDS);
long duration = System.currentTimeMillis() - startTime;
System.out.printf("Performance Test Results:%n");
System.out.printf("Total requests: %d%n", numberOfRequests);
System.out.printf("Successful: %d%n", successCount.get());
System.out.printf("Errors: %d%n", errorCount.get());
System.out.printf("Duration: %d ms%n", duration);
System.out.printf("Requests per second: %.2f%n",
(numberOfRequests * 1000.0) / duration);
}
@After
public void tearDown() {
executor.shutdown();
}
}
Best Practices
SOAP Service Design
// Good: Clear, focused service interface
@WebService(targetNamespace = "http://example.com/catalog/v2")
public interface CatalogService {
@WebMethod(operationName = "getProduct")
Product getProduct(@WebParam(name = "productId") String productId)
throws ProductNotFoundException, ServiceException;
@WebMethod(operationName = "searchProducts")
ProductSearchResult searchProducts(
@WebParam(name = "searchRequest") ProductSearchRequest request)
throws SearchException, ServiceException;
}
// Better: Use specific request/response objects
public class ProductSearchRequest {
private String query;
private String category;
private PriceRange priceRange;
private SortOptions sortOptions;
private PaginationOptions pagination;
// Getters and setters
}
public class ProductSearchResult {
private List<Product> products;
private int totalCount;
private PaginationInfo pagination;
// Getters and setters
}
Error Handling
// Comprehensive error handling
@WebService
public class RobustCatalogService implements CatalogService {
private static final Logger logger = LoggerFactory.getLogger(RobustCatalogService.class);
@WebMethod
public Product getProduct(@WebParam(name = "productId") String productId)
throws ServiceException {
// Input validation
if (productId == null || productId.trim().isEmpty()) {
throw new ServiceException("INVALID_INPUT",
"Product ID is required and cannot be empty");
}
try {
logger.info("Retrieving product with ID: {}", productId);
Product product = productRepository.findById(productId);
if (product == null) {
logger.warn("Product not found: {}", productId);
throw new ServiceException("PRODUCT_NOT_FOUND",
"Product with ID " + productId + " does not exist");
}
logger.info("Successfully retrieved product: {}", productId);
return product;
} catch (ServiceException e) {
throw e; // Re-throw service exceptions
} catch (Exception e) {
logger.error("Unexpected error retrieving product {}: {}", productId, e.getMessage(), e);
throw new ServiceException("INTERNAL_ERROR",
"An unexpected error occurred while retrieving the product");
}
}
}
Performance Optimization
// Connection pooling and caching
@Component
public class OptimizedSOAPClient {
private final CatalogService catalogService;
private final Cache<String, Product> productCache;
public OptimizedSOAPClient(@Value("${catalog.service.url}") String serviceUrl) {
// Configure connection pooling
System.setProperty("http.maxConnections", "20");
System.setProperty("http.keepAlive", "true");
// Initialize service with optimized settings
this.catalogService = createOptimizedService(serviceUrl);
// Setup caching
this.productCache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
}
private CatalogService createOptimizedService(String serviceUrl) {
try {
URL wsdlUrl = new URL(serviceUrl + "?wsdl");
QName serviceName = new QName("http://example.com/catalog", "CatalogService");
Service service = Service.create(wsdlUrl, serviceName);
CatalogService port = service.getPort(CatalogService.class);
// Configure timeouts and other properties
BindingProvider bp = (BindingProvider) port;
Map<String, Object> context = bp.getRequestContext();
context.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, serviceUrl);
context.put("com.sun.xml.ws.connect.timeout", 5000);
context.put("com.sun.xml.ws.request.timeout", 30000);
return port;
} catch (Exception e) {
throw new RuntimeException("Failed to create optimized SOAP client", e);
}
}
public Product getProduct(String productId) throws ServiceException {
// Check cache first
Product cached = productCache.getIfPresent(productId);
if (cached != null) {
return cached;
}
// Fetch from service
Product product = catalogService.getProduct(productId);
// Cache the result
productCache.put(productId, product);
return product;
}
}
Conclusion
SOAP provides a robust, standardized approach to web services with strong typing, formal contracts (WSDL), and comprehensive error handling. While more heavyweight than REST, SOAP excels in enterprise environments requiring strict contracts, transaction support, and advanced security features.
Next Steps
- Learn REST with XML for lighter-weight alternatives
- Explore WSDL for detailed contract definitions
- Study Web Service Security for securing SOAP services