Ejemplo de servicios REST con JSON y contenedor JEE

De ChuWiki
Saltar a: navegación, buscar

Veamos un ejemplo de cómo publicar un servicio web usando REST y JSON. El ejemplo se ha desplegado en un contenedor de aplicaciones Wildfly 10.0, que ya viene con todas las librerías necesarias. Puedes encontrar el proyecto completo en Github

Clase de Datos

Nuestro servicio web va a ser muy sencillo, simplemente manejará un tipo de dato cuya clase mostramos a continuación

public class Data {
   private int value;
   private String string;
   ...
   // Constructores, getters y setters
}

Sólo un par de atributos de ejmplo. No mostramos todos los métodos habituales (setters, getters, constructores, toString(), etc) porque no aportan nada a la explicación y así no liamos el ejemplo.

La clase con el servicio web

La clase que va a ser nuestro servicio Web no es más que una clase normal de java, con métodos. Únicamente hay que poner las anotaciones específicas de JAX-RS para que el contenedor de aplicaciones lo convierta en servicio web cuando arranque. Las anotaciones son las siguientes:

  • @Path para indicar el path en la url donde estará nuestro servicio web.
  • @Produces para indicar el formato en el que el servicio web nos devuelve el resultado. Son habituales "application/xml" y "application/json".
  • @Consumes para indicar el formato en el que nuestro servicio web admite los datos. Igual que @Produces, son habituales "application/xml" y "application/json".
  • El protocolo http define varios métodos para las peticiones http. En los web services son habituales las cuatro siguientes:
    • @GET Cuando queremos pedir datos al servicio web. Por ejemplo, una petición @GET a http://servidor/webservice/empleados podría devolver la lista de empleados y una llamada a http://servidor/webservice/empleados/23 podría devolver los datos del empleado cuyo identificador es 23.
    • @POST Cuando enviamos datos al servicio web para que el servicio web haga con ellos lo que considere oportuno. Por ejemplo, una llamada @POST a http://servidor/webservice/crea_empleado pasando los datos de un empleado, puede crear el empleado en base de datos.
    • @PUT Cuando queremos guardar datos en una url específica. Si esos datos no existen en esa URL, se crean. Si ya existen, deben modificarse para que sean lo que nosotros hemos pasado. Por ejemplo, una llamada @PUT a http://servidor/webservice/empleados/23 pasándole datos de un empleado, modificaría los datos del empleado cuyo identificador es el 23 para que sean los que nosotros pasamos. Si no hay ningún empleado con identificador 23, debería crearse un nuevo empleado y asignarle ese identificador concreto.
    • @DELETE Para borrar los datos de una url específica. Por ejemplo, una llamada @DELETE a http://servidor/webservice/empleados/23 debería eliminar el empleado de identificador 23.

Lectura @GET

El código es el siguiente

package com.chuidiang.examples.restful;

import java.util.LinkedList;
import java.util.List;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;

@Path("/service/")
public class RestfulService {
   
   @GET
   @Produces({MediaType.APPLICATION_JSON})
   public List<Data> getData(){
      List<Data> result = new LinkedList<>();
      result.add(new Data(1,"one"));
      result.add(new Data(2,"two"));
      return result;
   }
   
   @GET
   @Produces({MediaType.APPLICATION_JSON})
   @Path("{id}")
   public Data getData(@PathParam("id") String id){
      if ("1".equals(id)){
         return new Data(1,"one");
      }
      if ("2".equals(id)){
         return new Data(2,"two");
      }
      throw new WebApplicationException(404);
   }

}

No hemos querido complicar el ejemplo haciendo consultas a base de datos ni otra florituras similares. El método getData() simplmente crea una List a piñón fijo y la devuelve. El método getData(int) mira el id que se le pasa y si es 1 ó 2, devuelve un Data cuyo id es 1 ó 2. Da un error 404 (no encontrado) si el id no es ni 1 ni 2.

La clase lleva una anotación @Path("/service/") para indicar el path en la URL del navegador donde encontraremos este servicio web. La URL será en este caso http://<host>:<puerto>/<nombre aplicación>/resources/service/. Lo de resources lo contamos más adelante.

Según el protocolo REST, los métodos de consulta deben ser de tipo GET, por eso anotamos ambos métodos, que son de consulta, con la anotación @GET.

Como ambos métodos queremos que produzcan el resultado como texto JSON, anotammos ambos métodos con @Produces({"application/json"}). Si quisiéramos XML, debemos anotarlos con @Produces({"application/xml"}). De todas formas, no intentes hacerlo porque además de esto, habría que tocar más cosas en más sitios para que funciones como XML.

