Servicios web RESTful con Java
Introducción
Los servicios web RESTful se han convertido en el estándar de facto para la comunicación entre sistemas distribuidos en la web moderna. Esta arquitectura, basada en los principios REST (Representational State Transfer) propuestos por Roy Fielding, permite crear APIs escalables, fáciles de mantener y que aprovechan al máximo las características del protocolo HTTP. Java, con su rico ecosistema de frameworks y bibliotecas, ofrece herramientas excelentes para desarrollar servicios RESTful robustos y eficientes. En este artículo, aprenderemos los conceptos fundamentales de REST y cómo implementar servicios web RESTful utilizando Java.
¿Qué es REST?
REST (Representational State Transfer) es un estilo de arquitectura para sistemas distribuidos que se basa en un conjunto de principios que describen cómo los recursos son definidos y accedidos a través de la red. No es un protocolo ni un estándar, sino un conjunto de restricciones arquitectónicas.
Principios básicos de REST
- Arquitectura cliente-servidor: Separa las responsabilidades entre cliente y servidor, permitiendo que evolucionen independientemente.
- Sin estado (Stateless): Cada petición del cliente al servidor debe contener toda la información necesaria para entender y completar la petición.
- Cacheable: Las respuestas deben indicar si pueden ser almacenadas en caché para mejorar el rendimiento.
- Sistema por capas: El cliente no puede distinguir si está conectado directamente al servidor final o a un intermediario.
- Interfaz uniforme: Simplifica la arquitectura y desacopla la implementación de los servicios.
Recursos y URIs
En REST, todo se modela como un recurso (una entidad) que puede ser accedido mediante un identificador único (URI). Por ejemplo:
/usuarios
- Colección de usuarios/usuarios/123
- Un usuario específico con ID 123/usuarios/123/publicaciones
- Publicaciones del usuario 123
Métodos HTTP en REST
REST utiliza los métodos HTTP para definir operaciones sobre los recursos:
- GET: Obtener un recurso (lectura)
- POST: Crear un nuevo recurso
- PUT: Actualizar completamente un recurso existente
- PATCH: Actualizar parcialmente un recurso
- DELETE: Eliminar un recurso
Implementando servicios RESTful en Java
Existen varias tecnologías para implementar servicios RESTful en Java. Las más populares son JAX-RS (Java API for RESTful Web Services) y Spring REST. Veremos ejemplos de ambas aproximaciones.
JAX-RS con Jersey
JAX-RS es la especificación estándar de Java EE para servicios RESTful. Jersey es la implementación de referencia de JAX-RS.
Configuración básica
Primero, necesitamos añadir las dependencias necesarias. Si estamos usando Maven, añadimos esto al archivo pom.xml
:
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>3.1.1</version>
</dependency>
Luego, configuramos la aplicación JAX-RS creando una clase que extiende Application
:
package com.ejemplo.rest;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
@ApplicationPath("/api")
public class AplicacionRest extends Application {
// La configuración básica está completa con la anotación
}
Creando un modelo
Creemos una simple clase modelo para nuestro ejemplo:
package com.ejemplo.modelo;
public class Producto {
private Long id;
private String nombre;
private double precio;
// Constructor vacío necesario para deserialización
public Producto() {
}
public Producto(Long id, String nombre, double precio) {
this.id = id;
this.nombre = nombre;
this.precio = precio;
}
// Getters y setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
public double getPrecio() {
return precio;
}
public void setPrecio(double precio) {
this.precio = precio;
}
}
Implementando un recurso REST
Ahora crearemos un recurso REST que exponga operaciones CRUD sobre nuestros productos:
package com.ejemplo.rest;
import com.ejemplo.modelo.Producto;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.*;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Path("/productos")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ProductoResource {
// Simulamos una base de datos con un Map
private static Map<Long, Producto> productos = new HashMap<>();
private static Long idContador = 1L;
static {
// Datos iniciales
productos.put(1L, new Producto(1L, "Portátil", 899.99));
productos.put(2L, new Producto(2L, "Smartphone", 499.99));
idContador = 3L;
}
@GET
public List<Producto> obtenerTodos() {
return new ArrayList<>(productos.values());
}
@GET
@Path("/{id}")
public Response obtenerPorId(@PathParam("id") Long id) {
Producto producto = productos.get(id);
if (producto == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
return Response.ok(producto).build();
}
@POST
public Response crear(Producto producto) {
producto.setId(idContador++);
productos.put(producto.getId(), producto);
URI ubicacion = UriBuilder.fromResource(ProductoResource.class)
.path(String.valueOf(producto.getId()))
.build();
return Response.created(ubicacion)
.entity(producto)
.build();
}
@PUT
@Path("/{id}")
public Response actualizar(@PathParam("id") Long id, Producto producto) {
if (!productos.containsKey(id)) {
return Response.status(Response.Status.NOT_FOUND).build();
}
producto.setId(id);
productos.put(id, producto);
return Response.ok(producto).build();
}
@DELETE
@Path("/{id}")
public Response eliminar(@PathParam("id") Long id) {
if (!productos.containsKey(id)) {
return Response.status(Response.Status.NOT_FOUND).build();
}
productos.remove(id);
return Response.noContent().build();
}
}
Spring REST
Spring Framework ofrece excelente soporte para crear servicios RESTful mediante Spring MVC y Spring Boot.
Configuración con Spring Boot
Primero, creamos un proyecto Spring Boot con las dependencias necesarias. Para Maven, nuestro pom.xml
incluirá:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Aplicación principal
package com.ejemplo.springrest;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AplicacionSpringRest {
public static void main(String[] args) {
SpringApplication.run(AplicacionSpringRest.class, args);
}
}
Implementando un controlador REST
Con Spring, creamos controladores REST utilizando la anotación @RestController
:
package com.ejemplo.springrest.controlador;
import com.ejemplo.springrest.modelo.Producto;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/productos")
public class ProductoControlador {
// Simulamos una base de datos con un Map
private static Map<Long, Producto> productos = new HashMap<>();
private static Long idContador = 1L;
static {
// Datos iniciales
productos.put(1L, new Producto(1L, "Portátil", 899.99));
productos.put(2L, new Producto(2L, "Smartphone", 499.99));
idContador = 3L;
}
@GetMapping
public List<Producto> obtenerTodos() {
return new ArrayList<>(productos.values());
}
@GetMapping("/{id}")
public ResponseEntity<Producto> obtenerPorId(@PathVariable Long id) {
Producto producto = productos.get(id);
if (producto == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(producto);
}
@PostMapping
public ResponseEntity<Producto> crear(@RequestBody Producto producto) {
producto.setId(idContador++);
productos.put(producto.getId(), producto);
URI ubicacion = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(producto.getId())
.toUri();
return ResponseEntity.created(ubicacion).body(producto);
}
@PutMapping("/{id}")
public ResponseEntity<Producto> actualizar(@PathVariable Long id, @RequestBody Producto producto) {
if (!productos.containsKey(id)) {
return ResponseEntity.notFound().build();
}
producto.setId(id);
productos.put(id, producto);
return ResponseEntity.ok(producto);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> eliminar(@PathVariable Long id) {
if (!productos.containsKey(id)) {
return ResponseEntity.notFound().build();
}
productos.remove(id);
return ResponseEntity.noContent().build();
}
}
Consumiendo servicios REST desde Java
Además de crear servicios REST, también es importante saber cómo consumirlos. Java ofrece varias opciones para esto.
Usando HttpClient (Java 11+)
Java 11 introdujo una nueva API HTTP Cliente que facilita el consumo de servicios REST:
package com.ejemplo.cliente;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class ClienteRest {
private static final HttpClient cliente = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
public static void main(String[] args) throws Exception {
// Ejemplo de petición GET
HttpRequest solicitud = HttpRequest.newBuilder()
.uri(new URI("http://localhost:8080/api/productos"))
.GET()
.build();
HttpResponse<String> respuesta = cliente.send(
solicitud,
HttpResponse.BodyHandlers.ofString());
System.out.println("Código de estado: " + respuesta.statusCode());
System.out.println("Cuerpo de la respuesta: " + respuesta.body());
// Ejemplo de petición POST
String jsonProducto = "{\"nombre\":\"Monitor\",\"precio\":249.99}";
HttpRequest solicitudPost = HttpRequest.newBuilder()
.uri(new URI("http://localhost:8080/api/productos"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonProducto))
.build();
HttpResponse<String> respuestaPost = cliente.send(
solicitudPost,
HttpResponse.BodyHandlers.ofString());
System.out.println("Código de estado (POST): " + respuestaPost.statusCode());
System.out.println("Cuerpo de la respuesta (POST): " + respuestaPost.body());
}
}
Usando RestTemplate (Spring)
Si estamos trabajando con Spring, podemos usar RestTemplate
:
package com.ejemplo.springrest.cliente;
import com.ejemplo.springrest.modelo.Producto;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import java.util.Arrays;
import java.util.List;
public class ClienteSpring {
private static final String URL_BASE = "http://localhost:8080/api/productos";
private static final RestTemplate restTemplate = new RestTemplate();
public static void main(String[] args) {
// Obtener todos los productos
ResponseEntity<Producto[]> respuesta = restTemplate.getForEntity(
URL_BASE, Producto[].class);
List<Producto> productos = Arrays.asList(respuesta.getBody());
System.out.println("Productos obtenidos: " + productos.size());
// Obtener un producto por ID
Producto producto = restTemplate.getForObject(
URL_BASE + "/{id}",
Producto.class,
1);
System.out.println("Producto obtenido: " + producto.getNombre());
// Crear un nuevo producto
Producto nuevoProducto = new Producto(null, "Tablet", 299.99);
Producto creado = restTemplate.postForObject(
URL_BASE,
nuevoProducto,
Producto.class);
System.out.println("Producto creado con ID: " + creado.getId());
// Actualizar un producto
creado.setPrecio(329.99);
restTemplate.put(URL_BASE + "/{id}", creado, creado.getId());
// Eliminar un producto
restTemplate.delete(URL_BASE + "/{id}", creado.getId());
}
}
Buenas prácticas para servicios RESTful
Para diseñar servicios REST efectivos, considera estas recomendaciones:
- Usa correctamente los métodos HTTP según su semántica
- Diseña URIs intuitivas y jerárquicas que representen recursos, no acciones
- Utiliza los códigos de estado HTTP adecuados para comunicar el resultado de una operación
- Versiona tu API para permitir cambios sin romper clientes existentes
- Implementa HATEOAS (Hypermedia as the Engine of Application State) para permitir navegación por la API
- Utiliza autenticación y autorización para proteger tus recursos
- Documenta tu API usando herramientas como Swagger/OpenAPI
- Gestiona errores de forma consistente con mensajes informativos
Resumen
Los servicios web RESTful en Java nos permiten crear APIs escalables, mantenidas e interoperables que siguen los principios REST. Hemos explorado cómo implementar estos servicios utilizando dos aproximaciones populares: JAX-RS (con Jersey) y Spring REST. También hemos visto cómo consumir servicios REST desde aplicaciones Java utilizando HttpClient y RestTemplate.
Dominar el desarrollo de servicios RESTful es esencial para cualquier desarrollador Java moderno, ya que estas APIs forman la columna vertebral de la comunicación entre sistemas en aplicaciones distribuidas. Con las herramientas y conocimientos adquiridos en este artículo, ahora puedes comenzar a diseñar e implementar tus propios servicios web RESTful usando Java.