Monday, October 21, 2013

A best Spring AsyncRestTemplate!

I wrote an article on the famous Spring's RestTemplate some months ago (http://vincentdevillers.blogspot.fr/2013/02/configure-best-spring-resttemplate.html). Since the last version of Spring 4, currently 4.0.0.M3, or the BUILD-SNAPSHOT for the most courageous, you can use a custom RestTemplate in an async way: the AsyncRestTemplate API.

Why should I use that?
The purpose of this API is to allow Java applications to easily execute HTTP requests and asynchronously process the HTTP responses. In non async mode, the code will block until the response is fully received. In async mode, the code will continue and a listener will warn you about the availability of the response (entire or parts of the response).

As the RestTemple uses a ClientHttpRequestFactory for creating HTTP connections, the AsyncRestTemple uses an... AsyncClientHttpRequestFactory. Actually, only the httpcomponents implementation (HttpComponentsAsyncClientHttpRequestFactory) and a basic implementation using the jdk classes (SimpleClientHttpRequestFactory) are ready to use, but we can bet than many other will come, supporting frameworks like Netty (http://netty.io/), Grizzly (https://grizzly.java.net/) or kryonet (http://code.google.com/p/kryonet/).

NB: SimpleClientHttpRequestFactory implements now both ClientHttpRequestFactory and AsyncClientHttpRequestFactory. The trick is, when using the async way, the SimpleClientHttpRequestFactory relies on a TaskExecutor.

First, you need this:



<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.0.0.M3</version>
</dependency>

<!-- Apache Http Client -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.3</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpasyncclient</artifactId>
<version>4.0-beta4</version>
</dependency>

Add some configuration:


import java.util.List;

import javax.inject.Inject;

import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager;
import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor;
import org.apache.http.impl.nio.reactor.IOReactorConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.AsyncClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsAsyncClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.AsyncRestTemplate;
import org.springframework.web.client.RestTemplate;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Throwables;

@Configuration
public class HttpConfig {

private static final int DEFAULT_MAX_TOTAL_CONNECTIONS = 100;

private static final int DEFAULT_MAX_CONNECTIONS_PER_ROUTE = 5;

private static final int DEFAULT_READ_TIMEOUT_MILLISECONDS = (60 * 1000);

@Inject
private ObjectMapper objectMapper;

// ################################################### SYNC
@Bean
public ClientHttpRequestFactory httpRequestFactory() {
return new HttpComponentsClientHttpRequestFactory(httpClient());
}

@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate(httpRequestFactory());
List<HttpMessageConverter<?>> converters = restTemplate
.getMessageConverters();

for (HttpMessageConverter<?> converter : converters) {
if (converter instanceof MappingJackson2HttpMessageConverter) {
MappingJackson2HttpMessageConverter jsonConverter = (MappingJackson2HttpMessageConverter) converter;
jsonConverter.setObjectMapper(objectMapper);
}
}
return restTemplate;
}

@Bean
public CloseableHttpClient httpClient() {
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(DEFAULT_MAX_TOTAL_CONNECTIONS);
connectionManager
.setDefaultMaxPerRoute(DEFAULT_MAX_CONNECTIONS_PER_ROUTE);
connectionManager.setMaxPerRoute(new HttpRoute(new HttpHost(
"facebook.com")), 20);
connectionManager.setMaxPerRoute(new HttpRoute(new HttpHost(
"twitter.com")), 20);
connectionManager.setMaxPerRoute(new HttpRoute(new HttpHost(
"linkedin.com")), 20);
connectionManager.setMaxPerRoute(new HttpRoute(new HttpHost(
"viadeo.com")), 20);
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(DEFAULT_READ_TIMEOUT_MILLISECONDS).build();

CloseableHttpClient defaultHttpClient = HttpClientBuilder.create()
.setConnectionManager(connectionManager)
.setDefaultRequestConfig(config).build();
return defaultHttpClient;
}

// ################################################### ASYNC
@Bean
public AsyncClientHttpRequestFactory asyncHttpRequestFactory() {
return new HttpComponentsAsyncClientHttpRequestFactory(
asyncHttpClient());
}

@Bean
public AsyncRestTemplate asyncRestTemplate() {
AsyncRestTemplate restTemplate = new AsyncRestTemplate(
asyncHttpRequestFactory(), restTemplate());
return restTemplate;
}

@Bean
public CloseableHttpAsyncClient asyncHttpClient() {
try {
PoolingNHttpClientConnectionManager connectionManager = new PoolingNHttpClientConnectionManager(
new DefaultConnectingIOReactor(IOReactorConfig.DEFAULT));
connectionManager.setMaxTotal(DEFAULT_MAX_TOTAL_CONNECTIONS);
connectionManager
.setDefaultMaxPerRoute(DEFAULT_MAX_CONNECTIONS_PER_ROUTE);
connectionManager.setMaxPerRoute(new HttpRoute(new HttpHost(
"facebook.com")), 20);
connectionManager.setMaxPerRoute(new HttpRoute(new HttpHost(
"twitter.com")), 20);
connectionManager.setMaxPerRoute(new HttpRoute(new HttpHost(
"linkedin.com")), 20);
connectionManager.setMaxPerRoute(new HttpRoute(new HttpHost(
"viadeo.com")), 20);
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(DEFAULT_READ_TIMEOUT_MILLISECONDS)
.build();

CloseableHttpAsyncClient httpclient = HttpAsyncClientBuilder
.create().setConnectionManager(connectionManager)
.setDefaultRequestConfig(config).build();
return httpclient;
} catch (Exception e) {
throw Throwables.propagate(e);
}
}
}

