miércoles, 10 de noviembre de 2010

Servicios REST con Spring - Usando RestTemplate

Después de haber visto unos demos bien interesante del soporte que le da Spring al tema de servicios web en SpringOne 2GX 2010,  decidí hacer una prueba para ver si de verdad es tan sencillo. La primera prueba es tratar de consumir algún servicio REST usando RestTemplate. Como el nombre lo indica, esta clase es una implementación del patrón template orientada a procesar servicios REST, ya Spring nos tiene acostumbrados con este mecanismo con clases como JDBTemplate o JMSTemplate.
Para poder arrancar el demo lo primero es conseguir o implementar un servicio REST de tal forma de poder consumirlo. En mi caso voy a aprovechar que tengo instalado Sonatype Nexus el cual se encarga de exponer algunos servicios. La documentación de los servicios de nexus es casi inexistente al momento pero buscando por internet encontre algunos uri. Para las pruebas voy a usar el que retorna el estado de la instancia.

http://your.company.com/nexus/service/local/status

El cual devuelve un xml con la siguiente estructura (el xml es mas extenso, solo deje la parte que me interesa)

<status>
  <data>
 <appName>Sonatype Nexus Maven Repository Manager</appName>
 <formattedAppName>Sonatype Nexus&trade; Open Source Edition, Version: 1.8.0</formattedAppName>
 <version>1.8.0</version>
 <apiVersion>1.8.0</apiVersion>
 <editionLong>Open Source</editionLong>
 <editionShort>OSS</editionShort>
 <state>STARTED</state>
 <operationMode>STANDALONE</operationMode>
 <initializedAt>2010-10-26 12:28:06.732 EDT</initializedAt>
 <startedAt>2010-10-26 12:28:17.334 EDT</startedAt>
 <lastConfigChange>2010-10-26 12:28:17.334 EDT</lastConfigChange>
 <firstStart>false</firstStart>
 <instanceUpgraded>false</instanceUpgraded>
 <configurationUpgraded>false</configurationUpgraded>
  </data>
</status>

Antes de escribir cualquier clase creo el pom.xml de maven con las dependencias respectivas

<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>neptuno.demo.spring</groupId>
 <artifactId>demo-spring-rest</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <packaging>jar</packaging>
 <name>demo-spring-rest</name>
 <url>http://maven.apache.org</url>
 <build>
  <plugins>
   <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>2.3.2</version>
    <configuration>
     <source>1.5</source>
     <target>1.5</target>
    </configuration>
   </plugin>
  </plugins>
 </build>
 <properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <org.springframework.version>3.0.5.RELEASE</org.springframework.version>
  <org.logback.version>0.9.26</org.logback.version>
 </properties>
 <dependencies>
  <!-- Dependencias para Rest Template -->
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-web</artifactId>
   <version>${org.springframework.version}</version>
  </dependency>
  <dependency>
   <groupId>org.springframework.ws</groupId>
   <artifactId>spring-xml</artifactId>
   <version>1.5.9</version>
  </dependency>
  <!-- Dependencias para pruebas unitarias -->
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-test</artifactId>
   <version>${org.springframework.version}</version>
   <scope>test</scope>
  </dependency>
  <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>4.8.2</version>
   <scope>test</scope>
  </dependency>
  <!-- Dependencias para logging -->
  <dependency>
   <groupId>org.slf4j</groupId>
   <artifactId>slf4j-api</artifactId>
   <version>1.6.1</version>
  </dependency>
  <dependency>
   <groupId>ch.qos.logback</groupId>
   <artifactId>logback-core</artifactId>
   <version>${org.logback.version}</version>
  </dependency>
  <dependency>
   <groupId>ch.qos.logback</groupId>
   <artifactId>logback-classic</artifactId>
   <version>${org.logback.version}</version>
  </dependency>
 </dependencies>
</project>

El siguiente paso es crear un bean con la representación del xml

public class NexusStatus {

 private String appName;
 private String formattedAppName;
 private String version;
 private String apiVersion;
 private String editionLong;
 private String editionShort;
 private String state;
 private String operationMode;
 private String initializedAt;
 private String startedAt;
 private String lastConfigChange;

  // Generar todos los getters y setter

