lunes, 13 de abril de 2015

Simplificando REST usando Retrofit

Retrofit

En un post anterior se habló de la librería OkHttp y como esta podía usarse como un potente y eficiente cliente HTTP, el cual inclusive permite realizar llamados asíncronos. Sin embargo, su uso está más enfocado a operaciones bajo nivel. En esta ocasión se hablará de Retrofit, una librería desarrollada por la misma compañía (Square Open Source) la cual facilita bastante el acceso a APIs REST, tanto JSON como XML, mediante el uso de interfaces y anotaciones, evitando la construcción y manipulación manual de peticiones HTTP.

Retrofit no depende de OkHttp, por defecto usará los componentes nativos de Java para los llamados HTTP. Sólo usará OkHttp si encuentra la librería en el classpath de la aplicación. Dado que anteriormente se mencionaron e ilustraron algunas de las ventajas de OkHttp, se incluirá esta librería.

Este cliente REST es completamente configurable, a tal punto que es posible adicionar interceptores para manipular headers en cada petición, realizar autenticación Basic o mediante OAuth, etc. Algunas de estas características se pueden encontrar en la página web de Retrofit: http://square.github.io/retrofit

Por defecto, esta librería realiza la serialización de JSON mediante la librería de Google Gson, sin embargo quienes prefieran usar Jackson, pueden hacerlo simplemente indicando que se usará el conversor que el equipo de Square desarrolló para ello: https://github.com/square/retrofit/tree/master/retrofit-converters

En esta ocasión se trabajarán dos ejemplos muy sencillos, ya que el gran poder que tiene Retrofit (en mi opinión) es la facilidad de su uso, lo cual es lo que se quiere demostrar en este post.

Ejemplo sencillo: consulta del clima

Para este ejemplo se consultará el servicio OpenWeatherMap el cual provee un API REST gratuito para consultar el estado del clima en las ciudades del mundo.

La siguiente consulta retornará el estado del clima en la ciudad de Londres:
http://api.openweathermap.org/data/2.5/weather?id=2643743
{  
  "coord":{  
    "lon":-0.13,
    "lat":51.51
  },
  "sys":{  
    "message":0.0344,
    "country":"GB",
    "sunrise":1428901694,
    "sunset":1428951224
  },
  "weather":[  
    {  
      "id":800,
      "main":"Clear",
      "description":"Sky is Clear",
      "icon":"01n"
    }
  ],
  "base":"stations",
  "main":{  
    "temp":276.272,
    "temp_min":276.272,
    "temp_max":276.272,
    "pressure":1037.14,
    "sea_level":1045.23,
    "grnd_level":1037.14,
    "humidity":87
  },
  "wind":{  
    "speed":1.11,
    "deg":10.0094
  },
  "clouds":{  
    "all":0
  },
  "dt":1428899601,
  "id":2643743,
  "name":"London",
  "cod":200
}

El ejemplo completo se puede encontrar en: https://github.com/guillermo-varela/retrofit-example

Lo primero entonces será crear el archivo de configuración de Gradle con las librerías necesarias.

Nota: quienes aún no conozcan del todo Gradle pueden consultar: http://nombre-temp.blogspot.com/2016/01/tutorial-gradle.html

build.gradle

plugins {
  id 'net.researchgate.release' version '2.0.2'
}

apply plugin: 'java'
apply plugin: 'application'
apply plugin: 'eclipse'

sourceCompatibility = 1.7
targetCompatibility = 1.7

mainClassName = 'com.blogspot.nombre_temp.retrofit.example.Example'

jar {
    manifest {
        attributes 'Implementation-Title': 'OkHttp Example', 'Implementation-Version': version
        attributes 'Main-Class': mainClassName
    }
}

task wrapper(type: Wrapper) {
    gradleVersion = '2.3'
}

repositories {
    mavenCentral()
}

