Java ws restful

De The Linux Craftsman
Aller à la navigation Aller à la recherche
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

Introduction

Après la lecture du cours sur les Web Services, nous allons voir comment "fabriquer" notre propre Web Service Restful avec JAX-RS, GSON et Tomcat.

Ce Web Service permettra le CRUD (Create / Read / Update / Delete) d'utilisateurs, fonctionnalité plus que nécessaire dans les applications...

JAX-RS

Java définit le support de REST à travers la JSR 311 (Java Specification Request). Cette spécification est appelée JAX-RS (The Java API for RESTful Web Services).

JAX-RS utilise des annotations pour définir le caractère RESTful de certaines classes Java.

Jersey

JAX-RS étant uniquement une spécification (un bout de papier) nous n'allons pas pouvoir faire grand chose si nous n'utilisons pas une librairie qui implémente ces spécifications.

C'est justement l'objectif de Jersey que vous pouvez télécharger en cliquant sur le lien ci-dessous:

Jersey download link.png

GSON

Comme décrit dans le cours, On manipule des représentations des ressources, par les ressources directement.

La représentation de nos ressources se fera en JSON et l'implémentation que nous allons utiliser et celle faite par Google: GSON.

Vous pouvez la télécharger ici

Préparation du projet

Tout d'abord, il faut créer un projet sous Eclipse comme décrit ici.

Ajout des librairies

Il faut extraire les librairies précédemment téléchargées dans le dossier WebContent → WEB-INF → lib de votre projet:

Adding lib tomcat ws.png

Modification du web.xml

C'est dans le fichier web.xml que nous allons ajouter les indications qui vont permettre à Tomcat de trouver vos Web Services. Rappel : ce fichier se trouve dans le répertoire WebContent → WEB-INF

