When implementing a web service client, it is a good practice to take into account the scenario where the web service call takes a long time to complete. In this case, a timeout at client side could be used in order to avoid that the client remains blocked for a significant period of time.

The following step by step tutorial illustrates an example in which we will configure a Spring-WS timeout at client side. In addition, we will show how to handle the timeout exception. The example will use Spring Boot and Maven in order to configure, build and run.

If you want to learn more about Spring WS - head on over to the Spring-WS tutorials page.

General Project Setup

Tools used:

  • Spring-WS 2.4
  • HttpClient 4.5
  • Spring Boot 1.5
  • Maven 3.5

The setup of the example is based on a previous Spring WS tutorial in which we have swapped out the basic helloworld.wsdl for a more generic ticketagent.wsdl from the W3C WSDL 1.1 specification.

There are two implementations of the WebServiceMessageSender interface for sending messages via HTTP. The default implementation is the HttpUrlConnectionMessageSender, which uses the facilities provided by Java itself. The alternative is the HttpComponentsMessageSender, which uses the Apache HttpComponents HttpClient.

We will use the HttpComponentsMessageSender implementation in below example as it contains more advanced and easy-to-use functionality. On GitHub, however, we have also added a timeout example that uses the HttpUrlConnectionMessageSender implementation in case a dependency on the HttpClient is not desired.

As the HttpComponentsMessageSender has a dependency on the Apache HttpClient, we need to add the dependency to the Maven POM file.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.codenotfound</groupId>
  <artifactId>spring-ws-timeout-httpclient</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>spring-ws-timeout-httpclient</name>
  <description>Spring WS - Timeout Example</description>
  <url>https://www.codenotfound.com/spring-ws-client-timeout-example.html</url>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.9.RELEASE</version>
  </parent>

  <properties>
    <java.version>1.8</java.version>
    <httpclient.version>4.5.4</httpclient.version>
    <maven-jaxb2-plugin.version>0.13.3</maven-jaxb2-plugin.version>
  </properties>

  <dependencies>
    <!-- spring-boot -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web-services</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <!-- httpclient -->
    <dependency>
      <groupId>org.apache.httpcomponents</groupId>
      <artifactId>httpclient</artifactId>
      <version>${httpclient.version}</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <!-- spring-boot-maven-plugin -->
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
      <!-- maven-jaxb2-plugin -->
      <plugin>
        <groupId>org.jvnet.jaxb2.maven2</groupId>
        <artifactId>maven-jaxb2-plugin</artifactId>
        <version>${maven-jaxb2-plugin.version}</version>
        <executions>
          <execution>
            <goals>
              <goal>generate</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <schemaDirectory>${project.basedir}/src/main/resources/wsdl</schemaDirectory>
          <schemaIncludes>
            <include>*.wsdl</include>
          </schemaIncludes>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Setting the Template Timeout

As mentioned above, in this example we will use the WebServiceMessageSender implementation that uses the Apache HttpComponents HttpClient. There are two setters that allow controlling how long the client will wait. The setConnectionTimeout() specifies how long the client will wait before a connection to the server is successfully established. The setReadTimeout() configures how long the client will wait for a response once the request has been sent.

In the below example we have created a webServiceMessageSender bean on which we have set both timeouts with the same timeout value that is loaded from the application.yml properties file shown below.

client:
  default-uri: http://localhost:9090/codenotfound/ws/helloworld
  timeout: 2000

In order to use the webServiceMessageSender bean in our client we need to set it onto the webServiceTemplate using the setMessageSender() method.

package com.codenotfound.ws.client;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import org.springframework.ws.client.core.WebServiceTemplate;
import org.springframework.ws.transport.WebServiceMessageSender;
import org.springframework.ws.transport.http.HttpComponentsMessageSender;

@Configuration
public class ClientConfig {

  @Value("${client.default-uri}")
  private String defaultUri;

  @Value("${client.timeout}")
  private int timeout;

  @Bean
  Jaxb2Marshaller jaxb2Marshaller() {
    Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();
    jaxb2Marshaller.setContextPath("org.example.ticketagent");

    return jaxb2Marshaller;
  }

  @Bean
  public WebServiceTemplate webServiceTemplate() {
    WebServiceTemplate webServiceTemplate = new WebServiceTemplate();
    webServiceTemplate.setMarshaller(jaxb2Marshaller());
    webServiceTemplate.setUnmarshaller(jaxb2Marshaller());
    webServiceTemplate.setDefaultUri(defaultUri);
    webServiceTemplate.setMessageSender(webServiceMessageSender());

    return webServiceTemplate;
  }

  @Bean
  public WebServiceMessageSender webServiceMessageSender() {
    HttpComponentsMessageSender httpComponentsMessageSender = new HttpComponentsMessageSender();
    // timeout for creating a connection
    httpComponentsMessageSender.setConnectionTimeout(timeout);
    // when you have a connection, timeout the read blocks for
    httpComponentsMessageSender.setReadTimeout(timeout);

    return httpComponentsMessageSender;
  }
}