 @Override
 public String toString() {
  StringBuilder builder = new StringBuilder();
  builder.append("NexusStatus [appName=");
  builder.append(appName);
  builder.append(", formattedAppName=");
  builder.append(formattedAppName);
  builder.append(", version=");
  builder.append(version);
  builder.append(", apiVersion=");
  builder.append(apiVersion);
  builder.append(", editionLong=");
  builder.append(editionLong);
  builder.append(", editionShort=");
  builder.append(editionShort);
  builder.append(", state=");
  builder.append(state);
  builder.append(", operationMode=");
  builder.append(operationMode);
  builder.append(", initializedAt=");
  builder.append(initializedAt);
  builder.append(", startedAt=");
  builder.append(startedAt);
  builder.append(", lastConfigChange=");
  builder.append(lastConfigChange);
  builder.append("]");
  return builder.toString();
 }
}

Como en todas las aplicaciones de Spring, el archivo de configuración es el que se encarga de hacer la mayoría de la magia. Para este ejemplo el archivo de spring lo denomine nexus-spring.xml y esta ubicado en la carpeta resources

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
 xmlns:util="http://www.springframework.org/schema/util"
 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
  http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
  http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd">

 <context:component-scan base-package="neptuno.demo.spring.rest" />

 <bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
  <property name="messageConverters">
   <list>
    <bean
     class="org.springframework.http.converter.xml.SourceHttpMessageConverter" />
   </list>
  </property>
 </bean>

 <bean id="xpathTemplate" class="org.springframework.xml.xpath.Jaxp13XPathTemplate" />

 <bean id="nexusStatusNodeMapper" class="neptuno.demo.spring.rest.NexusStatusNodeMapper" />

</beans>

En este archivo se definen 3 bean y se agrega una instruccion especial para Spring

  • La entrada component-scan apunta al paquete que debe ser evaluado por Spring para buscar clases con anotaciones. 
  • restTemplate es una instancia de Spring RestTemplate indicando que el convertidor del cuerpo del mensaje es la clase SourceHttpMessageConverter.  Esta clase convierte entre request/response http y javax.xml.transform.Source, de esta forma se puede recibir la respuesta de Nexus y procesarla como xml.
  • xpathTemplate es usado para convertir entre Source y el bean NexusStatus, en este caso usando JAXP 1.3
  • nexusStatusNodeMapper implementa org.springframework.xml.xpath.NodeMapper. Es similar al row mapper de JDBCTemplate y se usa para convertir un nodo del xml al objeto NexusStatus
La clase que se encarga de utilizar toda la definición anterior es la siguiente

/**
 * 
 */
package neptuno.demo.spring.rest;

import javax.xml.transform.Source;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.xml.xpath.Jaxp13XPathTemplate;
import org.springframework.xml.xpath.NodeMapper;

@Component("nexusRestAccess")
public class NexusRestAccess {

 private static Logger logger = LoggerFactory
   .getLogger(NexusRestAccess.class);

 @Autowired
 private RestTemplate restTemplate;

 @Autowired
 private Jaxp13XPathTemplate xpathTemplate;

 @Autowired
 private NodeMapper nexusStatusNodeMapper;

 /**
  * Obtiene el estado de la instancia de Nexus
  * 
  * @return
  */
 public NexusStatus getNexusStatus() {
  Source source = restTemplate.getForObject(
    "http://my.company.com/nexus/service/local/status",
    Source.class);
  return (NexusStatus) xpathTemplate.evaluateAsObject("//data", source,
    nexusStatusNodeMapper);
 }

 public static void main(String[] args) {
  ApplicationContext applicationContext = new ClassPathXmlApplicationContext(
    "/nexus-spring.xml");
  NexusRestAccess demo = applicationContext.getBean("nexusRestAccess",
    NexusRestAccess.class);
  NexusStatus status = demo.getNexusStatus();
  logger.info(status.toString());
 }
}


Como ven la clase usa anotaciones para indicarle a Spring que debe inyectar. La linea

Source source = restTemplate.getForObject(
   "http://my.company.com/nexus/service/local/status",
   Source.class);

invoca el servicio remoto y devuelve el resultado en un objeto tipo Source. Después se utiliza xpathTemplate para convertirlo a NexusSource. Uno de los parametros de xpathTemplate es el mapper definido en el archivo de Spring. El código de la clase es el siguiente