dependencies {
    compile 'com.squareup.retrofit:retrofit:1.9.0'
    compile 'com.squareup.okhttp:okhttp:2.3.0'
    compile 'com.squareup.okhttp:okhttp-urlconnection:2.3.0'
    compile 'org.apache.commons:commons-lang3:3.4'
}

Como puede verse, además de las dependencias de Retrofit y OkHttp se agregaron también:

  • okhttp-urlconnection: permite la integración entre OkHttp y Retrofit
  • Apache Commons Lang3: Simplemente se agregó para facilitar el desarrollo de métodos "toString" que se usarán en este ejemplo. No se requiere para el funcionamiento de Retrofit.
Teniendo en cuenta la estructura de la respuesta JSON que retorna el servicio del clima, se procederá a desarrollar una clase con algunos de los atributos necesarios para almacenar esta información. Retrofit usará Gson para convertir el JSON que retorna el API en una instancia de esta clase. Es de notar que la clase no tiene todos los atributos que vienen en la respuesta JSON, esto se debe a que no es necesario tener todos los datos y se dejan solamente algunos para efectos del ejemplo:
package com.blogspot.nombre_temp.retrofit.example.model;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

import com.google.gson.annotations.SerializedName;

public class WeatherData {

  private Main main;
  private Wind wind;

  public Main getMain() {
    return main;
  }
  public void setMain(Main main) {
    this.main = main;
  }
  public Wind getWind() {
    return wind;
  }
  public void setWind(Wind wind) {
    this.wind = wind;
  }

  @Override
  public String toString() {
    return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
  }

  public static class Main {
    private float temp;
    @SerializedName("temp_min")
    private float tempMin;
    @SerializedName("temp_max")
    private float tempMax;
    private float pressure;
    private float humidity;

    public float getTemp() {
      return temp;
    }
    public void setTemp(float temp) {
      this.temp = temp;
    }
    public float getTempMin() {
      return tempMin;
    }
    public void setTempMin(float tempMin) {
      this.tempMin = tempMin;
    }
    public float getTempMax() {
      return tempMax;
    }
    public void setTempMax(float tempMax) {
      this.tempMax = tempMax;
    }
    public float getPressure() {
      return pressure;
    }
    public void setPressure(float pressure) {
      this.pressure = pressure;
    }
    public float getHumidity() {
      return humidity;
    }
    public void setHumidity(float humidity) {
      this.humidity = humidity;
    }
    @Override
    public String toString() {
      return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }
  }

  public static class Wind {
    private float speed;
    private float deg;

    public float getSpeed() {
      return speed;
    }
    public void setSpeed(float speed) {
      this.speed = speed;
    }
    public float getDeg() {
      return deg;
    }
    public void setDeg(float deg) {
      this.deg = deg;
    }
    @Override
    public String toString() {
      return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }
  }
}

La anotación "@SerializedName" se usa para indicar a Gson el nombre del dato que encontrará dentro del JSON.

A continuación se desarollará la interfaz Java con las operaciones del API REST que se usarán:
package com.blogspot.nombre_temp.retrofit.example.api;

import retrofit.Callback;
import retrofit.http.GET;
import retrofit.http.Query;

import com.blogspot.nombre_temp.retrofit.example.model.WeatherData;

public interface WeatherApi {

  @GET("/weather")
  WeatherData getWeather(@Query("id") long cityId);

  @GET("/weather")
  void getWeatherAsync(@Query("id") long cityId, Callback<Weatherdata> callback);
}

  • Método getWeather: Se indica mediante la anotación "@GET" que al llamar este método se realizará una petición GET a la operación "/weather" del API REST. Con la anotación "@Query" se está indicando que en la URL se debe agregar el parámetro "id" con el valor indicado en el parámetro "cityId", es decir si este método se llama con el parámetro "123" el llamado al API del clima será: http://api.openweathermap.org/data/2.5/weather?id=123. El JSON que retorne dicho llamado será convertido a una instancia de la clase "WeatherData".
  • Método getWeatherAsync: Es muy similar al anterior, con la diferencia de que este no retorna un resultado, ya que se trata de un llamado asíncrono. El procesamiento de la respuesta se dará en la instancia del objeto "Callback" que se indica en el segundo parámetro. Es de aclarar que no es la misma interfaz "com.squareup.okhttp.Callback" que se usó en el ejemplo de OkHttp, pero es el mismo principio.
