JAX-WS asynchronous Web Services made simple

TL;DR: Developing responsive JAX-WS clients on modern application servers requires asynchronous web service invocations. This guide updates the reference implementations to Jakarta EE 11 and WildFly 41.0.1.Final (utilizing Apache CXF), detailing how to set up non-blocking Polling and Callback execution strategies using modern JUnit 5 test suites.

Developing rigorous and responsive web service client applications has always been a challenge for architects and developers. JAX-WS (now standard in Jakarta XML Web Services) comes with an effective solution to this problem: asynchronous web service invocation. In this article, we will provide an exposition of this technology with concrete examples built upon WildFly's reference implementation.

SOAP vs. REST in Enterprise Systems

While RESTful architectures dominate modern web APIs due to their lightweight, resource-oriented nature, SOAP remains highly relevant in corporate environments. Enterprises continue to rely on SOAP/JAX-WS because of formal contracts (WSDL), comprehensive security architectures (WS-Security), distributed transaction management (WS-Coordination), and built-in reliable messaging. In these mission-critical scenarios, web service execution times can be unpredictable. This makes asynchronous design patterns essential for maintaining system throughput and responsive user experiences.

The Jakarta XML Web Services programming model offers two models for invoking operations asynchronously – polling and callback. Both methods allow the client thread to continue execution without blocking while waiting for a remote SOAP response.

jboss web services async

Let’s look at a modern example configured for Jakarta EE 11 and WildFly 41. Note that we have fully migrated all namespaces from the legacy javax.jws.* and javax.xml.ws.* APIs to their contemporary jakarta.jws.* and jakarta.xml.ws.* counterparts.

Here is the modern Service Endpoint Interface (SEI):

package org.jboss.test.ws.jaxws.samples.asynchronous;

import java.util.concurrent.Future;
import jakarta.jws.WebMethod;
import jakarta.jws.WebParam;
import jakarta.jws.WebResult;
import jakarta.jws.WebService;
import jakarta.jws.soap.SOAPBinding;
import jakarta.jws.soap.SOAPBinding.Style;
import jakarta.xml.ws.AsyncHandler;
import jakarta.xml.ws.Response;

@WebService(name = "Endpoint", targetNamespace = "http://org.jboss.ws/jaxws/asynchronous") 
@SOAPBinding(style = Style.RPC) 
public interface Endpoint {

  @WebMethod(operationName = "echo") 
  public Response<String> echoAsync(@WebParam(name = "String_1") String string1);

  @WebMethod(operationName = "echo") 
  public Future<?> echoAsync(@WebParam(name = "String_1") String string1, 
                             @WebParam(name = "asyncHandler") AsyncHandler<String> asyncHandler);
  
  @WebMethod 
  @WebResult(name = "result") 
  public String echo(@WebParam(name = "String_1") String string1);
}

And here is the Service Implementation class (SIB). Note that on the server side, you only need to write the standard synchronous business logic. The container's JAX-WS engine (Apache CXF in WildFly) automatically coordinates the asynchronous dispatch mechanisms on behalf of the client:

package org.jboss.test.ws.jaxws.samples.asynchronous;

import jakarta.jws.WebMethod;
import jakarta.jws.WebParam;
import jakarta.jws.WebResult;
import jakarta.jws.WebService;
import jakarta.jws.soap.SOAPBinding;
import jakarta.jws.soap.SOAPBinding.Style;
import org.jboss.logging.Logger;

@WebService(
    endpointInterface = "org.jboss.test.ws.jaxws.samples.asynchronous.Endpoint",
    serviceName = "EndpointBeanService",
    portName = "EndpointPort",
    targetNamespace = "http://org.jboss.ws/jaxws/asynchronous"
)
@SOAPBinding(style = Style.RPC) 
public class EndpointBean implements Endpoint {
  private static final Logger log = Logger.getLogger(EndpointBean.class);

  @Override
  @WebMethod 
  @WebResult(name = "result") 
  public String echo(@WebParam(name = "String_1") String msg) {
    log.info("Received echo request: " + msg);
    return msg;
  }

  // Under Jakarta EE standards, client-side asynchronous wrappers are managed
  // by the proxy generator, and do not need server-side implementation code.
  @Override
  public Response<String> echoAsync(String string1) {
    throw new UnsupportedOperationException("Asynchronous wrappers are handled client-side.");
  }

  @Override
  public Future<?> echoAsync(String string1, AsyncHandler<String> asyncHandler) {
    throw new UnsupportedOperationException("Asynchronous wrappers are handled client-side.");
  }
}

As you can see, the SEI has two echoAsync methods, each representing a different invocation strategy:

public Response<String> echoAsync(@WebParam(name = "String_1") String string1);

This uses a polling or "pull strategy". Once the initial SOAP request is dispatched, control returns immediately to the client. The client is free to execute other operations that do not depend on the web service outcome. When the response is ready, the client fetches the results using the returned Response<T> instance (which extends java.util.concurrent.Future).

On the other hand, using this method:

public Future<?> echoAsync(@WebParam(name = "String_1") String string1, @WebParam(name = "asyncHandler") AsyncHandler<String> asyncHandler);

