logo le blog invivoo blanc

Développement RAD avec Spring Boot – Partie 2

7 avril 2016 | DevOps, Java

L’objectif de cette démonstration consiste à vous montrer la rapidité avec laquelle le métier de votre application peut être exposé de manière sécurisée à travers une interface web RESTfull.

Voici les fonctionnalités offertes par notre application :

  • Consultation / ajout / suppression d’actions.
  • Consultation / ajout / suppression de cotation d’actions.

Notre application sera basée sur les technologies suivantes :

  • Spring Data Jpa pour la persistence de nos entités métiers.
  • Spring WebMCV pour exposer nos entités métiers.
  • Spring Security pour gérer l’accès à ces ressources.
  • HSQLDB comme base de données mémoire embarquée.

La démonstration fera usage d’AngularJS au sein des pages HTML fournies par le service pour démontrer les intéractions RESTfull avec l’api que nous allons construire. Les explications concernant la partie cliente seront brèves puisque le workshop n’a pas pour but de présenter AngularJS.

Configuration du projet

Déclaration du starter parent Spring Boot dans notre pom.xml

<!-- Inherit defaults from Spring Boot -->
org.springframework.boot
spring-boot-starter-parent
1.2.5.RELEASE

Notre projet nécessite donc les modules Spring Boot suivants :

  • spring-boot-starter-data-jpa
  • spring-boot-starter-web
  • spring-boot-security

En complément, nous utiliserons une base de données HSQLDB embarquée en mémoire. Elle facilitera notre développement

Il n’y a qu’à les déclarer en dépendances au sein du pom.xml


org.springframework.boot
spring-boot-starter-data-jpa

org.springframework.boot
spring-boot-starter-web

org.springframework.boot
spring-boot-starter-security

org.hsqldb
hsqldb

Création de nos entités métiers

Stock.java

package com.invivoo.springboot.securedrestapi.entity;

import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;

import com.fasterxml.jackson.annotation.JsonManagedReference;

@Entity
public class Stock {
private long id;
private String name;
private String isin;
private List stockQuotes;

public Stock() {

}

public Stock(long id, String name, String isin, List stockQuotes) {
this.id = id;
this.name = name;
this.isin = isin;
this.stockQuotes = stockQuotes;
}

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public long getId() {
return id;
}

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

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getIsin() {
return isin;
}

public void setIsin(String isin) {
this.isin = isin;
}

@JsonManagedReference
@OneToMany(cascade = CascadeType.ALL, mappedBy="stock")
public List getStockQuotes() {
return stockQuotes;
}

public void setStockQuotes(List stockQuotes) {
this.stockQuotes = stockQuotes;
}
}

StockQuote.java

ppackage com.invivoo.springboot.securedrestapi.entity;

import java.util.Date;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;

import com.fasterxml.jackson.annotation.JsonBackReference;

@Entity
public class StockQuote {
private long id;
private Stock stock;
private Double quote;
private Date timestamp;

public StockQuote() {

}

public StockQuote(long id, Stock stock, Double quote, Date timestamp) {
this.id = id;
this.stock = stock;
this.quote = quote;
this.timestamp = timestamp;
}

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public long getId() {
return id;
}

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

@ManyToOne
@JsonBackReference
public Stock getStock() {
return stock;
}

public void setStock(Stock stock) {
this.stock = stock;
}

public Double getQuote() {
return quote;
}

public void setQuote(Double quote) {
this.quote = quote;
}

public Date getTimestamp() {
return timestamp;
}

public void setTimestamp(Date timestamp) {
this.timestamp = timestamp;
}
}

Implémentation de la persistance des entités.

Nous utiliserons Spring Data JPA pour gérer la persistance de ces entités. Ce workshop n’a pas pour but d’expliquer le fonctionnement de Spring Data JPA mais en voici la principale fonctionnalité. Spring Data JPA s’occupe d’implémenter automatiquement les Data Access Object (DAO) des entités JPA de notre projet. Ainsi, l’outil nous fournit les implémentations nécessaires pour réaliser des opérations CRUD sur nos entités sans avoir à coder quoi que ce soit. Pour ce faire, nous devons simplement déclarer une interface qui hérite de JPARepository typé avec notre entité JPA.

Déclaration de notre DAO pour gérer notre entité Stock :

package com.invivoo.springboot.securedrestapi.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.invivoo.springboot.securedrestapi.entity.Stock;