Pues que queremos que la llamada http://<host>:<puerto>/<nombre aplicación>/resources/service/ nos devuelva la lista de Data, al primer método no le añadimos ningún Path adicional.

Sin embargo, para el segundo, queremos que el path sea http://<host>:<puerto>/<nombre aplicación>/resources/service/<id>, siendo el ''<id>'' del final el id del Data que solicitamos. Como hay un trozo más de path en la URL, lo añadimos con la anotación @Path("{id}") en el segundo método. El {id} entre llaves representa una variable y cualquier cosa que pongamos en la URL detrás de /service/, se nos pasará en esa variable id.

¿Cómo recogemos el valor de esa variable?. Añadiendo al método un parámetro id correspondientemente anotado

public Data getData(@PathParam("id") String id){
...
}

Hemos añadido el parámetro String id, y por medio de la anotación @PathParam("id"), indicamos que nos deben pasar lo que han escrito detrás de /service/ y que hemos llamado {id}.

¿Y qué hacemos si no hay ningún elemento con ese id?. Hay varias opciones, una sería devolver null, otra es hacer saltar una excepción con el error 404 (en el protocolo http, un error 404 indica que no se ha encontrado la URL). Si devolvemos null, el cliente verá que la petición es correcta, pero no recibirá ningún dato. Esta opción no es estrictamente correcta, porque no devolver datos está reservado para operaciones en las que no se esperan resultados, es decir, el ciente llama para que se haga una operación, pero ni siquiera lee el resultado porque no espera que lo haya. En este caso, sí se espera un resultado, el cliente hace la petición y espera recibir un resultado, que no hay. Lo estrictamente correcto es enviarle el error 404, indicando que la URL ..../service/33 no existe. Así que si el id no es ninguno de los esperados, lanzamos la excepción throw new WebApplicationException(404);

Listo, con estas anotaciones sólo nos queda rellenar con código nuestros métodos y devolver el resultado. En el código de ejemplo hemos hecho una cosa trivial, para no complicarlo. En realidad deberían, por ejemplo, hacerse consultas a base de datos para obtener esos datos.

Creación $POST

Como hemos indicado antes, podríamos crear también con @PUT, pero eso implica que el identificador del dato debe decidirlo el cliente, cosa que no es lo habitual. Lo habitual es que el identificador sea un identificador en base de datos que suele generar la misma base de datos o el servidor, por lo que el cliente no lo sabe por adelantado. Por ello, para creación suele ser habitual usar @POST mejor que @PUT. El código para la creación puede ser así

   @POST
   @Path("/create")
   @Consumes(MediaType.APPLICATION_JSON)
   public void save(Data data){
      System.out.println(data.toString()+ "created");
   }

El @Path que hemos puesto se añade al @Path de la clase, por lo que para llamar a este método será http://<host>:<port>/<nombre aplicación>/service/create.

La llamada es de tipo @POST. El navegador no puede generar una llamada de tipo POST ni adjuntar datos de tipo POST fácilmente, así que un plugin de Chrome como Postman puede ayudarnos a probar este método del web service.

Indicamos con la anotación @Consumes(MediaType.APPLICATION_JSON) que esperamos los datos en formato JSONS. Si te decides a intentarlo con Postman (u otra equivalente), en importante en el Header de la petición, poner "Content-Type:application/json", para que el servidor sepa que los datos le llegan en formato JSON, y en el cuerpo de la petición poner los datos en formato JSON, algo como esto

{
	"value": -1,
	"string": "dato para crear"
}

En este caso devolvemos void, y no hemos puesto @Produces, por lo que no vamos a devolver nada al navegador. Por supuesto, podríamos devolver lo que quisieramos, como por ejemplo, el dato con el identificador que haya decidido el servidor.

Creación/Modificación @PUT

Con este método debemos crear o modificar lo que exista en la url concreta. El código java puede ser este

   @PUT
   @Path("/{id}")
   @Consumes(MediaType.APPLICATION_JSON)
   @Produces(MediaType.APPLICATION_JSON)
   public Data update(@PathParam("id") int id, Data data){
      data.setValue(id);
      if (id==1 || id==2){
        LOG.info(data.toString() + " updated"); 
      } else {
         LOG.info(data.toString() + " created");
      }
      return data;
   }

El mecanismo es muy similar al de @GET con identificador, pero la anotación es @PUT y el código a realizar dentro del método es otro. En los parámetros del método, además de leer el identificador que se pasa en la url con @PathParam("id") int id, también recibimos los datos a insertar, que se esperarán en formato JSON, como indica la anotación @Consumes(MediaType.APPLICATION_JSON).

En esta ocasión y a diferencia de lo que hicimos en @POST, hemos decidido devolver el dato creado/modificado, por lo que se añade la anotación @Produces(MediaType.APPLICATION_JSON) para indicar al navegador que le devolvemos el resultado en JSON y hacemos un return data con los datos ya modificados.

