Cliente Standalone de JAX-RS (o cualquier endpoint RESTful)



Si tenemos una aplicación standalone (puede ser un JavaFX, de línea de comandos, batch, etc) que necesite consumir un endpoint hecho en RESTful, por lo general usaríamos algo como esto:

public class RestClient {
  
    private static final String REST_URI 
      = "http://localhost:8082/spring-jersey/resources/employees";
  
    private Client client = ClientBuilder.newClient();
 
    public Employee getJsonEmployee(int id) {
        return client
          .target(REST_URI)
          .path(String.valueOf(id))
          .request(MediaType.APPLICATION_JSON)
          .get(Employee.class);
    }
    //...
}

(Tomado de https://www.baeldung.com/jersey-jax-rs-client)

Lo cual no está mal, pero creo que debería ser lo más transparente posible. ¿Cómo es eso?
Si ven en la línea 13 del código anterior, significa que hay que decirle que haga un GET a la petición, además de pasarle el tipo de respuesta y otras cosas más. La cuestión se volvería algo compleja si queremos hacer otras peticiones como POST, DELETE, etc.

Pues aquí vengo con una solución que encontré revisando la documentación de JAX-RS.

El servidor

Para este ejemplo, he creado un pequeño servidor CRUD en Payara Micro, el cual puedes obtener su código aquí: https://github.com/apuntesdejava/demo-jaxrs-standalone/tree/master/demo-jaxrs-server

El Endpoint principal es este:
@Path("person")
@Produces(APPLICATION_JSON)
@Consumes(APPLICATION_JSON)
@ApplicationScoped
public class PersonEndpoint {

    @Inject
    private PersonRepository personRepository;

    @POST
    public Response create(PersonParam param) {
        Person p = personRepository.create(param.getName(), param.getEmail());
        return Response.ok(p).build();
    }

    @GET
    public Response list() {
        List<Person> list = personRepository.findAll();
        return Response.ok(list).build();
    }
    
    @DELETE
    @Path("{id}")
    public Response delete(@PathParam("id")long personId){
        personRepository.delete(personId);
        return Response.ok().build();
    }

}

El cual tiene tres métodos principales:

  • @POST create() para insertar registros
  • @GET list() para leer todos los registros
  • @DELETE delete() para borrar un registro
Una manera para probarlo es ejecutándolo y llamando desde un cliente:

Insertando un registro:

Listando los registros:

Borrando ese registro (en mi caso, el 97)

El Cliente

En el cliente tiene que existir un método que luzca igual que la clase del endpoint servidor para que sea "transparente" la invocación.

@Path("person")
@Produces(APPLICATION_JSON)
@Consumes(APPLICATION_JSON)
public interface PersonEndpoint {

    @POST
    Response create(PersonParam param);

    @GET
    Response list();

    @DELETE
    @Path("{id}")
    Response delete(@PathParam("id") long personId);
}

Pero esto tiene un tratamiento muy especial: no es una clase, es una interfaz. Aquí es lo divertido ¿cómo es que lo podrá identificar cada petición? Pues esta es la magia del cliente JAX-RS.

Hay varios clientes de JAX-RS, algunos son:

  • RestEasy: https://github.com/resteasy/resteasy-examples/tree/3.6.0.Final/jaxrs-2.0/simple-client
  • Quarkus: https://quarkus.io/guides/rest-client
  • Apache CXF: https://cxf.apache.org/
Aquí usaré el Apache CXF. Independientemente puede usarse cualquier implementación, pero seguirá siendo la misma interfaz. Solo cambia cómo se invoca al Endpoint del Cliente.

Así se construye usando Apache CXF:
        PersonEndpoint client = JAXRSClientFactory.create(
                REST_URI,
                PersonEndpoint.class,
                Arrays.asList(
                        new JacksonJaxbJsonProvider()
                ));

Luego, se llama como si fuera cualquier método "local":
        PersonParam param = new PersonParam("persona 1", "abc@mail.com"); //creo los parámetros
        Response resp = client.create(param); //invoco al endpoint
        LOG.log(Level.INFO, "status:{0}", resp.getStatusInfo().getReasonPhrase()); //muestro la respuesta
        if (resp.getStatus() == Response.Status.OK.getStatusCode()) { //si está ok...
            Person p = resp.readEntity(Person.class); //.. convierto la petición en la entidad que se recibió...
            LOG.log(Level.INFO, "-> registro insertado:{0}", p.toString()); //... y muestro el contenido
        }

Si deseamos listar, también se haría lo mismo:
        resp = client.list(); //invocamos el método de listado
        LOG.log(Level.INFO, "status:{0}", resp.getStatusInfo().getReasonPhrase()); //mostramos el resultado...
        List<Person> list = null;  //preparamos nuestra lista que vamos a recibir
        if (resp.getStatus() == Response.Status.OK.getStatusCode()) { //evaluamos el contenido... si está OK...
            list = resp.readEntity(new GenericType<List<Person>>() {  //... convertimos la petición en el listado
            });
            list.forEach((p) -> { //... y podemos listar el contenido.
                LOG.log(Level.INFO, "id:{0}\tname:{1}\temail:{2}", new Object[]{p.getPersonId(), p.getName(), p.getEmail()});
            });
        }

Código fuente

El código fuente para este proyecto se puede encontrar aquí:


Comentarios

Entradas más populares de este blog

UML en NetBeans

Cambiar ícono a un JFrame

RESTful... la forma más ligera de hacer WebServices (Parte 1)