Finalmente se tiene la clase que hace los llamados al API usando Retrofit:
package com.blogspot.nombre_temp.retrofit.example;

import java.util.Date;

import org.apache.commons.lang3.time.DateFormatUtils;

import retrofit.Callback;
import retrofit.RestAdapter;
import retrofit.RestAdapter.LogLevel;
import retrofit.RetrofitError;
import retrofit.client.Response;

import com.blogspot.nombre_temp.retrofit.example.api.WeatherApi;
import com.blogspot.nombre_temp.retrofit.example.model.WeatherData;

public class Example {

  private static final RestAdapter restAdapter = new RestAdapter.Builder()
    .setEndpoint("http://api.openweathermap.org/data/2.5")
    .setLogLevel(LogLevel.FULL)
    .build();

  private static final WeatherApi weatherService = restAdapter.create(WeatherApi.class);

  private static void print(String message) {
    System.out.println(message + ": " + DateFormatUtils.format(new Date(), "yyyy-MM-dd'T'HH:mm:ss.sss"));
  }

  public static void main(String[] args) {
    print("Starting main method");
    long londonId = 2643743L;

    WeatherData weatherData = weatherService.getWeather(londonId);
    print(String.format("Sync response [%s]", weatherData));

    weatherService.getWeatherAsync(londonId, new Callback<Weatherdata>() {
      @Override
      public void success(WeatherData weatherData, Response response) {
        print(String.format("Async response [%s]", weatherData));
      }
      @Override
      public void failure(RetrofitError error) {
        error.printStackTrace();
      }
    });

    print("Ending main method");
  }
}
  • Líneas 18-21: Se crea un "RestAdapter", el cual contiene las propiedades que tendrá el cliente REST a usar. En este caso se está indicando que use "LogLevel.FULL" sólo para propósitos de la demostración.
  • Línea 23: Se crea una instancia de la interfaz que se creó previamente con la definición de las operaciones del API. Es importante notar que en el código desarrollado no se hizo una clase que implementara dicha interfaz y definiera el cuerpo de sus métodos, esta es una tarea que Retrofit se encarga de hacer por nosotros.
  • Líneas 25-27: Definición de un método para que al imprimir un texto por consola se le adicione la fecha y hora actual. Esto con el fin de comprobar el comportamiento de las peticiones HTTP.
  • Líneas 29-48: Es el método main de la aplicación y allí se realiza:
    • Líneas 33-34: Llamado síncrono al API del clima y se imprime el resultado.
    • Líneas 36-45: Llamado asíncrono al API del clima. Se define también la clase anónima del tipo "Callback" en la cual se define cómo procesar la respuesta obtenida (método success) y cómo procesar un posible error en el llamado (método failure). Al igual que sucede con el ejemplo del post anterior sobre OkHttp, tanto el llamado HTTP al API REST, como el código dentro de la instancia de "Callback" se realizan en un hilo separado, es decir no se bloquea la ejecución del método main.
Al ejecutar esta clase se obtiene un resultado como el siguiente:
Starting main method: 2015-04-13T01:13:45.045
---> HTTP GET http://api.openweathermap.org/data/2.5/weather?id=2643743
---> END HTTP (no body)
<--- HTTP 200 http://api.openweathermap.org/data/2.5/weather?id=2643743 (735ms)
Server: nginx
Date: Mon, 13 Apr 2015 06:13:43 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Source: redis
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST
OkHttp-Selected-Protocol: http/1.1
OkHttp-Sent-Millis: 1428905625799
OkHttp-Received-Millis: 1428905625933