<?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_3_0.xsd"
	id="WebApp_ID" version="3.0">
	<display-name>userws</display-name>
	<servlet>
		<servlet-name>Jersey REST Service</servlet-name>
		<servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
		<init-param>
			<param-name>jersey.config.server.provider.packages</param-name>
			<param-value>fr.pigier.user.rest</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>
	<servlet-mapping>
		<servlet-name>Jersey REST Service</servlet-name>
		<url-pattern>/rest/*</url-pattern>
	</servlet-mapping>
</web-app>

Quelques explications s'imposent:

  • <display-name>...</display-name> → permet de définir le nom de l'application.
  • <servlet>...</servlet> → permet de définir une Servlet.
    • <servlet-name>...</servlet-name> → nom de la Servlet.
    • <servlet-class>...</servlet-class> → classe de la Servlet.
    • <init-param>...</init-param> → définit un paramètre à passer à la Servlet
      • <param-name>...</param-name> → le nom du paramètre
      • <param-value>...</param-value> → sa valeur
    • <load-on-startup>1</load-on-startup> → permet de démarrer la Servlet au démarrage de la WebAPP (application)
  • <servlet-mapping></servlet-mapping> → permet de définir un mapping entre une Servlet et une URL
    • <servlet-name>...</servlet-name> → le nom de la Servlet comme définit dans la balise <servlet>
    • <url-pattern>...</url-pattern> → l'URL utilisée pour joindre la Servlet

Le principe de fonctionnement est simple: on spécifie le package où se trouve les classes qui contiennent les Web Service RESTful (fr.pigier.user.rest) et Jersey se charge du reste.

Création de l'application

Les packages

Nous aurons besoin des packages suivants:

  • fr.pigier.user.model → pour les classes correspondant au modèle ;
  • fr.pigier.user.rest → pour nos Web Services ;
  • fr.pigier.user.helper → pour les classes servant à faire des traitements autonomes.
Java rest ws package.png

Création du modèle

Avant de pouvoir offrir une représentation d'un objet... il nous faut un objet ! Créez une classe User avec les attributs suivants:

  • id;
  • login;
  • password;
package fr.pigier.user.model;

public class User {

	private int id;
	private String login, password;

	/**
	 * @param login
	 * @param password
	 * @param id
	 */
	public User( int id, String login, String password) {
		super();
		this.login = login;
		this.password = password;
		this.id = id;
	}
	/**
	 * @return the login
	 */
	public String getLogin() {
		return login;
	}
	/**
	 * @param login the login to set
	 */
	public void setLogin(String login) {
		this.login = login;
	}
	/**
	 * @return the password
	 */
	public String getPassword() {
		return password;
	}
	/**
	 * @param password the password to set
	 */
	public void setPassword(String password) {
		this.password = password;
	}
	/**
	 * @return the id
	 */
	public int getId() {
		return id;
	}
	/**
	 * @param id the id to set
	 */
	public void setId(int id) {
		this.id = id;
	}
	/* (non-Javadoc)
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + id;
		result = prime * result + ((login == null) ? 0 : login.hashCode());
		result = prime * result
				+ ((password == null) ? 0 : password.hashCode());
		return result;
	}
	/* (non-Javadoc)
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		User other = (User) obj;
		if (id != other.id)
			return false;
		if (login == null) {
			if (other.login != null)
				return false;
		} else if (!login.equals(other.login))
			return false;
		if (password == null) {
			if (other.password != null)
				return false;
		} else if (!password.equals(other.password))
			return false;
		return true;
	}
	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return "User [login=" + login + ", password=" + password + ", id=" + id
				+ "]";
	}
	
}

Création du Web Service

La classe qui va gérer notre Web Service est un objet classique et non une Servlet !

Elle utilise uniquement les annotations JAX-RS comme énoncé plus haut:

package fr.pigier.user.rest;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import fr.pigier.user.model.User;

@Path("/user")
public class UserWS {

	private User user;

	public UserWS() {
		// Création d'un utilisateur pour les tests
		user = new User(0, "jc", "forton");
	}

	@GET
	@Produces(MediaType.TEXT_PLAIN)
	public String getUserText() {
		return user.toString();
	}

	@GET
	@Produces(MediaType.TEXT_HTML)
	public String getUserHtml() {
		String html = "<html> " + "<title>" + "User" + "</title>"
		        + "<body><h1>" + user.toString() + "</body></h1>" + "</html> ";
		return html;
	}

	@GET
	@Produces(MediaType.TEXT_XML)
	public String getUserXml() {
		String xml = "<?xml version=\"1.0\"?><user id=" + user.getId()
				+ " login=" + user.getLogin() + " password="
				+ user.getPassword() + "></user>";
		return xml;
	}

}

Comme on peut le constater, la classe UserWS possède:

  • une méthode :
    • getUserText qui renvoie une réponse formatée en text ;
    • getUserHTML qui renvoie une réponse formatée en HTML ;
    • getUserXml qui renvoie une réponse formatée en XML.
  • des annotations :
    • @GET pour spécifier le type de requête ;
    • @Produces pour spécifier le type de retour;
    • @Path pour spécifier l'URL d'accès au Web Service.

Voici les annotations utilisées par Jersey:

Annotation Description

@PATH(your_path)

Sets the path to base URL + /your_path. The base URL is based on your application name, the servlet and the URL pattern from the web.xml configuration file.

@POST

Indicates that the following method will answer to an HTTP POST request.

@GET

Indicates that the following method will answer to an HTTP GET request.

@PUT

Indicates that the following method will answer to an HTTP PUT request.

@DELETE

Indicates that the following method will answer to an HTTP DELETE request.

@Produces(MediaType.TEXT_PLAIN[, more-types])

@Produces defines which MIME type is delivered by a method annotated with @GET. In the example text ("text/plain") is produced. Other examples would be "application/xml" or "application/json".

@Consumes(type[, more-types])

@Consumes defines which MIME type is consumed by this method.

@PathParam

Used to inject values from the URL into a method parameter. This way you inject, for example, the ID of a resource into the method to get the correct object.

source

N'oubliez pas de démarrer le serveur Tomcat avant de poursuivre !

Utilisation du Web Service

Contenu HTML

Nous allons utiliser RESTClient, un plugin Firefox, pour tester notre Web Service.

Il suffit d'utiliser l'URL suivante pour accéder à votre Web Service: http://127.0.0.1:8080/UserWS/rest/user

Java user ws html.png

Autres contenus

Par défaut, le contenu renvoyé sera en HTML. Si vous désirez retourner un contenu dans un autre format, il faudra ajouter le header Accept:

Restclient header add.png

Spécifiez le type de retour souhaité et ajoutez-le :

Restclient header param.png
Restclient header use.png

Le retour du Web Service est alors modifié en fonction de ce header :

  • text/html ;
  • text/plain ;
  • text/xml ;

Formatage en JSON

Pour formater en JSON, il faut utiliser la librairie GSON et voici comment on procède:

import com.google.gson.Gson;

User user = new User(0, "jcf", "password"
Gson gson = new Gson();

String json = gson.toJson(user);
// json contient: {"id":0,"login":"jcf","password":"password"}

Le problème d'un tel code est qu'il oblige à instancier un objet Gson à chaque fois qu'une Servlet voudra renvoyer du Json... ce qui n'est pas très optimisé.

Il faudrait que le code qui permet la transformation en Json ne soit instancié qu'une seul fois puis réutilisé par la suite. C'est le rôle du Singleton.

Patron de conception Singleton

L'objectif du Singleton est de restreindre l'instanciation d'une classe à un seul objet.

Création du Singleton

On implémente un singleton en écrivant une classe contenant une méthode qui crée une instance uniquement s'il n'en existe pas. Sinon, elle renvoie une référence vers l'objet qui existe déjà. Il faudra veiller à ce que le constructeur de la classe soit privé, afin de s'assurer que la classe ne puisse être instanciée autrement que par la méthode de création contrôlée (getInstance).

Wikipedia

package fr.pigier.user.helper;

import com.google.gson.Gson;

public final class JsonManager {

	private Gson gson;
	private static volatile JsonManager instance;
	
	private JsonManager(){
		gson = new Gson();
	}
	
	public static JsonManager getInstance(){
		if(JsonManager.instance == null){
			instance = new JsonManager();
		}
		return instance;
	}
	
	public String toJson(Object o){
		return gson.toJson(o);
	}
	
}

Appel du Singleton

Pour appeler notre Singleton, il suffit de passer par la méthode getInstance():

User user = new User(0, "jcf", "password");

String json = JsonManager.getInstance().toJson(user);
// json contient: {"id":0,"login":"jcf","password":"password"}

Modification du Web Service

On peut maintenant modifier notre Web Service pour y ajouter la méthode getUserJson() :

package fr.pigier.user.rest;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import fr.pigier.user.model.User;

@Path("/user")
public class UserWS {

	private User user;

	public UserWS() {...}

	@GET
	@Produces(MediaType.TEXT_PLAIN)
	public String getUserText() {...}

	@GET
	@Produces(MediaType.TEXT_HTML)
	public String getUserHtml() {...}

	@GET
	@Produces(MediaType.TEXT_XML)
	public String getUserXml() {...}

	@GET
	@Produces(MediaType.APPLICATION_JSON)
	public String getUserJson() {
		return JsonManager.getInstance().toJson(user);
	}

}

Patron de conception DAO

Avant de poursuivre, il faut un moyen de mutualiser les données dans notre application. Pour rappel, les Servlets sont indépendantes et donc, il sera impossible de créer un utilisateur de manière fiable et unique.

Servlet pool.png

Le conteneur de Servlet (Tomcat) démarre plusieurs Servlets (un pool) et ne garantit pas que la même Servlet sera appelée à chaque fois. Cela constitue un problème pour le stockage des données qui doit absolument être centralisé.

Nous allons utiliser, encore une fois, un patron de conception: le DAO.

Introduction

Les objets en mémoire vive sont souvent liés à des données persistantes (stockées en base de données, dans des fichiers, dans des annuaires, etc.). Le modèle DAO propose de regrouper les accès aux données persistantes dans des classes à part, plutôt que de les disperser. Il s'agit surtout de ne pas écrire ces accès dans les classes "métier", qui ne seront modifiées que si les règles de gestion métier changent.

Wikipedia

Création du DAO

Pour que les données soient centralisées, il faut garantir l'unicité de l'instance du DAO grâce au Singleton:

package fr.pigier.user.controller;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import fr.pigier.user.model.User;

public class DaoManager {

	private Map<Integer, User> users;
	private int currentId = 0;
	private static volatile DaoManager instance;

	private DaoManager() {
		users = new HashMap<Integer, User>();
	}

	public static DaoManager getInstance() {
		if (DaoManager.instance == null) {
			instance = new DaoManager();
		}
		return instance;
	}

	/**
	 * 
	 * @return
	 * la liste des utilisateurs
	 */
	public Collection<User> readUsers(){
		return users.values();
	}
	
	/**
	 * Permet la lecture d'un utilisateur
	 * @param id
	 * L'id de l'utilisateur
	 * @return
	 * L'utilisateur s'il existe ou null
	 */
	public User readUser(int id) {
		// On vérifie si l'utilisateur existe
		if (!isUser(id)) {
			// On renvoie faux s'il n'existe pas
			return null;
		}
		// On retourne l'utilisateur
		return users.get(id);
	}

	/**
	 * Méthode permettant la création d'un utilisateur
	 * @param user
	 * L'utilisateur à créer
	 * @return
	 * L'id de l'utilisateur nouvellement créé
	 */
	public int createUser(User user) {
		// On initialise l'id de l'utilisateur
		user.setId(currentId);
		// On ajoute l'utilisateur dans le map
		users.put(currentId, user);
		// On incrémente currentId pour le prochain ajout
		currentId++;
		// On retourne l'id de l'utilisateur
		return currentId-1;
	}

	/**
	 * Permet l'effacement d'un utilisateur
	 * @param id
	 * L'id de l'utilisateur à effacer
	 * @return
	 * vrai si l'utilisateur à été éffacé, sinon faux
	 */
	public boolean deleteUser(int id) {
		// On vérifie si l'utilisateur existe
		if (!isUser(id)) {
			// On renvoie faux s'il n'existe pas
			return false;
		}
		// On le retire de la map
		users.remove(id);
		// On renvoie vrai
		return true;
	}

	/**
	 * Permet la modification d'un utilisateur
	 * @param user
	 * L'utilisateur à modifier
	 * @return
	 * vrai si l'utilisateur à été modifié, sinon faux
	 */
	public boolean modifyUser(User user) {
		// On vérifie si l'effacement fonctionne
		if (!deleteUser(user.getId())) {
			// On renvoie faux
			return false;
		}
		// On ajoute l'utilisateur dans la map au même index
		users.put(user.getId(), user);
		// On renvoie vrai
		return true;
	}

	/**
	 * Methode permettant de vérifier l'existance d'un utilisateur
	 * 
	 * @param id
	 * L'id de l'utilisateur
	 * @return
	 * vrai si l'utilisateur existe, sinon faux
	 */
	public boolean isUser(int id) {
		return users.containsKey(id);
	}

}