The response is processed via a callback strategy by the specified AsyncHandler implementation. Once the SOAP response arrives, the engine invokes the handler's callback method.

Callback mechanisms process responses on a different thread from the main client execution flow. This prevents thread starvation and significantly improves scalability when making concurrent remote calls.

Tip: Asynchronous polling clients are especially useful for implementing complex, multi-step orchestration workflows. Asynchronous callback clients are the gold standard for batch integration processing and reactive message-consumer systems.

The Web Service Client

You can implement a modern JAX-WS asynchronous client using two different abstractions: a high-level approach that leverages structural client interfaces (Dynamic Proxy) or a low-level payload representation API (Dispatch).

Web Service Client with Dynamic Proxy

The Dynamic Proxy approach is the standard developer-friendly API for working with SOAP endpoints. This approach generates a Java interface representing the port, using a concrete JDK-managed proxy to serialize calls.

Here is a modern JUnit 5 test demonstrating how to test both the polling and callback dynamic proxy approaches:

package org.jboss.test.ws.jaxws.samples.asynchronous;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import javax.xml.namespace.QName;
import jakarta.xml.ws.AsyncHandler;
import jakarta.xml.ws.Response;
import jakarta.xml.ws.Service;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class AsynchronousProxyTestCase {
  private final String targetNS = "http://org.jboss.ws/jaxws/asynchronous";
  private Exception handlerException;
  private boolean asyncHandlerCalled;

  @Test
  public void testInvokeAsync() throws Exception {
    Endpoint port = createProxy();
    Response<String> response = port.echoAsync("Async");
    
    // While the SOAP call is executing asynchronously, we can perform other work
    assertFalse(response.isDone());

    // Access future result (with safe timeout handling)
    String retStr = response.get(3, TimeUnit.SECONDS);
    assertEquals("Async", retStr);
  }

  @Test
  public void testInvokeAsyncHandler() throws Exception {
    AsyncHandler<String> handler = new AsyncHandler<>() {
      @Override
      public void handleResponse(Response<String> response) {
        try {
          String retStr = response.get(1000, TimeUnit.MILLISECONDS);
          assertEquals("Hello", retStr);
          asyncHandlerCalled = true;
        } catch (Exception ex) {
          handlerException = ex;
        }
      }
    };

    Endpoint port = createProxy();
    Future<?> future = port.echoAsync("Hello", handler);
    
    // Wait for the handler thread to process execution
    future.get(3, TimeUnit.SECONDS);
    
    if (handlerException != null) {
        throw handlerException;
    }
    assertTrue(asyncHandlerCalled, "Async handler callback was not triggered");
  }

  private Endpoint createProxy() throws MalformedURLException {
    // Port 8080 is the default HTTP listener on WildFly 41
    URL wsdlURL = new URL("http://localhost:8080/jaxws-samples-asynchronous/EndpointBean?wsdl");
    QName serviceName = new QName(targetNS, "EndpointBeanService");
    Service service = Service.create(wsdlURL, serviceName);
    return service.getPort(Endpoint.class);
  }
}

In the first test, testInvokeAsync, we utilize the modern standard non-blocking pull mechanism:

Response<String> response = port.echoAsync("Async");      
String retStr = response.get(3, TimeUnit.SECONDS);

The call is decoupled from the underlying connection binding at invocation time. Crucially, the response context does not automatically update the static BindingProvider context. Instead, a clean isolation barrier is preserved using the modern generic Response<T> class wrapper.

Web Service Client with Dispatcher

While dynamic proxies are elegant, they require compile-time SEI Java classes. When operating on raw, dynamic XML streams or payloads, the low-level Dispatch API is preferred.

Here is a complete, modern JUnit 5 implementation utilizing raw XML payloads and standard JDK Transformer APIs to verify execution:

package org.jboss.test.ws.jaxws.samples.asynchronous;