Catching the Timeout Exception

The WebServiceTemplate will now throw an exception in case a timeout occurs. As such we need to handle this in our TicketAgentClient implementation.

We add a try/catch block around the marshalSendAndReceive() method in order to catch the timeout exception. If the exception occurs we log it and create an empty flight list to be returned.

package com.codenotfound.ws.client;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.JAXBElement;

import org.example.ticketagent.ObjectFactory;
import org.example.ticketagent.TFlightsResponse;
import org.example.ticketagent.TListFlights;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.ws.client.core.WebServiceTemplate;

@Component
public class TicketAgentClient {

  private static final Logger LOGGER = LoggerFactory.getLogger(TicketAgentClient.class);

  @Autowired
  private WebServiceTemplate webServiceTemplate;

  @SuppressWarnings("unchecked")
  public List<BigInteger> listFlights() {
    ObjectFactory factory = new ObjectFactory();
    TListFlights tListFlights = factory.createTListFlights();

    JAXBElement<TListFlights> request = factory.createListFlightsRequest(tListFlights);
    JAXBElement<TFlightsResponse> response = null;

    try {
      response = (JAXBElement<TFlightsResponse>) webServiceTemplate.marshalSendAndReceive(request);
      return response.getValue().getFlightNumber();
    } catch (Exception e) {
      LOGGER.error(e.getMessage());
      // TODO handle the exception
      return new ArrayList<>();
    }
  }
}

Testing the Client Timeout

To wrap up we will create a basic unit test case in which a timeout exception will be triggered on the client side. In order to achieve this, we will modify the TicketAgentEndpoint implementation and introduce a sleep() of 10 seconds before a response is returned.

package com.codenotfound.ws.endpoint;

import java.math.BigInteger;

import javax.xml.bind.JAXBElement;

import org.example.ticketagent.ObjectFactory;
import org.example.ticketagent.TFlightsResponse;
import org.example.ticketagent.TListFlights;
import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.server.endpoint.annotation.PayloadRoot;
import org.springframework.ws.server.endpoint.annotation.RequestPayload;
import org.springframework.ws.server.endpoint.annotation.ResponsePayload;

@Endpoint
public class TicketAgentEndpoint {

  @PayloadRoot(namespace = "http://example.org/TicketAgent.xsd", localPart = "listFlightsRequest")
  @ResponsePayload
  public JAXBElement<TFlightsResponse> listFlights(
      @RequestPayload JAXBElement<TListFlights> request) throws InterruptedException {

    // sleep for 10 seconds so a timeout occurs
    Thread.sleep(10 * 1000);

    ObjectFactory factory = new ObjectFactory();
    TFlightsResponse tFlightsResponse = factory.createTFlightsResponse();
    tFlightsResponse.getFlightNumber().add(BigInteger.valueOf(101));

    return factory.createListFlightsResponse(tFlightsResponse);
  }
}

In addition, we change the existing test case to expect an empty flight list as shown below.

package com.codenotfound.ws;

import static org.assertj.core.api.Assertions.assertThat;

import java.math.BigInteger;
import java.util.List;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.test.context.junit4.SpringRunner;

import com.codenotfound.ws.client.TicketAgentClient;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT)
public class SpringWsApplicationTests {

  @Autowired
  private TicketAgentClient ticketAgentClient;

  @Test
  public void testListFlights() {
    List<BigInteger> flights = ticketAgentClient.listFlights();

    assertThat(flights.size()).isEqualTo(0);
  }
}

We are now ready to test the timeout. Open a command prompt in the projects root folder and execute following Maven command:

mvn test

The result should be a successful build during which the timeout exception error is logged as shown below:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.9.RELEASE)

15:44:29.142 [main] INFO  c.c.ws.SpringWsApplicationTests - Starting SpringWsApplicationTests on cnf-pc with PID 712 (started by CodeNotFound in c:\code\spring-ws\spring-ws-timeout-httpclient)
15:44:29.145 [main] INFO  c.c.ws.SpringWsApplicationTests - No active profile set, falling back to default profiles: default
15:44:31.824 [main] INFO  c.c.ws.SpringWsApplicationTests - Started SpringWsApplicationTests in 2.988 seconds (JVM running for 3.637)
15:44:33.951 [main] ERROR c.c.ws.client.TicketAgentClient - I/O error: Read timed out; nested exception is java.net.SocketTimeoutException: Read timed out
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 5.228 sec - in com.codenotfound.ws.SpringWsApplicationTests

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 9.903 s
[INFO] Finished at: 2017-12-08T15:44:36+01:00
[INFO] Final Memory: 19M/228M
[INFO] ------------------------------------------------------------------------

github mark If you would like to run the above code sample you can get the full source code here.

This concludes our example of how to configure a client timeout and catch the corresponding exception using Spring-WS and Spring Boot.

Drop a line if you enjoyed reading this post or in case you have a question you would like to ask.

Leave a Comment