Introducción a REST
REST (Representational State Transfer) es un tipo de arquitectura software en el que las interfaces entre sistemas usan directamente HTTP para obtener datos o indicar la ejecución de operaciones sobre los datos, en cualquier formato (XML, JSON, etc) sin las abstracciones adicionales de los protocolos basados en patrones de intercambio de mensajes, como por ejemplo SOAP.Los sistemas que siguen los principios REST se llaman con frecuencia RESTful.
Para poder crear/consumir servicios web REST, tenemos el API JAX-RS (Java API for RESTful Web Services) en el JRE/JDK de java
Algunos conceptor básicos
1) Programar un servicio web RESTfull debemos cambiar el enfoque, los conceptos básicos de los servicios web tradicionales (RPC) basados en la invocación de métodos cambian completamente.Ya no invocamos métodos del tipo addEmpleado(emp), removeEmpleado(emp), updateEmpleado(emp), deleteEmpleado(emp), ahora el 'recurso' empleado se expone en una URL (http://localhost:8080/rest-ws/rest/empleado) sobre la quepodemos realizar operaciones básicas de tipo create, read, update y delete invocando la URL mediante los métodos:POST, GET, PUT, DELETE de HTTP.Algunos ejemplo:
- Para dar de alta un empleado llamaríamos la siguiente URL por POST:
- Para dar de baja un empleado llamaríamos la siguiente URL por DELETE:
- Para recuperar un empleado llamaríamos la siguiente URL por GET:
- Para actualizar un empleado llamaríamos la siguiente URL por PUT:
- Para obtener la lista de empleados llamaríamos la siguiente URL por GET:
- Para obtener el número de empleados llamaríamos la siguiente URL por GET:
2) Destacar que la misma URI puede tener varias finalidades, en los ejemplos anteriores usamos la misma URI para el alta de un empleado y para su actualización, sólo se diferencian en el método HTTP por el que se invoca y en este ejemplo en los parámetros
3) Destacar también que los parámetros se añaden en la URI del recurso, es en servicio web quien tiene mapeados cada uno de ellos
4) Destacar que se trabaja principalmente cadenas de texto, en envío de cualquier binario deberá ser codificado
5) En RESTfull existe el wadl, que es equivalente a los wsdl de los servicios web basados en XML.
7) Generar/Encontrar el wadl
Si usamos el framework Jersey, la implementación de referencia de JAX-RS, como en el presente ejemplo, podremos acceder al fichero wadl con la URL: http://localhost:8080/rest-ws/rest/application.wadl
8) Generar cliente Java desde wadl
Hay una herramienta llamada WALD que utiliza Jersey 1.x and JAX-RS 2.0 para generar un cliente Java de un determinado fichero *.wadl. Hay un plugin disponible para Maven.
Servicio web ejemplo
Fichero web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <servlet> <servlet-name>jersey-serlvet</servlet-name> <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class> <init-param> <param-name>com.sun.jersey.config.property.packages</param-name> <param-value>es.ine.sgtic.rest</param-value> </init-param> <init-param> <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name> <param-value>true</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>jersey-serlvet</servlet-name> <url-pattern>/rest/*</url-pattern> </servlet-mapping> </web-app>
Fichero EmpleadoRest.java
package es.ine.sgtic.rest; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import es.ine.sgtic.model.pojo.Empleado; import es.ine.sgtic.model.pojo.Empleados; import es.ine.sgtic.servicios.EmpleadoManager; @Path("/empleado") public class EmpleadoRest { private EmpleadoManager empMngr = new EmpleadoManager(); @GET @Path("{id: [0-9]+}") @Produces(MediaType.APPLICATION_JSON) /* * http://localhost:8080/rest-ws/rest/empleado/{id} */ public Empleado readEmpleado(@PathParam("id")String id) { return empMngr.readEmpleado(id); } @POST @Path("{id: [0-9]+}/{nombre}/{ape1}/{ape2}/{email}") @Produces(MediaType.APPLICATION_JSON) /* * http://localhost:8080/rest-ws/rest/empleado/{id}/{nombre}/{ape1}/{ape2}/{email} */ public Empleado createEmpleado(@PathParam("id")String id, @PathParam("nombre")String nombre, @PathParam("ape1")String ape1, @PathParam("ape2")String ape2, @PathParam("email")String email) { return empMngr.createEmpleado(id, nombre, ape1, ape2, email); } @PUT @Path("{id: [0-9]+}/{nombre}/{ape1}/{ape2}/{email}") @Produces(MediaType.APPLICATION_JSON) /* * http://localhost:8080/rest-ws/rest/empleado/{id}/{nombre}/{ape1}/{ape2}/{email} */ public Empleado updateEmpleado(@PathParam("id")String id, @PathParam("nombre")String nombre, @PathParam("ape1")String ape1, @PathParam("ape2")String ape2, @PathParam("email")String email) { return empMngr.updateEmpleado(id, nombre, ape1, ape2, email); } @DELETE @Path("{id: [0-9]+}") @Produces(MediaType.APPLICATION_JSON) /* * http://localhost:8080/rest-ws/rest/empleado/{id} */ public Empleado deleteEmpleado(@PathParam("id")String id) { return empMngr.deleteEmpleado(id); } @GET @Path("/count") @Produces(MediaType.APPLICATION_JSON) /* * http://localhost:8080/rest-ws/rest/empleado/count */ public String getCount() { return empMngr.getNumeroEmpleados(); } @GET @Path("/list") @Produces(MediaType.APPLICATION_JSON) /* * http://localhost:8080/rest-ws/rest/empleado/list */ public Empleados getEmpleados() { return empMngr.getListaEmpleados(); } @GET @Path("/find") @Produces(MediaType.APPLICATION_JSON) /* * Debería responder a cualquiera de estas combinaciones * http://localhost:8080/rest-ws/rest/empleado/find?id=2 * http://localhost:8080/rest-ws/rest/empleado/find?nombre=pepe * http://localhost:8080/rest-ws/rest/empleado/find?nombre=pepe&ape1=garcia */ public Empleados findEmpleado(@QueryParam("id")String id, @QueryParam("nombre")String nombre, @QueryParam("ape1")String ape1, @QueryParam("ape2")String ape2, @QueryParam("email")String email) { return empMngr.getListaEmpleadosCoincidentes(id, nombre, ape1, ape2, email); } }
Fichero EmpleadoManager.java
package es.ine.sgtic.servicios; import es.ine.sgtic.model.dao.EmpleadoDao; import es.ine.sgtic.model.pojo.Empleado; import es.ine.sgtic.model.pojo.Empleados; public class EmpleadoManager { private static final EmpleadoDao empDao = EmpleadoDao.getInstance(); public Empleado createEmpleado(final String id, final String nombre, final String ape1, final String ape2, final String email){ final Empleado emp = new Empleado(); emp.setId(id); emp.setNombre(nombre); emp.setApe1(ape1); emp.setApe2(ape2); emp.setEmail(email); return empDao.createEmpleado(id, emp); } public Empleado readEmpleado(String id){ return empDao.readEmpleado(id); } public Empleado updateEmpleado(final String id, final String nombre, final String ape1, final String ape2, final String email){ final Empleado emp = new Empleado(); emp.setId(id); emp.setNombre(nombre); emp.setApe1(ape1); emp.setApe2(ape2); emp.setEmail(email); return empDao.updateEmpleado(id, emp); } public Empleado deleteEmpleado(String id){ return empDao.deleteEmpleado(id); } /** * Ojo aquí el truco está en devolver el objeto 'empleados' que contiene internamente la lista de empleados, de otro modo estaríamos devolviendo una lista con N objetos empleado pero sin un 'nodo raiz', dos horas de pelea para enterder el error que daba. * @return */ public Empleados getListaEmpleados(){ final Empleados empleados = new Empleados(); empleados.getLista().addAll(empDao.getListaEmpleados().values()); return empleados; } public Empleados getListaEmpleadosCoincidentes(final String id, final String nombre, final String ape1, final String ape2, final String email){ final Empleados empleados = new Empleados(); empleados.getLista().addAll(empDao.getListaEmpleadosCoincidentes(id, nombre, ape1, ape2, email).values()); return empleados; } public String getNumeroEmpleados(){ return empDao.getNumeroEmpleados(); } }
Fichero EmpleadoDao.java
package es.ine.sgtic.model.dao; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.xml.bind.annotation.XmlRootElement; import es.ine.sgtic.model.pojo.Empleado; @XmlRootElement /** * Clase de acceso a datos, la montamos para evitar tener que acceder a * una BBDD de verdad. Al ser un Singleton hay una única instancia de la * clase y tendremos persistencia entre llamada y llamada de la lista de * empleados. */ public class EmpleadoDao { private static EmpleadoDao instance = null; final Map<String, Empleado> empleados; public static EmpleadoDao getInstance() { if(instance == null) { instance = new EmpleadoDao(); } return instance; } /** * Constructor donde metemos algunos empleados de prueba */ protected EmpleadoDao(){ empleados = new HashMap<String, Empleado>(); Empleado emp = new Empleado(); emp.setId("1"); emp.setNombre("nombre1"); emp.setApe1("ape11"); emp.setApe2("ape12"); emp.setEmail("nombre1.ape11@test.com"); empleados.put("1", emp); emp = new Empleado(); emp.setId("2"); emp.setNombre("nombre2"); emp.setApe1("ape21"); emp.setApe2("ape22"); emp.setEmail("nombre2.ape21@test.com"); empleados.put("2", emp); emp = new Empleado(); emp.setId("3"); emp.setNombre("nombre3"); emp.setApe1("ape31"); emp.setApe2("ape32"); emp.setEmail("nombre3.ape31@test.com"); empleados.put("3", emp); emp = new Empleado(); emp.setId("4"); emp.setNombre("antonio"); emp.setApe1("garcia"); emp.setApe2("gonzalez"); emp.setEmail("antonio.garcia.gonzalez@test.com"); empleados.put("4", emp); emp = new Empleado(); emp.setId("5"); emp.setNombre("antonio"); emp.setApe1("perez"); emp.setApe2("gonzalez"); emp.setEmail("antonio.perez.gonzalez@test.com"); empleados.put("5", emp); emp = new Empleado(); emp.setId("6"); emp.setNombre("luis"); emp.setApe1("garcia"); emp.setApe2("ruiz"); emp.setEmail("luis.garcia.ruiz@test.com"); empleados.put("6", emp); } public Map<String, Empleado> getListaEmpleados(){ return empleados; } /** * Devolvemos la lista de empleados coincidentes con TODOS los parámetros recibidos * @return */ public Map<String, Empleado> getListaEmpleadosCoincidentes(final String id, final String nombre, final String ape1, final String ape2, final String email){ //Clonamos la lista original de emplados final List<Empleado> lstEmpleadosCoincidentes = new ArrayList<Empleado>(); lstEmpleadosCoincidentes.addAll(empleados.values()); //Vamos eliminando de la lista clonada aquellos que no coincidan con alguno de los criterios recibidos for (Iterator<Empleado> iterator = lstEmpleadosCoincidentes.iterator(); iterator.hasNext();) { Empleado emp = iterator.next(); if (id!=null && !emp.getId().equals(id)) { iterator.remove(); } else if (nombre!=null && !emp.getNombre().equals(nombre)) { iterator.remove(); } else if (ape1!=null && !emp.getApe1().equals(ape1)) { iterator.remove(); } else if (ape2!=null && !emp.getApe2().equals(ape2)) { iterator.remove(); } else if (email!=null && !emp.getEmail().equals(email)) { iterator.remove(); } } //Generamos el Map de empleados coincidentes Map<String, Empleado> mapEmpleadosCoincidentes = new HashMap<String, Empleado>(); for (Empleado emp : lstEmpleadosCoincidentes) { mapEmpleadosCoincidentes.put(emp.getId(),emp); } //Devolvemos el Map de empleados coincidentes return mapEmpleadosCoincidentes; } public String getNumeroEmpleados(){ //TODO: Parece que le molesta si devuelves un Integer... return Integer.toString(empleados.size()); } public Empleado createEmpleado(final String id, final Empleado empleado){ return empleados.put(id, empleado); } public Empleado readEmpleado(String id){ return empleados.get(id); } public Empleado updateEmpleado(final String id, final Empleado empleado){ final Empleado emp = empleados.get(id); emp.setNombre(empleado.getNombre()); emp.setApe1(empleado.getApe1()); emp.setApe2(empleado.getApe2()); emp.setEmail(empleado.getEmail()); return emp; } public Empleado deleteEmpleado(String id){ return empleados.remove(id); } }
Ejemplo de cliente java
Un pequeño ejemplo Java que hace uso del servicio web.
Clase AppCliente.java con el main:
package es.ine.sgtic; import java.util.List; import org.codehaus.jackson.jaxrs.JacksonJaxbJsonProvider; import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.WebResource; import com.sun.jersey.api.client.config.ClientConfig; import com.sun.jersey.api.client.config.DefaultClientConfig; import com.sun.jersey.api.json.JSONConfiguration; import es.ine.sgtic.model.pojo.Empleado; import es.ine.sgtic.model.pojo.Empleados; public class AppCliente { private static ClientConfig clienteRestConfig; private static Client clienteRest; public static void main(String[] args) { //Instanciamos el cliente y lo configuramos para que acepte JSON clienteRestConfig = new DefaultClientConfig(); clienteRestConfig.getClasses().add(JacksonJaxbJsonProvider.class); clienteRestConfig.getFeatures().put(JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE); clienteRest = Client.create(clienteRestConfig); try { //Obtenemos el número de empleados obtenerNumeroEmpleados(); //Obtenemos la lista de empleados obtenerListaEmpleados(); //Realizamos diferentes búsquedas buscarEmpleadosCoincidentes(null,"antonio",null,null,null); buscarEmpleadosCoincidentes(null,"antonio",null,"gonzalez",null); buscarEmpleadosCoincidentes(null,null,"garcia",null,null); //TODO: Como se peta porque la lista Empleados recibida sólo tiene un elemento //hemos tenido que ajustar en el server el parámetro de inicio siguiente: //<param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name> buscarEmpleadosCoincidentes(null,"antonio","garcia","gonzalez",null); //Recuperamos el empleado 1 obtenerEmpleado("1"); //Añadimos un empleado crearEmpleado("7","nombre7","ape71","ape72", "nombre7.ape71@test.com"); //Obtenemos la lista de empleados por segunda vez obtenerListaEmpleados(); //Actualizamos empleado actualizarEmpleado("7","nombre7","ape71","apeCAMBIADO", "nombre7.ape71@test.com"); //Obtenemos la lista de empleados por segunda vez obtenerListaEmpleados(); //Borramos un empleado borrarEmpleado("7"); //Obtenemos la lista de empleados por segunda vez obtenerListaEmpleados(); } catch (Exception e) { e.printStackTrace(); } } private static void obtenerEmpleado(final String id) throws Exception { //Preparamos la URI final String EmpleadoURI = "http://localhost:8080/rest-ws/rest/empleado/1"; //Invocamos al WS obteniendo la respuesta final WebResource webResource = clienteRest.resource(EmpleadoURI); final ClientResponse respuesta = webResource.get(ClientResponse.class); //Verificamos si tenemos una respuesta correcta if (respuesta.getStatus() == 200) { Empleado emp = respuesta.getEntity(Empleado.class); System.out.println("-----------------------------------------------------------"); System.out.println(">> obtenerEmpleado("+id+")"); System.out.println(emp.getNombre() + " " + emp.getApe1() + " " + emp.getApe2() + " " + emp.getEmail()); System.out.println("-----------------------------------------------------------"); } else { throw new RuntimeException("HTTP Error: " + respuesta.getStatus()); } } private static void obtenerNumeroEmpleados() throws Exception { //Preparamos la URI final String EmpleadoURI = "http://localhost:8080/rest-ws/rest/empleado/count"; //Invocamos al WS obteniendo la respuesta final WebResource webResource = clienteRest.resource(EmpleadoURI); final ClientResponse respuesta = webResource.get(ClientResponse.class); //Verificamos si tenemos una respuesta correcta y la tratamos if (respuesta.getStatus() == 200) { String numEmp = respuesta.getEntity(String.class); System.out.println("-----------------------------------------------------------"); System.out.println(">> obtenerNumeroEmpleados()"); System.out.println("Hay un total de " + numEmp + " empleados."); System.out.println("-----------------------------------------------------------"); } else { throw new Exception("HTTP Error: " + respuesta.getStatus()); } } private static void obtenerListaEmpleados() throws Exception { //Preparamos la URI final String EmpleadoURI = "http://localhost:8080/rest-ws/rest/empleado/list"; //Invocamos al WS obteniendo la respuesta final WebResource webResource = clienteRest.resource(EmpleadoURI); final ClientResponse respuesta = webResource.get(ClientResponse.class); //Verificamos si tenemos una respuesta correcta y la tratamos if (respuesta.getStatus() == 200) { final Empleados empleados = respuesta.getEntity(Empleados.class); System.out.println("-----------------------------------------------------------"); System.out.println(">> obtenerListaEmpleados()"); final List<Empleado> lstEmpleados = empleados.getLista(); for (int c=0; c<empleados.getLista().size(); c++){ System.out.println(lstEmpleados.get(c).getNombre() + " " + lstEmpleados.get(c).getApe1() + " " + lstEmpleados.get(c).getApe2()+ " " + lstEmpleados.get(c).getEmail() ); } System.out.println("-----------------------------------------------------------"); } else { throw new Exception("HTTP Error: " + respuesta.getStatus()); } } private static void buscarEmpleadosCoincidentes(final String id, final String nombre, final String ape1, final String ape2, final String email) throws Exception { //Preparamos la URI final StringBuilder sbCriterios = new StringBuilder(); if (id!=null){ sbCriterios.append("id="+id); } if (nombre!=null){ if(sbCriterios.length()>0) { sbCriterios.append("&nombre="+nombre); } else { sbCriterios.append("nombre="+nombre); } } if (ape1!=null){ if(sbCriterios.length()>0) { sbCriterios.append("&ape1="+ape1); } else { sbCriterios.append("ape1="+ape1); } } if (ape2!=null){ if(sbCriterios.length()>0) { sbCriterios.append("&ape2="+ape2); } else { sbCriterios.append("ape2="+ape2); } } if (email!=null){ if(sbCriterios.length()>0) { sbCriterios.append("&email="+email); } else { sbCriterios.append("email="+email); } } System.out.println("http://localhost:8080/rest-ws/rest/empleado/find?"+sbCriterios.toString()); final String EmpleadoURI = String.format("http://localhost:8080/rest-ws/rest/empleado/find?%s", sbCriterios.toString()); //Invocamos al WS obteniendo la respuesta final WebResource webResource = clienteRest.resource(EmpleadoURI); final ClientResponse respuesta = webResource.get(ClientResponse.class); //Verificamos si tenemos una respuesta correcta y la tratamos if (respuesta.getStatus() == 200) { final Empleados empleados = respuesta.getEntity(Empleados.class); System.out.println("-----------------------------------------------------------"); System.out.println(">> buscarEmpleadosCoincidentes("+id+","+nombre+","+ape1+","+ape2+","+email+")"); final List<Empleado> lstEmpleados = empleados.getLista(); for (int c=0; c<empleados.getLista().size(); c++){ System.out.println(lstEmpleados.get(c).getNombre() + " " + lstEmpleados.get(c).getApe1() + " " + lstEmpleados.get(c).getApe2()+ " " + lstEmpleados.get(c).getEmail() ); } System.out.println("-----------------------------------------------------------"); } else { throw new Exception("HTTP Error: " + respuesta.getStatus()); } } private static void crearEmpleado(final String id, final String nombre, final String ape1, final String ape2, final String email) throws Exception { //Preparamos la URI final String EmpleadoURI = String.format("http://localhost:8080/rest-ws/rest/empleado/%s/%s/%s/%s/%s", id, nombre, ape1, ape2, email); //Invocamos al WS obteniendo la respuesta final WebResource webResource = clienteRest.resource(EmpleadoURI); final ClientResponse respuesta = webResource.post(ClientResponse.class); //Verificamos si tenemos una respuesta correcta if (respuesta.getStatus() == 200 || respuesta.getStatus() == 204) { System.out.println("-----------------------------------------------------------"); System.out.println(">> crearEmpleado("+id+","+nombre+","+ape1+","+ape2+","+email+") --> " + respuesta.getStatus()); System.out.println("-----------------------------------------------------------"); } else { throw new RuntimeException("HTTP Error: " + respuesta.getStatus()); } } private static void actualizarEmpleado(final String id, final String nombre, final String ape1, final String ape2, final String email) throws Exception { //Preparamos la URI final String EmpleadoURI = String.format("http://localhost:8080/rest-ws/rest/empleado/%s/%s/%s/%s/%s", id, nombre, ape1, ape2, email); //Invocamos al WS obteniendo la respuesta final WebResource webResource = clienteRest.resource(EmpleadoURI); final ClientResponse respuesta = webResource.put(ClientResponse.class); //Verificamos si tenemos una respuesta correcta if (respuesta.getStatus() == 200 || respuesta.getStatus() == 204) { Empleado emp = respuesta.getEntity(Empleado.class); System.out.println("-----------------------------------------------------------"); System.out.println(">> actualizarEmpleado("+id+","+nombre+","+ape1+","+ape2+","+email+") --> " + respuesta.getStatus()); System.out.println(emp.getNombre() + " " + emp.getApe1() + " " + emp.getApe2() + " " + emp.getEmail()); System.out.println("-----------------------------------------------------------"); } else { throw new RuntimeException("HTTP Error: " + respuesta.getStatus()); } } private static void borrarEmpleado(final String id) throws Exception { //Preparamos la URI final String EmpleadoURI = String.format("http://localhost:8080/rest-ws/rest/empleado/%s", id); //Invocamos al WS obteniendo la respuesta final WebResource webResource = clienteRest.resource(EmpleadoURI); final ClientResponse respuesta = webResource.delete(ClientResponse.class); //Verificamos si tenemos una respuesta correcta if (respuesta.getStatus() == 200 || respuesta.getStatus() == 204) { Empleado emp = respuesta.getEntity(Empleado.class); System.out.println("-----------------------------------------------------------"); System.out.println(">> borrarEmpleado("+id+") --> " + respuesta.getStatus()); System.out.println("-----------------------------------------------------------"); } else { throw new RuntimeException("HTTP Error: " + respuesta.getStatus()); } } }
Pojo empleados.java
package es.ine.sgtic.model.pojo; import java.util.ArrayList; import java.util.List; import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement //JAX-RS mapea automáticamente las clases anotadas con JAXB a JSON public class Empleados { List<Empleado> lista = new ArrayList<Empleado>(); public List<Empleado> getLista() { return lista; } public void setLista(List<Empleado> lista) { this.lista = lista; } }
Pojo empleado.java
package es.ine.sgtic.model.pojo; import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement //JAX-RS mapea automáticamente las clases anotadas con JAXB a JSON public class Empleado { private String id; private String nombre; private String ape1; private String ape2; private String email; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getNombre() { return nombre; } public void setNombre(String nombre) { this.nombre = nombre; } public String getApe1() { return ape1; } public void setApe1(String ape1) { this.ape1 = ape1; } public String getApe2() { return ape2; } public void setApe2(String ape2) { this.ape2 = ape2; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } }
No hay comentarios:
Publicar un comentario