Les Web services ont été créés dans le but de connecter des systèmes disparates, de définir des sources de données communes et de maximiser la re-utilisation du code. Concrètement, si vous considérez une entreprise dont le système d'information a plusieurs dizaines d'années derrière lui. Vous aurez rarement un unique fournisseur, un seul logiciel et un seul type de machine. De plus pour des raisons de coût ou plus simplement de concurrence, il est rarement indiqué de tout uniformiser.
C'est à ce stade qu'interviennent les Web services car ils permettent à des éléments d'un parc hétérogène d'interagir (e.g. échanger des données, exécuter des méthodes à distance ...) entre eux. Il y a trivialement deux technologies derrière le terme Services Web : Les Web services SOAP et les Web services REST dont je parle ici.
REST (REpresentational State Transfer) n'est ni un protocole, ni un format, c'est trivialement une description de la manière dont le Web fonctionne. Vous accédez à une ressource via son URI et vous pouvez changer l'état de cette ressource en utilisant les méthodes natives de HTTP (GET, POST, PUT et DELETE). Dans cet article je vais indiquer comment déployer un Web service REST via une simple Servlet. Puis j'expliquerai comment le consommer.
REST propose une vision orientée ressource des Web services. C'est à dire qu'avec REST les variables, ce sont les objets (ou ressources). Selon l'approche REST, chaque ressource est identifiable de manière non ambiguë et unique via son URI (Universal Resource Identifier) de la forme http://localhost:8080/weatherws/cities/307.
Avec REST pour une ressource donnée, vous proposez un groupe de services (ou opérations) de base. Idéalement toujours les mêmes : créer, consulter, modifier et supprimer (voir CRUD). Les opérations à réaliser sur ces ressources, coïncident avec les méthodes natives de HTTP :
| HTTP | REST | Description |
|---|---|---|
| GET | consulter | Retourne une représentation XML de la ressource. L'état de la ressource est inchangé. |
| POST | créer | Créé une nouvelle ressource. |
| PUT | modifier | Met à jour une ressource existante. |
| DELETE | supprimer | Supprime la ressource du système. |
Dans ce document, je parcourai une application qui propose pour chaque ville, la météo courante. Ce sera un Web service fidèle aux fondamentaux de REST. Pour la sérialisation et la désérialisation du java en XML, j'utiliserai JAXB dont j'ai déjà parlé dans Sérialisation et désérialisation XML avec Java 6 (JAXB).
Le Web service qui donne pour chaque ville la météo courante sera une Servlet (voir CityServlet). Chaque ville sera représentée par un bean annoté pour JAXB (voir City).
En format XML une ville ressemble à ceci :
<?xml version="1.0" encoding="ISO-8859-1"?>
<city id="207">
<name>Grenoble</name>
<temperature>25</temperature>
<humidity>61</humidity>
<weather>Ensoleillé</weather>
<uri>http://localhost:8080/weatherws/cities/207</uri>
<lastUpdate>24-08-07 à 05:43</lastUpdate>
</city>
La température est exprimée en Celsius et l'humidité est un pourcentage.
Créez la structure de données java associée (City.java) :
/**
* @author Hyacinthe MENIET
* Created on 24 août 07
*/
package net.dotmyself.weatherws;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
"name",
"temperature",
"humidity",
"weather",
"uri",
"lastUpdate"
})
@XmlRootElement(name = "city")
public class City {
@XmlElement(required = true)
protected String name;
@XmlElement(required = true)
protected BigDecimal temperature;
@XmlElement(required = true)
protected long humidity;
@XmlElement(required = true)
protected String weather;
@XmlElement
protected String uri;
@XmlElement
protected String lastUpdate;
@XmlAttribute
protected Long id;
/**
* Gets the value of the name property.
*
* @return
* possible object is
* {@link String }
*
*/
public String getName() {
return name;
}
/**
* Sets the value of the name property.
*
* @param value
* allowed object is
* {@link String }
*
*/
public void setName(String value) {
this.name = value;
}
/**
* Gets the value of the temperature property.
*
* @return
* possible object is
* {@link BigDecimal }
*
*/
public BigDecimal getTemperature() {
return temperature;
}
/**
* Sets the value of the temperature property.
*
* @param value
* allowed object is
* {@link BigDecimal }
*
*/
public void setTemperature(BigDecimal value) {
this.temperature = value;
}
/**
* Gets the value of the humidity property.
*
*/
public long getHumidity() {
return humidity;
}
/**
* Sets the value of the humidity property.
*
*/
public void setHumidity(long value) {
this.humidity = value;
}
/**
* Gets the value of the weather property.
*
* @return
* possible object is
* {@link String }
*
*/
public String getWeather() {
return weather;
}
/**
* Sets the value of the weather property.
*
* @param value
* allowed object is
* {@link String }
*
*/
public void setWeather(String value) {
this.weather = value;
}
/**
* Gets the value of the uri property.
*
* @return
* possible object is
* {@link String }
*
*/
public String getUri() {
return uri;
}
/**
* Sets the value of the uri property.
*
* @param value
* allowed object is
* {@link String }
*
*/
public void setUri(String value) {
this.uri = value;
}
/**
* Gets the value of the lastUpdate property.
*
* @return
* possible object is
* {@link String }
*
*/
public String getLastUpdate() {
return lastUpdate;
}
/**
* Sets the value of the lastUpdate property.
*
* @param value
* allowed object is
* {@link String }
*
*/
public void setLastUpdate(String value) {
this.lastUpdate = value;
}
/**
* Gets the value of the id property.
*
* @return
* possible object is
* {@link Long }
*
*/
public Long getId() {
return id;
}
/**
* Sets the value of the id property.
*
* @param value
* allowed object is
* {@link Long }
*
*/
public void setId(Long value) {
this.id = value;
}
@Override
public String toString() {
return "City=" + name;
}
@Override
public boolean equals(Object obj) {
if (obj==this) {
return true;
}
if (obj instanceof City) {
City other = (City) obj;
if (this.name != other.name) {
if (this.name == null || !this.name.equals(other.name)) {
return false;
}
}
return true;
}
return false;
}
@Override
public int hashCode() {
return getName() != null ? getName().hashCode() : 1;
}
}
Le bean est annoté via des tags JAXB. Ces annotations faciliteront la sérialisation et désérialisation de java vers XML.
Pour donner un peu de réalisme à cet exemple, j'utilise une mémoire volatile (e.g. à chaque redémarrage de Tomcat les données sont perdues) mais suffisante pour contextualiser les échanges. Créez la classe (CityMemory.java) :
/**
* @author Hyacinthe MENIET
* Created on 21 août 07
*/
package net.dotmyself.weatherws;
import java.util.HashMap;
import java.util.Map;
/**
* Data Access Object which allows to Create, Retrieve, Update and Delete {@link City}.
*/
public class CityMemory {
private Map<Long, City> mapCities = new HashMap<Long, City>();
private long nextId = 0;
/**
* Inserts the given {@link City}.
* @param item
* The {@link City} to insert.
* @return
* The key associated with the {@link City}.
*/
public Long create(City item) {
Long key = nextKey();
item.setId(key);
mapCities.put(key, item);
return key;
}
/**
* Gets a {@link City}.
* @param key
* The key associated with the {@link City}.
* @return
* The {@link City} or <code>null</code> if
* no {@link City} could be found.
*/
public City retrieve(Long key) {
return mapCities.get(key);
}
/**
* Updates an existing {@link City}.
* @param key
* The key associated with the {@link City}.
* @param item
* The {@link City}.
*/
public void update(Long key,City item) {
if (mapCities.containsKey(key)) {
item.setId(key);
mapCities.put(key, item);
} else {
throw new IllegalArgumentException("There is no City with key=" + key);
}
}
/**
* Removes the {@link City} for this key if present.
* @param key
* The key associated with the {@link City}.
*/
public void delete(Long key) {
mapCities.remove(key);
}
/**
* Searches for the given {@link City}, testing for equality using the equals method.
* @param item
* The {@link City}.
* @return
* The key or <code>null</code> if no key could be found.
*/
public Long getKey(City item) {
for (Map.Entry<Long, City> entry : mapCities.entrySet()) {
if (entry.getValue().equals(item)) {
return entry.getKey();
}
}
return null;
}
/**
* Returns the next key available.
* @return
* The next key available.
*/
protected Long nextKey() {
return new Long(++nextId);
}
}
Terminons par la Servlet (CityServlet.java) :
/**
* @author Hyacinthe MENIET
* Created on 22 août 07
*/
package net.dotmyself.weatherws;
import java.io.IOException;
import java.io.OutputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
/**
* Servlet which uses HTTP verbs (GET,POST,PUT and DELETE) to perform a REST web service.
*/
public class CityServlet extends HttpServlet {
private static final long serialVersionUID = 7159907909932997509L;
private static final String characterEncoding = "iso-8859-1";
private static final String contentType = "application/xml";
private CityMemory memory;
public CityServlet () {
this.memory = new CityMemory();
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
try {
City city = getCityFromMemory(request);
city.setLastUpdate(getCurrentDate());
city.setUri(getCityUri(request,city.getId()));
// Marshalling retrieved City to the given OutputStream
JAXBContext context = JAXBContext.newInstance(City.class);
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.setProperty(Marshaller.JAXB_ENCODING, "ISO-8859-1");
response.setContentType(contentType);
response.setCharacterEncoding(characterEncoding);
OutputStream os = response.getOutputStream();
marshaller.marshal(city,os);
} catch (IllegalStateException e) {
response.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
} catch (Exception e) {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
try {
City city = getCityFromXml(request);
Long key = memory.create(city);
response.setHeader("Location", getCityUri(request, key));
response.setStatus(HttpServletResponse.SC_CREATED);
} catch (Exception e) {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
}
}
@Override
protected void doPut(HttpServletRequest request, HttpServletResponse response) throws IOException {
try {
City city = getCityFromMemory(request);
city.setLastUpdate(getCurrentDate());
memory.update(city.getId(), getCityFromXml(request));
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
} catch (Exception e) {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
}
}
@Override
protected void doDelete(HttpServletRequest request, HttpServletResponse response) throws IOException {
memory.delete(getKey(request));
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
}
private String getCityUri(HttpServletRequest request, Long key) {
return request.getScheme() + "://" + request.getServerName() + ":"
+ request.getServerPort() + request.getContextPath()
+ request.getServletPath()
+ "/" + key;
}
private String getCurrentDate() {
DateFormat dateFormat = new SimpleDateFormat("dd-MM-yy 'à' hh':'mm");
return dateFormat.format(new Date());
}
private City getCityFromXml(HttpServletRequest request) throws JAXBException, IOException {
JAXBContext context = JAXBContext.newInstance(City.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
City city = (City) unmarshaller.unmarshal(request.getReader());
return city;
}
private City getCityFromMemory(HttpServletRequest request) {
City city = memory.retrieve(getKey(request));
if (city == null) {
throw new NullPointerException("Can't find the requested city");
}
return city;
}
private Long getKey(HttpServletRequest request) {
String[] parts = request.getPathInfo().split("/");
if (parts.length <= 1) {
throw new IllegalStateException("Missing id");
}
return Long.valueOf(parts[1]);
}
}
Pour déployer votre application sous Tomcat vous aurez besoin d'un descripteur de déploiement (web.xml) :
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<display-name>Weather of the world</display-name>
<description>
This application is a Web service which allows to retrieve
the weather of several cities in the world
</description>
<init-param>
<param-name>readonly</param-name>
<param-value>false</param-value>
</init-param>
<servlet>
<servlet-name>CityServlet</servlet-name>
<servlet-class>net.dotmyself.weatherws.CityServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>CityServlet</servlet-name>
<url-pattern>/cities/*</url-pattern>
</servlet-mapping>
</web-app>
Créez un fichier index.jsp qui servira de page d'accueil Web pour votre Web service :
Utilisez ant ou l'outil de votre choix pour créer un war (par exemple weatherws.war) avec l'arborescence suivante :
Quand vous avez terminé, disposez simplement votre war dans CATALINA_HOME/webapps/ et redémarrez Tomcat. Vous pouvez tester l'application Web en vous connectant à l'adresse http://localhost:8080/weatherws/.
Une fois n'est pas coutume, pour interagir avec le Web service ci-dessus, je ne créerai pas de client Java. Premièrement parce que j'aborde cet aspect dans Clients Web services REST avec Java 6 (JAXB). Ensuite, c'est l'occasion de présenter l'outil en ligne de commande : curl fort utile pour déboguer un Web service ou toute application réseau.
Ajoutez la ville de Grenoble :
La ville de Grenoble a bien été ajoutée comme le signale le HTTP/1.1 201 Crée. La réponse vous retourne également l'URI à partir de laquelle vous consulterez la météo relative à Grenoble :
Ajoutez d'autres villes :
Si vous avez lu le paragraphe ci-dessus, vous savez déjà comment consulter la météo relative à une ville. Par exemple pour consulter la météo associée à Madrid (ville n° 3), tapez :
Modifiez la ville de Tokyo (ville n° 4) :
Vérifiez que les changements sont pris en compte :
Supprimez la ville de Paris (ville n° 2) :
Vérifiez que la ville a correctement été retirée de l'application :