/**
 * 
 */
package neptuno.demo.spring.rest;

import org.springframework.xml.xpath.NodeMapper;
import org.w3c.dom.DOMException;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class NexusStatusNodeMapper implements NodeMapper {

 /**
  * (non-JSDoc)
  * 
  * @see org.springframework.xml.xpath.NodeMapper#mapNode(org.w3c.dom.Node,
  *      int)
  */
 @Override
 public Object mapNode(Node node, int i) throws DOMException {
  NexusStatus newStatus = new NexusStatus();
  NodeList children = node.getChildNodes();
  for (int j = 0; j < children.getLength(); j++) {
   Node n = children.item(j);
   if ("appName".equals(n.getNodeName())) {
    newStatus.setAppName(n.getTextContent());
   } else if ("version".equals(n.getNodeName())) {
    newStatus.setVersion(n.getTextContent());
   } else if ("formattedAppName".equals(n.getNodeName())) {
    newStatus.setFormattedAppName(n.getTextContent());
   } else if ("apiVersion".equals(n.getNodeName())) {
    newStatus.setApiVersion(n.getTextContent());
   } else if ("state".equals(n.getNodeName())) {
    newStatus.setState(n.getTextContent());
   } else if ("editionLong".equals(n.getNodeName())) {
    newStatus.setEditionLong(n.getTextContent());
   } else if ("editionShort".equals(n.getNodeName())) {
    newStatus.setEditionShort(n.getTextContent());
   } else if ("operationMode".equals(n.getNodeName())) {
    newStatus.setOperationMode(n.getTextContent());
   } else if ("initializedAt".equals(n.getNodeName())) {
    newStatus.setInitializedAt(n.getTextContent());
   } else if ("startedAt".equals(n.getNodeName())) {
    newStatus.setStartedAt(n.getTextContent());
   } else if ("lastConfigChange".equals(n.getNodeName())) {
    newStatus.setLastConfigChange(n.getTextContent());
   }
  }
  return newStatus;
 }

}


Por ultimo ejecutamos el metodo main() de la clase NexusRestAccess la cual invoca el servicio y muestra por consola el objeto NexusStatus


Nov 10, 2010 3:56:13 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@1add2dd: startup date [Wed Nov 10 15:56:13 EST 2010]; root of context hierarchy
Nov 10, 2010 3:56:13 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [nexus-spring.xml]
Nov 10, 2010 3:56:16 PM org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@1bc82e7: defining beans [nexusRestAccess,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,restTemplate,xpathTemplate,nexusStatusNodeMapper]; root of factory hierarchy

15:56:19.798 [main] INFO  n.demo.spring.rest.NexusRestAccess - NexusStatus [appName=Sonatype Nexus Maven Repository Manager, formattedAppName=Sonatype Nexus&trade; Open Source Edition, Version: 1.8.0, version=1.8.0, apiVersion=1.8.0, editionLong=Open Source, editionShort=OSS, state=STARTED, operationMode=STANDALONE, initializedAt=2010-10-26 12:28:06.732 EDT, startedAt=2010-10-26 12:28:17.334 EDT, lastConfigChange=2010-10-26 12:28:17.334 EDT]


También podemos agregar una pequeña prueba unitaria para probar el código
package neptuno.demo.spring.rest;

import junit.framework.Assert;

import org.junit.Before;
import org.junit.Test;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;

@ContextConfiguration(locations = { "/nexus-spring.xml" })
public class NexusRestAccessTest extends AbstractJUnit4SpringContextTests {

 private NexusRestAccess demo;

 @Before
 public void setup() {
  demo = applicationContext.getBean("nexusRestAccess",
    NexusRestAccess.class);
 }

 @Test
 public void testNexusStatus() {
  NexusStatus status = demo.getNexusStatus();
  Assert.assertNotNull(status);
  Assert.assertTrue(status.getAppName().indexOf("Sonatype") >= 0);
 }

}

Como ven es bien sencillo consumir servicios REST usando Spring RestTemplate. Esta clase no solo soporta consultas (GET) sino también los otros métodos (PUT, DELETE, POST, etc). Para mas informaición revisen la documentación.
Publicar un comentario