Webservices Restful avec JEE

Introduction

Ce billet traitera bien évidemment de JAX-RS,  Java API for RESTful Web Services, c’est simplement une interface de programmation fournie par JEE afin d’implémenter des webservices avec une architecture REST (representational state transfer). Cet article ne donne que des exemples incomplets mais qui sont le résultat d’expériences concrètes chez différents clients, pour plus de précisions il faudra aller voir les tutos JAX-RS de Oracle ou JBOSS. Donc pour toute question n’hésitez pas à me solliciter.

Qu’est ce que REST?

Les webservices REST contrairement à SOAP qui est un protocole, s’appuie totalement sur les standards web et sur le protocole HTTP. REST a été décrit pour la première fois par Roy Fielding en 2000. Le concept d’une architecture REST est simple et se base sur le concept de ressources, dans REST tout est ressource et on y accède par l’intermédiaire des méthodes HTTP :

  • DELETE pour la suppression
  • GET pour la récupération
  • PATCH pour la modification partiel
  • POST pour la création
  • PUT pour la modification
  • etc…

Chaque ressource est identifiée par une URI unique, celle-ci nous permettra de récupérer, créer, modifier, supprimer notre dite ressource. Et enfin REST nous donne la possibilité de récupérer ces différentes ressources sous différents formats tel que du texte, de l’XML , du JSON etc…

Pour finir sur REST, si on respecte les standards que j’évoque ci-dessus on pourra parler d’architecture RESTFul. Car beaucoup croient qu’ils font du REST en exposant des ressources par Webservices, par exemple pour supprimer une ressource livre certains font :

GET /<context-root>/livre?id=1&method=delete

Hors ici on ne s’appuie pas sur le verbe HTTP GET pour indiquer le type d’action que l’on désire appliquer, dans un cadre RESTFul on devrait faire :

DELETE /<context-root>/livre/1

Pour plus de détails sur les Webservices RESTFul je vous invite à à vous documenter, car pour faire du RESTFul il y a d’autres contraintes que celles que j’évoque ci-dessus, il existe plein d’articles donnant les détails exacts de ce type d’architecture.

JAX-RS

Par la suite nous parlerons de JAX-RS 2.0 qui est fournit par JEE7 qui correspond à la JSR 339 qui succède à JAX-RS 1 qui découle de la JSR 311.

Implémentations JAX-RS

Les Annotations

Annotation Description
@ApplicationPath(String) Définit le pont d’entrée de notre application REST. Cette annotation se place sur la classe
@Path(String) Définit le chemin de la ressource. Cette annotation se place sur la classe et/ou sur la méthode implémentant le service
@DELETE Définit le verbe HTTP implémenté par la méthode de la classe
@GET Définit le verbe HTTP implémenté par la méthode de la classe
@POST Définit le verbe HTTP implémenté par la méthode de la classe
@PUT Définit le verbe HTTP implémenté par la méthode de la classe
@Consumes(String[]) Spécifie le ou les Types MIME de la réponse du service
@Produces(String[]) Spécifie le ou les Types MIME accepté en entrée du service
@CookieParam(String) Permet de récupérer un cookie dans le service
@FormParam(String) Permet de récupérer un champ de formulaire dans le service
@HeaderParam(String) Permet de récupérer un header HTTP dans le service
@PathParam(String) Permet de récupérer un élément du Path d’appel(/élément)
@QueryParam(String) Permet de récupérer un paramètre de l’url (?parametre=valeur)

La liste n’est pas exhaustive j’ai surtout cité les annotations qui servent très régulièrement, il reste d’autres annotations comme @NameBinding qui permet de mapper une méthode et un interceptor.

Comment implémente-t-on une application

Application REST

Pour commencer on va implémenter une simple classe qui hérite de javax.ws.rs.core.Application et est annotée par @ApplicationPath

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

@ApplicationPath("/rest")
public class RestApplication extends Application {
}