public interface StockRepository extends JpaRepository<Stock, Long> {

}

Spring Data JPA, qui sera initialisé par Spring Boot, s’occupera de scanner notre package com.invivoo.springboot.securedrestapi pour y rechercher des interfaces héritant de JPARepository. Le framework construira ensuite l’implémentation de l’interface nécessaire pour réaliser les opérations CRUD basique. Cette implémentation sera donc disponible à notre application pour une injection par le conteneur Spring.

Dans le cadre de notre exemple, nous n’avons pas besoin de DAO pour StockQuote puisque cette dernière a été configurée pour être cascadée lors de la sauvegarde de notre entité Stock.

...

@OneToMany(cascade = CascadeType.ALL, mappedBy="stock")
public List getStockQuotes() {
return stockQuotes;
}
...

Implémentation de la couche Service

La couche service sert habituellement à la manipulation métier. Toute manipulation ou validation fonctionnelle doit normalement y être implémentée. Cette couche correspond au lien entre la couche contrôleur et celle de la persistance.

StockService.java

package com.invivoo.springboot.securedrestapi.service;

import java.util.List;

import com.invivoo.springboot.securedrestapi.entity.Stock;

public interface StockService {
List findAll();

Stock findOne(long id);

Stock save(Stock stock);
}

StockServiceImpl.java

package com.invivoo.springboot.securedrestapi.service.impl;

import java.util.List;

import javax.annotation.Resource;

import org.springframework.stereotype.Service;

import com.invivoo.springboot.securedrestapi.entity.Stock;
import com.invivoo.springboot.securedrestapi.repository.StockRepository;
import com.invivoo.springboot.securedrestapi.service.StockService;

@Service
public class StockServiceImpl implements StockService {
@Resource
private StockRepository stockRepository;

@Override
public List findAll() {
return stockRepository.findAll();
}

@Override
public Stock findOne(long id) {
return stockRepository.findOne(id);
}

@Override
public Stock save(Stock stock) {
return stockRepository.save(stock);
}
}

Implémentation du controleur Spring MVC pour l’entité Stock

La classe StockEndpoint servira à déclarer les points d’accès de notre Web Service REST. Cette implémentation est très standard aux fonctionnalités classiques offertes par Spring Web MVC.

StockController.java

package com.invivoo.springboot.securedrestapi.endpoint;

import java.util.List;

import javax.annotation.Resource;

import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.invivoo.springboot.securedrestapi.entity.Stock;
import com.invivoo.springboot.securedrestapi.service.StockService;

@Controller
@RequestMapping(value = { "/", "/stock" }, produces = MediaType.APPLICATION_JSON_VALUE)
public class StockEndpoint {
@Resource
private StockService stockService;

/**
* Service qui retourne au format JSON l'intégralité des actions présentes
* en base.
*
* Url : GET /stock
*
* @return
*/
@RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public @ResponseBody List findAll() {
return stockService.findAll();
}

/**
* Fournit la redirection vers la page HTML
* src/main/resources/resources/stocks.html.
*
* Url : GET /stock
*
* @return
*/
@RequestMapping(method = RequestMethod.GET, produces = MediaType.TEXT_HTML_VALUE)
public String getStocksPage() {
return "stocks.html";
}

/**
* Service qui retourne au format JSON l'action en fonction de l'identifiant
* fourni.
*
* Url : GET /stock/1
*
* @param id
* @return
*/
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public @ResponseBody Stock findOne(@PathVariable long id) {
return stockService.findOne(id);
}

/**
* Service de sauvegarde de l'action passé dans le corps de la requête HTTP.
* Retourne le résultat de la sauvegarde.
*
* Url : POST /stock
*
* @param stock
* @return
*/
@RequestMapping(method = RequestMethod.POST)
public @ResponseBody Stock save(@RequestBody Stock stock) {
return stockService.save(stock);
}
}

Fournir un petit jeu de données

Spring Boot s’occupe de configurer automatiquement la base de données embarquée HSQLDB. Pour l’instant, nous fournirons un fichier data.sql dans le classpath ( ex : src/main/resources) pour initialiser des données de tests. Le paramétrage par défaut d’un DataSource par Spring Boot s’occupera d’exécuter quelconque SQL présent dans ce fichier.

data.sql

