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:html

  <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>

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);
  }
 }
}

測試java

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

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);
 }
}

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:git

<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();
 }
相關文章
相關標籤/搜索