import java.io.StringReader;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import javax.xml.namespace.QName;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import jakarta.xml.ws.AsyncHandler;
import jakarta.xml.ws.Dispatch;
import jakarta.xml.ws.Response;
import jakarta.xml.ws.Service;
import jakarta.xml.ws.Service.Mode;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class AsynchronousDispatchTestCase {
  private final String targetNS = "http://org.jboss.ws/jaxws/asynchronous";
  private final String reqPayload = "<ns2:echo xmlns:ns2=\"" + targetNS + "\"><String_1>Hello</String_1></ns2:echo>";
  
  private Exception handlerException;
  private boolean asyncHandlerCalled;

  @Test
  public void testInvokeAsynch() throws Exception {
    Source reqObj = new StreamSource(new StringReader(reqPayload));
    Response<Source> response = createDispatch().invokeAsync(reqObj);
    
    Source resSource = response.get(3, TimeUnit.SECONDS);
    verifyResponse(resSource);
  }

  @Test
  public void testInvokeAsynchHandler() throws Exception {
    AsyncHandler<Source> handler = new AsyncHandler<>() {
      @Override
      public void handleResponse(Response<Source> response) {
        try {
          verifyResponse(response.get());
          asyncHandlerCalled = true;
        } catch (Exception ex) {
          handlerException = ex;
        }
      }
    };

    StreamSource reqObj = new StreamSource(new StringReader(reqPayload));
    Future<?> future = createDispatch().invokeAsync(reqObj, handler);
    future.get(3, TimeUnit.SECONDS);
    
    if (handlerException != null) {
        throw handlerException;
    }
    assertTrue(asyncHandlerCalled, "Async Dispatch callback was not executed");
  }

  private Dispatch<Source> createDispatch() throws MalformedURLException {
    URL wsdlURL = new URL("http://localhost:8080/jaxws-samples-asynchronous/EndpointBean?wsdl");
    QName serviceName = new QName(targetNS, "EndpointBeanService");
    QName portName = new QName(targetNS, "EndpointPort");
    Service service = Service.create(wsdlURL, serviceName);
    
    return service.createDispatch(portName, Source.class, Mode.PAYLOAD);
  }

  private void verifyResponse(Source result) throws Exception {
    TransformerFactory factory = TransformerFactory.newInstance();
    Transformer transformer = factory.newTransformer();
    StringWriter writer = new StringWriter();
    transformer.transform(result, new StreamResult(writer));
    String resStr = writer.toString();
    
    assertTrue(resStr.contains("result") && resStr.contains("Hello"), 
               "Unexpected XML response: " + resStr);
  }
}

The Dispatch API supports two execution modes, configured by the jakarta.xml.ws.Service.Mode enumeration:

  • Mode.MESSAGE: In this mode, clients process complete physical messages. For SOAP, this means the client's payload must include the SOAP Envelope, Header blocks, and Body elements. This mode is useful when you need to read or manipulate the low-level SOAP transport headers directly.
  • Mode.PAYLOAD: As used in our example above, this mode exposes only the contents of the <soap:Body> element to the client application, filtering out transport envelopes and headers.

JAX-WS Handlers vs. Apache CXF Interceptors

When executing asynchronous SOAP flows, you may need to intercept messages to handle custom logging, security headers, or auditing. On WildFly 41, you can use either standard JAX-WS Handlers or Apache CXF Interceptors:

  • JAX-WS Handlers (Logical and SOAP): Defined by the JAX-WS standard, they are portable across different application servers. However, they rely on the SAAJ (SOAP with Attachments API for Java) model, which parses the entire message into a DOM-like tree in memory. This can lead to performance overhead and high memory consumption on large payloads.
  • Apache CXF Interceptors: These are native to JBossWS-CXF. They use a high-performance streaming model (StAX) to inspect or modify the XML payload on-the-fly without parsing the full message structure into memory. While proprietary, they are the recommended choice on WildFly 41 for performance-critical systems.

Timeout Configuration Best Practices

Asynchronous operations prevent client threads from blocking, but they can still exhaust connection pools if remote servers hang indefinitely. You should always configure both connection and read timeouts on your client proxy. In WildFly 41, you can set these timeouts programmatically using Jakarta XML Web Services properties:

import jakarta.xml.ws.BindingProvider;
import java.util.Map;

// Cast your client proxy to BindingProvider
BindingProvider bp = (BindingProvider) port;
Map<String, Object> requestContext = bp.getRequestContext();

// Set connection timeout (e.g., 5 seconds)
requestContext.put("jakarta.xml.ws.client.connectionTimeout", 5000);

// Set read timeout (e.g., 10 seconds)
requestContext.put("jakarta.xml.ws.client.receiveTimeout", 10000);

Securing Your Modern SOAP Invocations

In enterprise production environments, never transmit sensitive transactional payloads over plain HTTP using basic credentials. Always enforce transport-level security (HTTPS/TLS). For message-level security requirements, use WS-Security policies (via Apache WSS4J integration in CXF) to sign and encrypt the SOAP payload. This ensures that even if an intermediary intercepts the network traffic, the message content remains encrypted and tamper-proof.


Recommended Articles

How to develop CXF Web services with JBoss / WildFly

Learn how to develop, deploy, and secure SOAP Web Services on WildFly 41.0.1.Final using Jakarta EE 11 and Apache CXF. Master JAX-WS handlers, CXF interceptors, and robust cloud configurations.

Developing SOAP Web Services on WildFly 41 (Jakarta EE 11) with Apache CXF

Learn how to develop, deploy, and secure Jakarta XML Web Services (SOAP) on WildFly 41.0.1.Final and Jakarta EE 11 using Apache CXF, WS-Security, and modern testing tools.

Using Axis Web Services with JBoss: Migration to modern JAX-WS with WildFly 41

Learn how to migrate legacy Apache Axis SOAP web services from JBoss to modern Jakarta EE 11 and JAX-WS on WildFly 41.0.1.Final using Apache CXF with secure HTTPS configurations and debugging.

Apache CXF Interceptors on WildFly 41 and Jakarta EE 11

Learn how to write, configure, and customize Apache CXF Interceptors on WildFly 41.0.1.Final and Jakarta EE 11. Upgrade from legacy configuration to modern Jakarta XML Web Services.