Ir al contenido principal

Spring Rest

Spring dispone de librerías propias diseñadas para facilitar el desarrollo de APIS REST. La anotación @RestController permite registrar una clase Java como un manejador de peticiones contra rutas de url; y orienta el manejador como una respuesta REST (sin estado, y orientada a los datos).

En las librerias de spring para servicios web, se incluye un Servlet de refencia que se registra en las aplicaciones web, y procesa todas las peticiones entrantes bajo determinada ruta. Dicho Servlet carga un contexto de Spring, y entre otros, analiza las clases que están marcadas con la anotacion RestController, buscando todos los métodos que se mapeen contra una petición Http (GetMapping, PostMapping, PutMapping, PathMapping, DeleteMapping). Con toda esa información agregada durante la construcción del contexto el servlet es capaz de redirigir las peticiones entrantes a las clases Java adecuadas. El servlet de Spring se encargará de convertir los "datos de entrada" definidos en los objetos Java más adecuados para invocar al controlador, y con el resultado: se encargar de convertir los objetos de los resultados en los "datos de salida" adeucados. Si alguna de la conversiones fallan, se producirá un error fuera de nuestras clases de negocio.

@RestController
@RequestMapping("/foos")
class FooController {
    @Autowired
    private IFooService service;
 
    @GetMapping
    public List findAll(@QueryParam(name="edad", required=false) Integer edad) {
        return edad==null ? service.findAll() : service.findAllByEdad( edad );
    }
 
    @GetMapping(value = "/{id}")
    public Foo findById(@PathVariable("id") Long id) {
        return service.findById(id);
    }
 
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Foo create(@RequestBody Foo resource) {
        return service.create(resource);
    }
 
    @PutMapping(value = "/{id}")
    @ResponseStatus(HttpStatus.OK)
    public Foo update(@PathVariable( "id" ) Long id, @RequestBody Foo resource) {
        return service.update(id, resource);
    }
 
    @DeleteMapping(value = "/{id}")
    @ResponseStatus(HttpStatus.OK)
    public void delete(@PathVariable("id") Long id) {
        service.deleteById(id);
    }

}

Con este sencillo código estamos definiendo el típico controlador de Spring para Rest:

  • Con RequestMapping indicamos una ruta url raiz que será manejada por este controlador.
  • Con @GetMapping definimos un método que recupera una lista de elementos en caso de hacer un get contra la url raiz del recurso.
    • Tenemos un argumento del método llamado edad, que hemos anotado como un query param no requerido. De esa forma si se indica un ?edad=### el valor del parámetro se recogerá en esa variable (siendo nulo en casao de no tener valor).
  • Con @GetMapping(value="/{id}") definimos un método que recupera un registro en caso de hacer un get contra la url del recurso, seguida de un / que tomaremos como id.
    • A la parte / que indicamos como {id} en el mapping, accedemos anotando uno de los parámetros de la petición como @PathVariable.
  • Con @PostMapping definimos un método que se invocará para crear registros.
    • El argumento de la función está marcado com un @RequestBoyd. Por ello Spring intentará convertir el texto del body de la petición en un objeto java. Habitualmente las peticiones y respuestas ya se hacen usando json, con lo que lo normal será que Spring convierta el objeto json en la instancia Java en memoria.
    • Al anotar el método con la anotación @ResponseStatus indicamos que a mayores de la respuesta en forma de objeto, tendremos un estado HTTP 200.

En todos los casos, Spring cogerá el objeto retornado por la función, y en base a las cabeceras de Accept del cliente compondrá un objeto de respuesta. Lo habitual será que se el objeto se serialice como un json.

Paginado y parámetros en los listados

Un elemento importante a tener en cuenta cuande se trabaja con un API rest que ofrece una consulta para listar datos, es le necesario que los resultados de dicho listao esten separados en páginas. Siempre deberíamos tratar los datos de maneras coherente y unificadas. Por lo tanto si tenemos seis recursos distintos, y para uno de ellos necesitamos paginar, lo correcto sería que todos los recursos recuperasen sus datos de forma paginada.