Appel du DAO

Pour appeler notre DAO, il suffit de passer par la méthode getInstance():

User user = new User(0, "jcf", "password");

DaoManager.getInstance().createUser(user);

Implémentation du CRUD

Le CRUD pour Create, Read, Update, Delete, va nous permettre de rendre notre Web Service complètement fonctionnel.

Create

Ici, nous allons utiliser la méthode POST présente dans le protocole HTTPhttp://127.0.0.1:8080/UserWS/rest/user

@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON)
public Response createUser(@FormParam("login") String login,
		@FormParam("password") String password) {
	int id = DaoManager.getInstance().createUser(new User(0, login, password));
	return Response.ok().entity(JsonManager.getInstance().toJson(id)).build();
}

Pour appeler notre Web Service il va falloir passer les paramètres qui sont récupérés par @FormParam. Pour cela, il faut ajouter le header suivant:

Header xml url encoded.png

Puis ajouter les paramètres dans le corps de la requête:

Header param encoded.png

Read

Ici, nous allons utiliser la méthode POST présente dans le protocole HTTP:

@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response getUserByIdJson(@PathParam("id") int id) {
	if (!DaoManager.getInstance().isUser(id)) {
		return Response.status(404).build();
	}
	String json = JsonManager.getInstance().toJson(DaoManager.getInstance().readUser(id));
	return Response.ok().entity(json).build();
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getUsersJson() {
	String json = JsonManager.getInstance().toJson(DaoManager.getInstance().readUsers());
	return Response.ok().entity(json).build();
}

Update

Ici, nous allons utiliser la méthode PUT présente dans le protocole HTTPhttp://127.0.0.1:8080/UserWS/rest/user/0

@PUT
@Path("{id}")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response updateUser(@PathParam("id") int id, @FormParam("login") String login,
		@FormParam("password") String password) {
	if(!DaoManager.getInstance().modifyUser(new User(id, login, password))){
		return Response.status(404).build();
	}
	return Response.ok().build();
}

Pour appeler notre Web Service il va falloir passer les paramètres qui sont récupérés par @FormParam. Pour cela, il faut ajouter le header Content-Type avec la valeur application/x-www-form-urlencoded comme pour la création

Puis ajouter les paramètres dans le corps de la requête comme pour la création

Delete

Ici, nous allons utiliser la méthode DELETE présente dans le protocole HTTPhttp://127.0.0.1:8080/UserWS/rest/user/0

@DELETE
@Path("{id}")
public Response deleteUser(@PathParam("id") int id) {
	if(!DaoManager.getInstance().deleteUser(id)){
		return Response.status(404).build();
	}
	return Response.ok().build();
}

Utilisation avec AJAX

Pour utiliser un client en JavaScript, il faut modifier le header Access-Control-Allow-Origin pour autoriser les requêtes depuis un navigateur !

@GET
@Produces(MediaType.APPLICATION_JSON)
Public Response getUserJson() {
	String userStr = JsonManager.getInstance().toJson(user);
	return Response.ok()
		      .header("Access-Control-Allow-Origin", "*")
		      .header("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE").entity(userStr).build();
}

Voici un exemple de client Javascript:

<html>
<head>
	<title>UserWS</title>
	<script type="text/javascript">
		function getXhr() {
			var xhr;
			if (window.XMLHttpRequest) {
				// code for IE7+, Firefox, Chrome, Opera, Safari
				xhr = new XMLHttpRequest();
			} else {
				// code for IE6, IE5
				xhr = new ActiveXObject("Microsoft.XMLHTTP");
			}
			return xhr;
		}
		function getUserList(){
			var xhr = new XMLHttpRequest();
			xhr.open("GET", "http://127.0.0.1:8080/UserWS/rest/user/0",false);
			xhr.setRequestHeader("Accept", "application/json");
			xhr.send();
			var result = xhr.responseText;
			var user = JSON.parse(result);
			alert(user.login+" : "+user.password);
		}
	</script>
</head>
<body onLoad="getUserList();">
</body>
</html>

Ajout d'une authentification

Pour ajouter une authentification à nos Web Service, nous allons utiliser les Filter qui permettent de couper le fil d’exécution d'une requête.

web.xml

<filter>
  <filter-name>login</filter-name>
  <filter-class>fr.pigier.user.filter.LoginFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>login</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

Dans le package fr.pigier.user.filter

package fr.pigier.user.filter;

import java.io.IOException;
import java.nio.charset.Charset;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.tomcat.util.codec.binary.Base64;

/**
 * Servlet Filter implementation class LoginFilter
 */
@WebFilter("/LoginFilter")
public class LoginFilter implements Filter {

	/**
	 * @see Filter#doFilter(ServletRequest, ServletResponse, FilterChain)
	 */
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
		// On caste l'objet ServletResponse en HttpServletResponse
		HttpServletResponse resp = (HttpServletResponse) response;
		if(!authenticate(request)){
			// On coupe le fil de l'execution en renvoyant un code 401
			resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
		}
		// On laisse continuer le fil de l'execution
		chain.doFilter(request, response);
	}

	private boolean authenticate(ServletRequest request){
		// On caste l'objet ServletRequest en HttpServletRequest
		HttpServletRequest req = (HttpServletRequest) request;
		// On recupere le header authorization
		String authHeader = req.getHeader("authorization");
		// On recupere la deuxieme partie du header (sans "Basic")
		String encodedValue = authHeader.split(" ")[1];
		// On decode la chaine d'authetification
		String credentials = new String(Base64.decodeBase64((encodedValue).toString()),
				Charset.forName("UTF-8"));
		// On separe le login...
		String login = credentials.split(":")[0];
		// ... du mot de passe
		String password = credentials.split(":")[1];
		// On verifie les valeurs
		if(login.equals("toto") && password.equals("password")){
			return true;
		}
		return false;
	}
	
	@Override
	public void destroy() {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void init(FilterConfig arg0) throws ServletException {
		// TODO Auto-generated method stub
		
	}

}

Utilisation dans RestClient

Dans RestClient, il suffit d'utiliser l'authentification basic: Spécifiez le type de retour souhaité et ajoutez-le :

Restclient header auth menu.png
Restclient header auth use.png