Si decides probar esto con Postman, ten en cuenta las mismas indicaciones que dimos en @POST.

Borrado @DELETE

Para borrado, lo mismo que con @GET e identificador. El código puede ser este

   @DELETE
   @Path("{id}")
   public void remove(@PathParam("id") int id){
      if (id==1 || id==2){
         LOG.info("data removed");
      } else {
         throw new WebApplicationException(404);
      }
   }

La anotación es @DELETE. No esperamos datos, salvo el identificador que viene en la url, por lo que no necesitamos @Consume. Tampoco vamos a devolver nada, así que el método devuelve void y no ponemos anotación @Produces. Al igual que hicimos en @GET, recogemos el identificador, y a diferencia de lo que hicimos en @GET, en vez de devolver el dato, lo borramos.

Si el dato no existe y queremos informar al navegador, lanzamos una excepción WebApplicationException(404). Al navegador le llegará un error 404 (recurso no disponible).

La clase Application

Una vez que tenemos nuestras clases de servicio web adecuadamente anotadas, debemos darlas de alta de alguna forma para que el contenedor web las localice y las publique como servicios web. Hay varias opciones, como indicarlas en el fichero web.xml, pero aquí vamos a optar por otra opción. Basta crear una clase que herede de Application y sobreescribir el método getClasses(). El siguiente código puede ser un ejemplo

package com.chuidiang.examples.restful;

import java.util.HashSet;
import java.util.Set;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("resources")
public class MyApplication extends Application{
   
   @Override
   public Set<Class<?>> getClasses() {
      Set<Class<?>> classes = new HashSet<>();
      classes.add(RestfulService.class);
      return classes;
   }
}

La clase en sí no tiene nada especial. Hereda de Application y sobreescribe el método getClasses(), devolviendo en él un conjunto de las clases que son servicios web. Sólo hay una anotación imporante: @ApplicationPath("resources"). Esta anotación indica un path por debajo del cual irán todos los serivcios web, en este ejemplo, todos irán por debajo de http://<host>:<puerto>/<nombre aplicacion>/resources/. El path concreto por debajo lo decide cada clase concreta con su anotación @Path, que en nuestro ejemplo era @Path("/service/"), de ahí que la URL para nuestro servicio sea http://<host>:<puerto>/<nombre aplicacion>/resources/service/

Algunos detalles del proyecto

El tipo de proyecto debe ser un proyecto web java, es decir, de los que se empaquetan en un fichero war. El siguiente fichero pom.xml de maven puede ser un ejemplo

<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/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.chuidiang.examples</groupId>
	<artifactId>EJB_War_Project</artifactId>
	<packaging>war</packaging>
	<version>0.0.1-SNAPSHOT</version>
	<name>EjbWebApplicacion Maven Webapp</name>
	<url>http://chuwiki.chuidiang.org</url>
	<dependencies>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>3.8.1</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.jboss.spec</groupId>
			<artifactId>jboss-javaee-7.0</artifactId>
			<version>1.0.3.Final</version>
			<type>pom</type>
			<scope>provided</scope>
		</dependency>
	</dependencies>
	<build>
		<finalName>EjbWebApplicacion</finalName>
	</build>
	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<maven.compiler.target>1.7</maven.compiler.target>
		<maven.compiler.source>1.7</maven.compiler.source>
	</properties>

</project>

Algunos detalles a tener en cuenta en este fichero.

Puesto que el contenedor donde hemos desplegado el ejemplo es un contenedor Wildfly 10.0 de JBoss, hemos puesto como dependencia la siguiente

		<dependency>
			<groupId>org.jboss.spec</groupId>
			<artifactId>jboss-javaee-7.0</artifactId>
			<version>1.0.3.Final</version>
			<type>pom</type>
			<scope>provided</scope>
		</dependency>

Al ser de tipo pom, en realidad es un conjunto de jar. El scope es provided, puesto que Wildfly ya los tiene y no es necesario empaquetarlos en nuestro war.

Otro detalle a tener en cuenta es que la versión de java necesita ser la 7 o superior. Eso en el proyecto maven se consigue fijando las propiedades maven.compiler.target y maven.compiler.source, como se puede ver al final del fichero pom.xml

Otro detalle más para los que usen Eclipse, el fichero web.xml del proyecto debe tener esta cabecera

<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
                        http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
	version="3.0" id="WebApp_ID">
...
</web-app>

es decir, debemos fijarnos en la versión (la 3.0). Si usamos una versión muy antigua, eclipse nos dará errores en el proyecto diciendo que no puede poner la faceta de aplicación web de la versión que solicitemos.