Spring, en sus interfaces para repositorios de datos proporciona métodos para el paginado. La interface PagingAndSortingRepository es antecesor tanto de JpaRepository como de MongoRepository, con lo que es sencillo hacer uso de ella. La interfaz PagingAndSortingRepository retorna objetos de tipo Page que contienen la pagina de resultados concreta, así como información para moverse entre páginas. Como ventaja adicional, el objeto Page ya está diseñado para que se pueda serializar cómodamente en Json; lo que hace más fácil utilizarlo como resultado de la paginación.

Otro de los habituales problemas con los métodos de listado es la recepción de parámetros. Si tenemos un recurso con una estructura grande (muchos atributos), es fácil que tengamos varios filtros para los listados. Si contamos un parámetro para el número de pagina, otro para el tamaño de página, y un tercero para ordenar los resultados... con poco o nada ya estamos creando un método con muchos parámetros que puede ser dificil de gestionar. Spring intenta rellenar automáticamente los parámetro de un método de controlador que sean de clases complejas a partir de los parametros de la query, pero teniendo en cuenta que el parámetro debe cumplir varios requisitos:

  • Debe estar marcado con la anotación @Valid (de javax.validation).
  • No puede estar marcado como @QueryParam
  • El resto de parámetros tienen que tener una marca que los permita extraer de la petición (PathParam, RequestBody, HeaderParam...)

Cumpliendo esos sencillos pasos, podemos usar un bean para recibir los query params y utilizarlo para componer filtros complejos.

  public Page list(@Valid FooSearhParams filter,
      @RequestParam(required = false, name="pageNumber") Integer pageNumber,
      @RequestParam(required = false, name="pageSize") Integer pageSize) {
  ....

  class FooSearchParams {
    private String name;
    private int edadMaxima;
    private int departamento;

    .....
  }

Con ese código, spring parseará la url /foo?name=*os*&departamento=1 y la invocación de list se hará con filter.getName() == '*os*' y filter.getDepartamento() == 1

Control de errores

En un API rest debemos encargarnos de que los errores sean consistentes. Los diferentes códigos de error http tienen significado, y deberíamos utilizarlos para tener un API consistente.

Cuando Spring ejecuta un método de un RestController, si dicho método genera una excepción, se buscará en el contexto un Servicio de resolución de excepciones para Rest. Si en nuestro proyecto tenemos una o más clases anotadas con @ControllerAdvice, spring se encargará de usarlas para buscar manejadores de excepciones.

@ControllerAdvice
public class ExceptionHandlerEstandarHttp {
  @ExceptionHandler(MaxUploadSizeExceededException.class)
  public ResponseEntity handleMaxSizeException(
      final MaxUploadSizeExceededException exception) {
    return new ResponseEntity<>(HttpStatus.PAYLOAD_TOO_LARGE);
  }
  @ExceptionHandler(NotAllowedException.class)
  public ResponseEntity handleNotAllowedException(final NotAllowedException exception,
      final HttpServletRequest request) {
    return request.getUserPrincipal() == null ? new ResponseEntity<>(HttpStatus.UNAUTHORIZED)
        : new ResponseEntity<>(HttpStatus.FORBIDDEN);
  }
  @ExceptionHandler(NoResultException.class)
  public ResponseEntity handleNoFoundException(final NoResultException exception) {
    return new ResponseEntity<>("Not found", HttpStatus.NOT_FOUND);
  }

De esta forma estamos respetando la división de responsabilidades, y la inversión de control. La clase controladora sigue sin tener dependencias con HTTP, y podríamos utilizarla en otros contextos sin especiales problemas.

HttpServletRequest y HttpServletResponse

Si quisieramos acceder directamente a la petición o la respuesta simplemente podríamos hacerlo añadiendo parámetros de dicho tipo. Spring intentará resolver primero los diferentes parámetros desde el contexto, y para una petición web; tanto el HttpServletRequest como el HttpServletResponse estan disponibles.

Como practica de desarrollo no es aconsejable. Una vez que hemos incluido esos parámetros estaremos haciendo que el controlador dependa de las peticiones http, y no será tan sencillo usar sus métodos en otro contexto.

Comentarios