C’est le point d’entrée de notre application REST, toutes les ressources seront accessibles à partir de

 /<context-root>/rest/

Service REST

Commençons par une ressource qu’on voudra exposer à l’aide de nos services REST, que j’ai simplement appelé MaRessourceJson

import java.io.Serializable;

public class MaRessourceJson implements Serializable{
	

	/**
	 * 
	 */
	private static final long serialVersionUID = 4254739092263227552L;
	
	private int id;
	
	private String properties;
	
	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getProperties() {
		return properties;
	}

	public void setProperties(String properties) {
		this.properties = properties;
	}

}

Comme vous pouvez le constater c’est un simple bean qui implémente l’interface Serializable et qui contient deux propriétés id et properties.

Et voici le service qui servira notre première ressource MaRessource

import javax.ws.rs.Consumes;
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.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

...

@Path("/ma-ressource")
public class MaRessourceRest {
	
	...
	
	@GET
	@Produces(MediaType.APPLICATION_JSON)
	public Response getMesRessources() {
		// le service retourne une liste de ressoure List et un code HTTP 200
		return Response.ok(maRessourceService.findAll()).build();
	}

	@GET
	@Path("/{id}")
	@Produces(MediaType.APPLICATION_JSON)
	public Response getMaRessourceParId(@PathParam("id") String id) {
		// le service retourne une ressoure : MaRessource et un code HTTP 200
		return Response.ok((maRessourceService.getMaRessourceById(id)).build();
	}

	@POST
	@Produces(MediaType.APPLICATION_JSON)
	@Consumes(MediaType.APPLICATION_JSON)
	public Response createMaRessource(MaRessourceJson maRessource) {
		// le service crée une ressoure MaRessource, retourne l'ID de la ressource et un code HTTP 201
		MaRessourceJson result = maRessourceService.createMaRessource(maRessource);
		return Response.status(Status.CREATED).entity(result.getId()).build();
	}
	
	@PUT
        @Path("/{id}")
	@Produces(MediaType.APPLICATION_JSON)
	@Consumes(MediaType.APPLICATION_JSON)
	public Response updateMaRessource(@PathParam("id") String id, MaRessourceJson maRessource) {
		// le service met à jour une ressoure MaRessource, retourne la ressource et un code HTTP 200
		maRessourceService.updateMaRessource(maRessource);
		return Response.ok(maRessource).build();
	}

	@DELETE
	@Path("/{id}")
	@Produces(MediaType.APPLICATION_JSON)
	public Response deleteMaRessource(@PathParam("id") String id) {
		// le service supprime une ressoure MaRessource et retourne un code HTTP 204
                maRessourceService.deleteMaRessource(id);
		return Response.status(Status.NO_CONTENT).build();
	}

	
}

Comme vous pouvez le constater je n’ai pas eu à indiquer à mon service que le point d’entrée sera /<context-root>/rest/ma-ressource JAX-RS le fait automatiquement car mon point d’entrée étend javax.ws.rs.core.Application.

Ce service REST peut :