{"coord":{"lon":-0.13,"lat":51.51},"sys":{"message":0.0169,"country":"GB","sunrise":1428901694,"sunset":1428951224},"weather":[{"id":800,"main":"Clear","description":"Sky is Clear","icon":"01d"}],"base":"stations","main":{"temp":276.272,"temp_min":276.272,"temp_max":276.272,"pressure":1037.14,"sea_level":1045.23,"grnd_level":1037.14,"humidity":87},"wind":{"speed":1.11,"deg":10.0094},"clouds":{"all":0},"dt":1428904512,"id":2643743,"name":"London","cod":200}

<--- END HTTP (461-byte body)
Sync response [WeatherData[main=WeatherData.Main[temp=276.272,tempMin=276.272,tempMax=276.272,pressure=1037.14,humidity=87.0],wind=WeatherData.Wind[speed=1.11,deg=10.0094]]]: 2015-04-13T01:13:45.045
Ending main method: 2015-04-13T01:13:46.046
---> HTTP GET http://api.openweathermap.org/data/2.5/weather?id=2643743
---> END HTTP (no body)
<--- HTTP 200 http://api.openweathermap.org/data/2.5/weather?id=2643743 (122ms)
Server: nginx
Date: Mon, 13 Apr 2015 06:13:43 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Source: redis
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST
OkHttp-Selected-Protocol: http/1.1
OkHttp-Sent-Millis: 1428905626004
OkHttp-Received-Millis: 1428905626126

{"coord":{"lon":-0.13,"lat":51.51},"sys":{"message":0.0169,"country":"GB","sunrise":1428901694,"sunset":1428951224},"weather":[{"id":800,"main":"Clear","description":"Sky is Clear","icon":"01d"}],"base":"stations","main":{"temp":276.272,"temp_min":276.272,"temp_max":276.272,"pressure":1037.14,"sea_level":1045.23,"grnd_level":1037.14,"humidity":87},"wind":{"speed":1.11,"deg":10.0094},"clouds":{"all":0},"dt":1428904512,"id":2643743,"name":"London","cod":200}

<--- END HTTP (461-byte body)
Async response [WeatherData[main=WeatherData.Main[temp=276.272,tempMin=276.272,tempMax=276.272,pressure=1037.14,humidity=87.0],wind=WeatherData.Wind[speed=1.11,deg=10.0094]]]: 2015-04-13T01:13:46.046

Inicialmente se ve que se imprime la petición y la respuesta del llamado síncrono (letras verdes) y es sólo cuando termina dicho llamado que termina de ejecutarse el método main (letras azules). Ya después de terminado el método main es que aparece la petición y la respuesta del llamado asíncrono, con lo cual comprobamos que dicho llamado no ha bloqueado la ejecución de la aplicación, sino que se ha realizado en un hilo aparte.

Puede comprobarse también al comparar las respuestas en JSON y las representaciones String de los objetos "WeatherData" (resaltado en amarillo) que la conversión se ha realizado correctamente, sin que tampoco se haya tenido que procesar manualmente el JSON de la respuesta de API.

Nota: la aplicación termina de ejecutarse aproximadamente un minuto después de procesar la respuesta del llamado asíncrono, por la espera de 60 segundos para descartar los hilos del pool que se explicaba al final del post anterior sobre OkHttp.

Conclusión

Como puede verse en el resultado de la ejecución usando Retrofit (y dejando que el nivel de log sea FULL) ha sido la propia librería la encargada de generar todo el procesamiento JSON y HTTP requerido para consumir el API REST de una manera muy sencilla, simplemente definiendo una interfaz Java con anotaciones, inclusive de manera asíncrona sin tener que manipular hilos tampoco.

Referencias

http://square.github.io/retrofit
https://github.com/square/retrofit
http://openweathermap.org/api

1 comentario: