Spring Boot est un très bon moyen pour débuter avec Spring. Il vous permet de construire des applications Spring avec peu d'effort.
Aerospike est une base de données distribuée et répliquée en mémoire, optimisée pour utiliser à la fois la mémoire DRAM et Flash/SSD native.
Aerospike a aussi une grande fiabilité et est compatible ACID. Les développeurs peuvent rapidement faire évoluer leur cluster de base de données de deux nœuds à vingt noeuds sans arrêter le service de base de données.
Qu'est-ce que vous allez construire
Cet article va vous guider à travers la création d'un service Web RESTful simple avec Spring Boot.
Vous allez construire un service qui accepte une demande HTTP GET. Il répond avec le JSON suivant :
{"expiration":121023390,"bins":{"DISTANCE":2446,"DEST_CITY_NAME":"New York","DEST":"JFK","YEAR":2012,"ORI_AIRPORT_ID":"14679","DEP_TIME":"802","DAY_OF_MONTH":12,"DEST_STATE_ABR":"NY","ORIGIN":"SAN","FL_NUM":160,"CARRIER":"AA","ORI_STATE_ABR":"CA","FL_DATE":"2012/01/12","AIR_TIME":291,"ORI_CITY_NAME":"San Diego","ELAPSED_TIME":321,"ARR_TIME":"1623","AIRLINE_ID":19805},"generation":1}
Les données que vous allez utiliser sont des informations de vols commerciaux (inclus dans le code d'exemple), c'est un fichier de données flights_from.csv
. Il contient environ un million de dossiers de vol.
Il y a également de nombreuses fonctionnalités ajoutées à votre application "out-of-the-box" pour la gestion du service en environnement de production (ou autre). Cela vient fonctionnellement de Spring (voir le guide de Spring : Construire un service web RESTful).
Ce dont vous aurez besoin
- Votre éditeur de texte ou IDE préféré
- Le JDK7 ou supérieur
- Maven 3.0+
- Aerospike Java SDK 3.0+
Mettre en place le projet
Vous pouvez utiliser n'importe quel système de build pour la création d'applications avec Spring, mais le code Maven est présenté dans cet article. Si vous n'êtes pas familier avec Maven, vous pouvez vous référer au guide de Spring : Construire des Projets Java avec Maven.
Vous aurez aussi besoin de construire et d'installer le client Java Aerospike dans votre dépôt local. Téléchargez et décompressez la source, puis exécutez les commandes Maven suivantes :
mvn install:install-file -Dfile=client/depends/gnu-crypto.jar -DgroupId=org.gnu -DartifactId=gnu-crypto -Dversion=2.0.1 -Dpackaging=jar
mvn clean
mvn package
Création de la structure de répertoire
Dans le projet de votre choix, créez la structure suivante :
└─ src/
└─ main/
└─ java/
└─ com/
└─ aerospike/
└─ client/
└─ rest/
Création du pom Maven
Créez un fichier pom.xml
pour Maven à la racine de votre projet, avec le contenu suivant :
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.aerospike</groupId>
<artifactId>aerospike-restful-example</artifactId>
<version>1.0.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>0.5.0.M4</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Aerospike client. -->
<dependency>
<groupId>com.aerospike</groupId>
<artifactId>aerospike-client</artifactId>
<version>3.0.9</version>
</dependency>
<!-- Apache command line parser. -->
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
<properties>
<start-class>com.aerospike.client.rest.AerospikeRESTfulService</start-class>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>http://repo.spring.io/libs-snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>http://repo.spring.io/libs-snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
Cela paraît effrayant, mais çà ne l'est pas réellement.
Création de la classe de traduction en JSON
L'API AeroSpike retournera un objet Record et il contiendra la génération, l'expiration et les valeurs des nombres "bin" (séries de valeurs numériques). Mais vous voulez avoir ces valeurs renvoyées au format JSON. La meilleure façon d'y parvenir est d'utiliser une classe de traduction.
Créer une classe de traducteur avec le code suivant. Il s'agit d'une classe générique qui traduira un objet Record AeroSpike en un JsonObject.
src/main/java/com/aerospike/client/rest/JSONRecord.java
package com.aerospike.client.rest;
import java.util.Map;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import com.aerospike.client.Record;
/**
* JSONRecord is used to convert an Aerospike Record
* returned from the cluster to JSON format
*
*/
@SuppressWarnings("serial")
public class JSONRecord extends JSONObject {
@SuppressWarnings("unchecked")
public JSONRecord(Record record){
put("generation", record.generation);
put("expiration", record.expiration);
put("bins", new JSONObject(record.bins));
if (record.duplicates != null){
JSONArray duplicates = new JSONArray();
for (Map<String, Object> duplicate : record.duplicates){
duplicates.add(new JSONObject(duplicate));
}
put("duplicates", duplicates);
}
}
}
Cette classe n'est pas compliquée et est très générique. Vous pouvez spécialiser votre traduction de JSON pour les objets Record spécifiques.
Créer un contrôleur de ressource
Avec Spring, les points de terminaison de services REST sont des contrôleurs Spring MVC. Le code suivant gère une requête GET pour /as/{namespace}/{set}/getAll/1234
et retourne l'enregistrement (objet Record) dont la clé est 1234
, où {namespace}
est la variable de chemin d'accès (path) pour l'espace de noms AeroSpike et {set}
est la variable de chemin d'accès (path) pour le set
AeroSpike.
src/main/java/com/aerospike/client/rest/RESTController.java
package com.aerospike.client.rest;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import com.aerospike.client.AerospikeClient;
import com.aerospike.client.Bin;
import com.aerospike.client.Key;
import com.aerospike.client.Record;
import com.aerospike.client.policy.Policy;
import com.aerospike.client.policy.WritePolicy;
@Controller
public class RESTController {
@Autowired
AerospikeClient client;
@RequestMapping(value="/as/{namespace}/{set}/getAll/{key}", method=RequestMethod.GET)
public @ResponseBody JSONRecord getAll(@PathVariable("namespace") String namespace,
@PathVariable("set") String set,
@PathVariable("key") String keyvalue) throws Exception {
Policy policy = new Policy();
Key key = new Key(namespace, set, keyvalue);
Record result = client.get(policy, key);
return new JSONRecord(result);
}
}
Le contrôleur de service REST fournira les données dans le corps de la réponse (response body), dans notre cas un objet JSON qui représente l'enregistrement lut à partir d'AeroSpike.
L'annotation @ResponseBody
explique à Spring MVC d'écrire l'objet renvoyé dans le corps de la réponse.
Créer une classe exécutable principale (méthode main)
Mettez en oeuvre la méthode main
pour créer un contrôleur Spring MVC. La meilleure façon de le faire est d'utiliser la "helper" classe SpringApplication.
src/main/java/com/aerospike/client/rest/AerospikeRESTfulService.java
package com.aerospike.client.rest;
import java.util.Properties;
import javax.servlet.MultipartConfigElement;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import com.aerospike.client.AerospikeClient;
import com.aerospike.client.AerospikeException;
@Configuration
@EnableAutoConfiguration
@ComponentScan
public class AerospikeRESTfulService {
@Bean
public AerospikeClient asClient() throws AerospikeException {
Properties as = System.getProperties();
return new AerospikeClient(as.getProperty("seedHost"),
Integer.parseInt(as.getProperty("port")));
}
@Bean
public MultipartConfigElement multipartConfigElement() {
return new MultipartConfigElement("");
}
public static void main(String[] args) throws ParseException {
Options options = new Options();
options.addOption("h", "host", true,
"Server hostname (default: localhost)");
options.addOption("p", "port", true, "Server port (default: 3000)");
// parse the command line args
CommandLineParser parser = new PosixParser();
CommandLine cl = parser.parse(options, args, false);
// set properties
Properties as = System.getProperties();
String host = cl.getOptionValue("h", "localhost");
as.put("seedHost", host);
String portString = cl.getOptionValue("p", "3000");
as.put("port", portString);
// start app
SpringApplication.run(AerospikeRESTfulService.class, args);
}
}
L'annotation @EnableAutoConfiguration
a été ajoutée : elle permet un chargement de valeurs par défaut (comme le conteneur de servlet intégré) en fonction du contenu de votre classpath, et d'autres choses.
Il est également annoté avec @ComponentScan
, qui "dit" à Spring de scanner l'ensemble du package bonjour
pour les contrôleurs (avec toutes les autres classes de composants annotées).
Enfin, cette classe est annotée avec @Configuration
. Cela vous permet de configurer une instance de la classe AerospikeClient
comme un bean Spring.
Il y a aussi un bean MultipartConfigElement
qui est défini. Cela vous permet de traiter les opérations POST avec ce service.
La plupart du corps de la méthode main
lit simplement les arguments de ligne de commande et renseigne les propriétés du système pour spécifier l'adresse de l'hôte (seed host) et le port du cluster Aerospike.
Trop facile !
Charger les données
Vous allez vouloir charger des données vers ce service. Pour cela vous devrez ajouter une nouvelle méthode au contrôleur REST pour traiter le fichier téléchargé (uploaded). Dans cet exemple, ce sera un fichier CSV contenant la liste des vols.
src/main/java/com/aerospike/client/rest/RESTController.java
@Controller
public class RESTController {
. . . (code omitted) . . .
/*
* CSV flights file upload
*/
@RequestMapping(value="/uploadFlights", method=RequestMethod.GET)
public @ResponseBody String provideUploadInfo() {
return "You can upload a file by posting to this same URL.";
}
@RequestMapping(value="/uploadFlights", method=RequestMethod.POST)
public @ResponseBody String handleFileUpload(@RequestParam("name") String name,
@RequestParam("file") MultipartFile file){
if (!file.isEmpty()) {
try {
WritePolicy wp = new WritePolicy();
String line = "";
BufferedReader br = new BufferedReader(new InputStreamReader(file.getInputStream()));
while ((line = br.readLine()) != null) {
// use comma as separator
String[] flight = line.split(",");
/*
* write the record to Aerospike
* NOTE: Bin names must not exceed 14 characters
*/
client.put(wp,
new Key("test", "flights",flight[0].trim() ),
new Bin("YEAR", Integer.parseInt(flight[1].trim())),
new Bin("DAY_OF_MONTH", Integer.parseInt(flight[2].trim())),
new Bin("FL_DATE", flight[3].trim()),
new Bin("AIRLINE_ID", Integer.parseInt(flight[4].trim())),
new Bin("CARRIER", flight[5].trim()),
new Bin("FL_NUM", Integer.parseInt(flight[6].trim())),
new Bin("ORI_AIRPORT_ID", Integer.parseInt(flight[7].trim())),
new Bin("ORIGIN", flight[8].trim()),
new Bin("ORI_CITY_NAME", flight[9].trim()),
new Bin("ORI_STATE_ABR", flight[10].trim()),
new Bin("DEST", flight[11].trim()),
new Bin("DEST_CITY_NAME", flight[12].trim()),
new Bin("DEST_STATE_ABR", flight[13].trim()),
new Bin("DEP_TIME", Integer.parseInt(flight[14].trim())),
new Bin("ARR_TIME", Integer.parseInt(flight[15].trim())),
new Bin("ELAPSED_TIME", Integer.parseInt(flight[16].trim())),
new Bin("AIR_TIME", Integer.parseInt(flight[17].trim())),
new Bin("DISTANCE", Integer.parseInt(flight[18].trim()))
);
System.out.println("Flight [ID= " + flight[0]
+ " , year=" + flight[1]
+ " , DAY_OF_MONTH=" + flight[2]
+ " , FL_DATE=" + flight[3]
+ " , AIRLINE_ID=" + flight[4]
+ " , CARRIER=" + flight[5]
+ " , FL_NUM=" + flight[6]
+ " , ORIGIN_AIRPORT_ID=" + flight[7]
+ "]");
}
br.close();
return "You successfully uploaded " + name;
} catch (Exception e) {
return "You failed to upload " + name + " => " + e.getMessage();
}
} else {
return "You failed to upload " + name + " because the file was empty.";
}
}
}
Une nouvelle méthode handleFileUpload()
répond à la requête POST et lit le flux de téléchargement ligne par ligne. Chaque ligne est analysée et un objet Key
et plusieurs objets Bin
sont construits pour former un enregistrement (record) Aerospike. Enfin, la méthode aerospike put()
est appelée pour stocker l'enregistrement dans le cluster Aerospike.
Une autre nouvelle méthode provideUploadInfo()
répond à une requête GET et renvoie un message indiquant que des ajouts sont possibles.
Télécharger l'application cliente
Le chargement peut être fait comme vous le voulez. Mais vous pouvez utiliser la classe suivante pour charger les données vers le service.
src/test/java/com.aerospike.client.rest/FlightsUploader.java
package com.aerospike.client.rest;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.io.FileSystemResource;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
public class FilghtsUploader {
private static final String TEST_FILE = "flights_from.csv";
@Before
public void setUp() throws Exception {
}
@Test
public void upload() {
RestTemplate template = new RestTemplate();
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<String, Object>();
parts.add("name", TEST_FILE);
parts.add("file", new FileSystemResource(TEST_FILE));
String response = template.postForObject("http://localhost:8080/uploadFlights",
parts, String.class);
System.out.println(response);
}
}
Données de vols
Ce sont des données réelles depuis 2012. Elles représentent approximativement 1 million d'enregistrements, donc soyez conscients que cela prendra quelques minutes à charger.
Construire et exécuter le Service
Le fichier pom.xml
pour Maven va packager le service dans un unique fichier jar. Utilisez la commande suivante :
mvn clean package
Cela va générer une application autonome de web service dans un jar exécutable dans le sous-répertoire target
. Ce fichier jar contient une instance de Tomcat, donc vous pouvez simplement l'exécuter sans installer un serveur d'application :
java -jar aerospike-restful-example-1.0.0.jar
Pour conclure
Félicitations ! Vous venez de développer un service RESTful simple avec Spring et en le connectant à un cluster Aerospike.
Le code complet
Considérations sur la conception
Le contrôle d'accès est actuellement géré par l'application versus la base de données. Comme le processus d'authentification ralentit la vitesse de base de données, pratiquement toutes les bases NoSQL ne supportent pas cette fonction. La plupart de nos clients donnent la priorité à une vitesse accrue plutôt qu'à une fonction d'authentification intégrée.
Une autre caractéristique souvent demandée est une jointure de deux ensembles différents de données. C'est un défi à relever pour toutes les bases de données distribuées, car les données du join (jointure) sont distribuées. Dans ce cas, le développeur doit mettre en oeuvre une jointure dans la requête.
A propos de l'Auteur
Peter Milne est un professionnel expérimenté de l'IT avec une vaste expérience dans le développement de logiciel complet et les cycles de vie des produits. Il a des compétences techniques et une expérience de gestion avec des équipes de développement de toutes tailles. Peter était récemment Architecte de solutions senior chez Aerospike. Avant cela, il a été Analyste principal et Programmeur chez MLC, et a été Directeur technique chez iTerative Consulting pour construire un outil Java de conversion Forte/UDS avec 99,999% de précision. Peter est titulaire d'une maîtrise en informatique distribuée de l'Université de Technologie de Sydney, ainsi que de plusieurs certificats de sécurité pour hélicoptères.