As you can see, the AsyncRestTemplate uses an underlying RestTemplate. This template is used for all the configuration (http message converters, errors handler...).

And then test:


import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import javax.inject.Inject;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.AnnotationConfigContextLoader;
import org.springframework.web.client.AsyncRestTemplate;
import org.springframework.web.client.RestTemplate;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.JdkFutureAdapters;
import com.leguide.Feed.BindConfig;

@ContextConfiguration(loader = AnnotationConfigContextLoader.class, classes = {
HttpConfig.class, BindConfig.class })
@RunWith(SpringJUnit4ClassRunner.class)
public class Feed {

@Configuration
static class BindConfig {

@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
}

@Inject
private RestTemplate restTemplate;

@Inject
private AsyncRestTemplate asyncRestTemplate;

@Test
public void sync() {

// Arrange
String url = "https://api.github.com/users/treydone";

// Actions
ResponseEntity<Map> entity = restTemplate.getForEntity(url, Map.class);

// Asserts
assertTrue(entity.getStatusCode().equals(HttpStatus.OK));
assertEquals("Treydone", entity.getBody().get("login"));
}

@Test
public void async_withFuture() throws InterruptedException,
ExecutionException {

// Arrange
String url = "https://api.github.com/users/treydone";

// Actions
Future<ResponseEntity<Map>> future = asyncRestTemplate.getForEntity(
url, Map.class);

while (!future.isDone()) {
TimeUnit.MILLISECONDS.sleep(100);
}

ResponseEntity<Map> entity = future.get();

// Asserts
assertTrue(entity.getStatusCode().equals(HttpStatus.OK));
assertEquals("Treydone", entity.getBody().get("login"));
}

@Test
public void async_withCallback() throws InterruptedException {

// Arrange
String url = "https://api.github.com/users/treydone";
ExecutorService executorService = Executors.newFixedThreadPool(1);

// Actions
Futures.addCallback(
JdkFutureAdapters.listenInPoolThread(
asyncRestTemplate.getForEntity(url, Map.class),
executorService),
new FutureCallback<ResponseEntity<Map>>() {

@Override
public void onSuccess(ResponseEntity<Map> entity) {
// Asserts
assertTrue(entity.getStatusCode().equals(HttpStatus.OK));
assertEquals("Treydone", entity.getBody().get("login"));
}

@Override
public void onFailure(Throwable t) {

}
}, executorService);

TimeUnit.SECONDS.sleep(3);
}
}

You can find more examples on Github: https://github.com/spring-projects/spring-framework/tree/master/spring-web/src/test/java/org/springframework/http/client


The new async API for RestTemplate is really easy to use and the transition between sync to async seems to be effortless. Spring continue to offers a good abstraction over http frameworks and the REST capacities powered by, among other, the underlying HttpMessageConverters, are also available in the async API, priceless!
But the API is still young, and maybe not as production-ready, as the async-http-client from Ning (https://github.com/AsyncHttpClient/async-http-client), which supports Grizzly, Netty and Apache as underlying providers, and is well documented (AsyncRestTemplate is not yet documented on spring.io:  http://docs.spring.io/spring/docs/4.0.x/spring-framework-reference/html/remoting.html#rest-resttemplate). Moreover, one big missing thing is a provided callback in the methods: all methods return a Future, but if you want to play with callbacks (Runnable or Callable) or ListenableFuture, you have to cheat with Guava, when others frameworks can do it natively:


<dependency>
<groupId>com.ning</groupId>
<artifactId>async-http-client</artifactId>
<version>1.7.20</version>
</dependency>



@Test
public void ning() throws InterruptedException, IOException {

// Arrange
String url = "https://api.github.com/users/treydone";
AsyncHttpClient client = new AsyncHttpClient();

// Actions
client.prepareGet(url).execute(new AsyncCompletionHandlerBase() {

@Override
public Response onCompleted(Response response) throws Exception {
// Asserts
assertTrue(response.getStatusCode() == HttpStatus.OK.value());
assertEquals(
"Treydone",
objectMapper.readValue(response.getResponseBody(),
Map.class).get("login"));
return super.onCompleted(response);
}

});

client.close();
}



Enjoy!

Source:
https://github.com/spring-projects/spring-framework/tree/master/spring-web/src/test/java/org/springframework/http/client
http://sonatype.github.io/async-http-client/
https://jira.springsource.org/browse/SPR-8804

2 comments:

  1. I am wondering how to get it to work with SSL? The synchronous client used PoolingHttpClientConnectionManager which had Registry as constructor argument. It allowed setting up SSL as part of connection manger. PoolingNHttpClientConnectionManager has different constructor. I could set up SSL while building the client (via setSSLSocketFactory), but ConnectionManager solution is much cleaner.

    ReplyDelete
    Replies
    1. Hello Mark,

      Did you ever figure out how to get this to work with SSL?

      Delete