  • Retourner la liste de toutes les ressources « maResource » avec la méthode getMesRessources en appelant GET /<context-root>/rest/ma-ressource
  • Retourner une ressource avec la méthode getMaRessource en appelant GET /<context-root>/rest/ma-ressource/id
  • Créer une ressource avec createMaRessource en appelant POST /<context-root>/rest/ma-ressource
  • Mettre à jour une ressource avec updateMaRessource en appelant PUT /<context-root>/rest/ma-ressource/id
  • Et enfin supprimer une ressource avec deleteMaRessource en appelant DELETE /<context-root>/rest/ma-ressource/id

il est à noter que le nom de mes méthodes est arbitraire, tout comme le nom de ma classe, la seule chose qui compte c’est l’annotation @Path et le verbe HTTP apposé sur la méthode. De plus dans mes exemples mes services ne consomment et produisent que du JSON (MediaType.APPLICATION_JSON).

Autre particularité, nous utilisons ici en retour de nos services Rest l’objet Response, cette objet va nous permettre de renvoyer le code http, la ressource à retourner, les headers etc… Ce qui donne concrètement en retour d’un appel HTTP :

# Request
GET /rest/ma-ressource HTTP/1.1
Host: localhost:8080
Accept: application/json;q=1.0

# Response
HTTP/1.1 200 OK
Content-Type: application/json
[{"id": 0...}]

 

On se posera quand même la question de savoir comment la conversion Objet -> Json et Json -> Objet se fait? Et bien c’est grâce à une librairie de « data binding » que JAX-RS pourra le faire de manière transparente. Par exemple Wildfly 10 fournit nativement la librairie Jackson 2. Mais on peut utiliser d’autres librairies de « JSON provider » tel que GSON (google), JSONP (Oracle) ou encore JSON.simple. Vous pouvez lire un comparatif de ces 4 librairies ici.

Donc nous venons d’écrire avec deux classes un fournisseur de Webservices REST qui expose une ressource « MaRessource » et ce sans paramétrage autre, c’est à dire qu’il n’y a rien de spéciale dans notre fichier web.xml que contiendra notre war à déployer sur n’importe quel des serveurs d’application JEE tel que TomEE, Weblogic, Wildfly.

Gérer les erreurs

On peut constater dans le code écrit précédemment qu’on ne fait aucune gestion d’erreur. Imaginons que l’ID d’une ressource à mettre à jour ou à supprimer n’existe pas ou bien qu’une exception technique est levée lors d’appel aux services métier.

Il existe avec JAX-RS une façon très élégante de gérer les codes retour spécifique grâce à l’annotation @Provider.

Imaginons que nous avons créée une classe exception particulière pour gérer nos exceptions métier, une classe BusinessException, et bien nous pouvons avec JAX-RS automatiquement envoyer le bon retour si une exception est levée sur notre service métier sans gérer les cas d’erreur dans notre service REST. Cela permet de simplifier le code de nos services REST. Il suffit pour cela d’implémenter un mapper qui sera annoté @Provider qui implémente l’interface javax.ws.rs.ext.ExceptionMapper.

import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

import fr.test.rest.exceptions.BusinessException;

@Provider
public class BusinessExceptionMapper implements ExceptionMapper<BusinessException> {

 @Override
 public Response toResponse(BusinessException BusinessException) {
    return Response.status(Response.Status.BAD_REQUEST)
            .entity(BusinessException).type(MediaType.APPLICATION_JSON)
            .build();
 }

}

Dans notre exemple nous renvoyons systématiquement un code erreur 500 (Response.Status.BAD_REQUEST) sur la levée d’une BusinessException, nous pouvons bien entendu affiner cela dans une vrai application métier. Mais cette exemple illustre la simplicité de la mise en oeuvre de la gestion des exceptions pour nos webservices REST.

Client REST

Avec JAX-RS 2.0 est arrivé un client de webservice REST, ci-dessous un exemple pour récupérer une ressource MaRessource :

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;


public class ClientRest {
	
	public static void main(String[] args){
		Client client = ClientBuilder.newBuilder().newClient();
		WebTarget target = client.target("http://<host>/test/rest");
		target = target.path("ma-ressource/" + 1);
		 
		Invocation.Builder builder = target.request();
		Response response = builder.get();
		MaRessource maRessource = builder.get(MaRessource.class);
		
	}
}

Avec JAX-RS 2.0 arrivent d’autres nouveautés tel que faire du traitement asynchrone de requête HTTP, les filtres et les interceptors.
L’idée de cette article était surtout d’expliquer un peu les concepts REST et de donner un point de départ pour développer des Webservices REST en JEE, pour ce qui est des fonctionnalités avancées cela pourra faire l’objet d’un prochain article.

You may also like...