INSERT INTO STOCK (ID, NAME, ISIN) VALUES (1, 'ACCOR', 'FR0000120404');
INSERT INTO STOCK (ID, NAME, ISIN) VALUES (2, 'AIR LIQUIDE', 'FR0000120073');
INSERT INTO STOCK (ID, NAME, ISIN) VALUES (3, 'ALSTOM', 'FR0010220475');

INSERT INTO STOCK_QUOTE (ID, STOCK_ID, QUOTE, TIMESTAMP) VALUES (1, 1, 100.00, TIMESTAMP ( '2016-03-16'));
INSERT INTO STOCK_QUOTE (ID, STOCK_ID, QUOTE, TIMESTAMP) VALUES (2, 1, 102.00, TIMESTAMP ( '2016-03-17'));
INSERT INTO STOCK_QUOTE (ID, STOCK_ID, QUOTE, TIMESTAMP) VALUES (3, 1, 104.00, TIMESTAMP ( '2016-03-18'));

INSERT INTO STOCK_QUOTE (ID, STOCK_ID, QUOTE, TIMESTAMP) VALUES (4, 2, 50.00, TIMESTAMP ( '2016-03-16'));
INSERT INTO STOCK_QUOTE (ID, STOCK_ID, QUOTE, TIMESTAMP) VALUES (5, 2, 49.00, TIMESTAMP ( '2016-03-17'));
INSERT INTO STOCK_QUOTE (ID, STOCK_ID, QUOTE, TIMESTAMP) VALUES (6, 2, 41.00, TIMESTAMP ( '2016-03-18'));

INSERT INTO STOCK_QUOTE (ID, STOCK_ID, QUOTE, TIMESTAMP) VALUES (7, 3, 13.00, TIMESTAMP ( '2016-03-16'));
INSERT INTO STOCK_QUOTE (ID, STOCK_ID, QUOTE, TIMESTAMP) VALUES (8, 3, 15.00, TIMESTAMP ( '2016-03-17'));
INSERT INTO STOCK_QUOTE (ID, STOCK_ID, QUOTE, TIMESTAMP) VALUES (9, 3, 14.00, TIMESTAMP ( '2016-03-18'));

Spring Boot initialisera une base de données vide, générera le schéma à l’aide HBM2DDL d’Hibernate puis exécutera data.sql afin d’importer des données pour vos développement. Il faut toujours garder à l’esprit que cette fonctionnalité est configurable et désactivable à travers les options Spring Boot que nous verrons plus tard.

Implémentation de notre classe d’amorçage Spring Boot

SecuredRestApi.java

package com.invivoo.springboot.securedrestapi;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SecuredRestApi {
public static void main(String[] args) {
SpringApplication.run(SecuredRestApi.class, args);
}
}

Client du webservice.

Le client du web service est un client AngularJS. J’ai construit une archive avec tous les fichiers nécessaires, qu’il suffit de déposer dans le répertoire src/main/resources/resources (voir soures github).

Démarrage de l’application

Comme vu à la partie 1, vous n’avez qu’à lancer votre classe maincom.invivoo.springboot.securedrestapi.SecuredRestApi. Spring boot s’occupera d’initialiser votre application et tous les beans auto configurables fournis par les modules starter que vous avez déclarés dans le pom.xml. La base de données sera créée en mémoire et initialisée. spring-boot-starter-securitysécurisera par défaut toutes les ressources exposées à travers http://localhost/*. Il est donc important de noter le mot de passe aléatoire généré par Spring Boot. Il sera visible dans vos logs de démarrage.

« `
2016-03-19 12:39:10,085 [localhost-startStop-1] INFO o.s.b.a.s.AuthenticationManagerConfiguration –

Using default security password: 6ef3b623-0cb4-400b-853d-af7fbc8d93a9
«

Connexion à l’application

Vous pourrez par la suite accéder à l’application depuis http://localhost:8080/. La configuration par défaut vous permettra de vous connecter avec le compte user. Le mot de passe étant celui que vous retrouverez dans les logs de démarrage.

Et ensuite ?

Spring Boot est magique. En quelques déclarations nous sommes lancés avec une application fonctionnelle. Malheureusement (ou heureusement), la magie n’est pas une caractéristique qui est recherchée en ingénierie logicielle. C’est pourquoi j’expliquerai prochainement le fonctionnement du mécanisme d’auto configuration de Spring qui donne vie à nos applications.