Restangular : une architecture full-REST avec Restlet et AngularJS

Il y a un an, je vous proposais d'intégrer AngularJS et Spring MVC. L'excellent support du format JSON par ce dernier en faisait une technologie de choix pour exposer des ressources en REST/JSON, format favori d'AngularJS.

Un an plus tard, je découvre Restlet, un peu par hasard.
Je lis la documentation, mon intérêt s'éveille, je fais quelques tests... puis je repense à AngularJS.

A ma gauche, un framework spécifiquement conçu pour exposer des ressources en REST ; à ma droite, un framework qui consomme principalement des ressources REST... Aurais-je trouvé le couple parfait ? Vite, un prototype !

Dans cet article, je vous présente le résultat de ma petite expérience : une micro-application (la classique Todo-list) à l'architecture 100% RESTful, basée sur Restlet et AngularJS, et saupoudrée d'un peu de Bootstrap 3 pour le style graphique (voir capture d'écran ci-dessous).

Vous avez fait le plein de café et de tartines ? Alors c'est parti, suivez le guide !

list.png

Côté serveur

Configuration du backend

Pour planter le décor, voyons rapidement la partie backend, volontairement très simplifiée dans ce POC.

Le modèle métier se résume à une unique classe Todo, dont la structure est la suivante :

public class Todo {
    private Long id;
    private String title;
    private String description;
    private boolean done;
}

Les Todos sont gérés par un TodoRepository simpliste, qui les sauvegarde dans une Map en mémoire[1].

public final class TodoRepository {
 
    private static final TodoRepository INSTANCE = new TodoRepository();
    public static TodoRepository getInstance() {
        return INSTANCE;
    }
 
    private static final Map<Long, Todo> REPOSITORY = new ConcurrentSkipListMap<>();
    private static final AtomicLong IDS = new AtomicLong(0);
 
    private TodoRepository() {
    }
 
    public List<Todo> list() {
        return new ArrayList<>(REPOSITORY.values());
    }
 
    public Todo get(Long id) {
        return REPOSITORY.get(id);
    }
 
    public void create(Todo todo) {
        long id = IDS.getAndIncrement();
        todo.setId(id);
        REPOSITORY.put(id, todo);
    }
 
    public void update(Todo todo) {
        REPOSITORY.put(todo.getId(), todo);
    }
 
    public boolean delete(Long id) {
        return REPOSITORY.remove(id) != null;
    }
}

Exposition en REST

Entrons maintenant dans le vif du sujet : la mise en oeuvre de Restlet pour exposer les Todos en REST.

Restlet, un framework très complet

En abordant ce framework, j'ai été très surpris. Je m'attendais à une petite librairie de mapping, et je me suis retrouvé face à un gros framework, un environnement à part entière, capable de consommer et d'exposer des ressources REST (et bien plus encore), et déployable aussi bien de façon autonome que sur des environnements plus classiques.

Certains concepts de Restlet peuvent être déroutants au début ; je vais essayer d'en expliquer les bases, juste de quoi comprendre cet article.

Vue de loin, une application basée sur Restlet ressemble à une application Java EE classique. Chaque requête entrante est :

  • acceptée par un composant central (classe Application =~ serveur d'applications Java EE),
  • routée selon des règles de correpondance (classe Router =~ Front Controller),
  • éventuellement filtrée (classe Filter =~ filtre de servlet),
  • puis traitée par une unité d'exécution (classe Restlet =~ Servlet), chargée de produire une représentation particulière de la ressource (classe Representation et ses spécialisations, comme JacksonRepresentation).

Une fois l'Application configurée (routage, sécurité, logging...), elle peut être déployée à l'aide d'un Composant (classe Component).
Un Composant fait principalement office d'adaptateur entre une Application développée de manière portable, et un environnement d'exécution particulier (serveur autonome, serveur d'applications Java EE, plateforme Google AppEngine, Android...) - on pourrait comparer son rôle à celui de la JVM, qui offre aux programmes Java un environnement d'exécution uniforme quel que soit le système d'exploitation sous-jacent. Un Composant propose également bien d'autres services (Virtual Hosts...), mais nous n'en aurons pas besoin pour notre application.

Implémentation

Mais revenons justement à l'application !

Voici la façon dont j'ai choisi d'exposer les Todos :

  • /rest/todos (GET)  : liste des Todos
  • /rest/todos (POST)  : création d'un nouveau Todo
  • /rest/todos/{id} (GET)  : récupération d'un Todo
  • /rest/todos/{id} (PUT)  : modification d'un Todo
  • /rest/todos/{id} (DELETE) : suppression d'un Todo

Ce mapping fait apparaître deux ressources REST différentes : une ressource "liste de Todos" et une ressource "Todo". Leur traitement est donc confié à deux Restlets différentes : TodoListResource et TodoResource.

Maintenant que le plan d'action est fixé, le code de la classe RestangularApplication, qui étend Application, est trivial :

public class RestangularApplication extends Application {
 
    @Override
    public Restlet createInboundRoot() {
        Router router = new Router(getContext());
        router.attach("/rest/todos",          TodoListResource.class);
        router.attach("/rest/todos/{todoId}", TodoResource.class);
        return router;
    }
 
}

Les Restlets traitant les requêtes sont également très simples. La classe JacksonRepresentation, s'appuyant sur la librairie open-source Jackson, se charge de convertir nos objets métiers en leur représentation JSON, attendue par AngularJS ; elle permet également de parser le JSON envoyé par AngularJS lors de la création ou de la mise à jour d'une tâche.

public class TodoListResource extends ServerResource {
 
    private TodoRepository repository = TodoRepository.getInstance();
 
    @Get
    public Representation list() {
        return new JacksonRepresentation<>(repository.list());
    }
 
    @Post("json")
    public void create(Representation representation) throws IOException {
        JacksonRepresentation<Todo> jsonRepresentation = new JacksonRepresentation<Todo>(representation, Todo.class);
        Todo todo = jsonRepresentation.getObject();
        repository.create(todo);
    }
 
}
public class TodoResource extends ServerResource {
 
    private TodoRepository repository = TodoRepository.getInstance();
 
    private Long todoId;
 
    @Override
    protected void doInit() throws ResourceException {
        this.todoId = Long.valueOf(getAttribute("todoId"));
    }
 
    @Get
    public Representation get() {
        Todo todo = repository.get(todoId);
        if (todo == null) {
            throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND);
        }
        return new JacksonRepresentation<>(todo);
    }
 
    @Put("json")
    public void update(Representation representation) throws IOException {
        JacksonRepresentation<Todo> jsonRepresentation = new JacksonRepresentation<Todo>(representation, Todo.class);
        Todo todo = jsonRepresentation.getObject();
        repository.update(todo);
    }
 
    @Delete
    public void remove() {
        repository.delete(todoId);
    }
 
}

Si vous êtes attentif, vous aurez remarqué que les méthodes create(Representation) et update(Representation) étaient respectivement annotées @Post("json") et @Put("json"). La précision sur l'annotation permet d'activer le mécanisme de négociation de contenu, afin de s'assurer que les données transmises par la requête sont bien au format JSON.

Pour finir, il ne reste plus qu'à utiliser un Composant pour déployer l'application. Pour vous permettre de faire fonctionner le code plus facilement, et pour prouver que les serveurs d'applications Java EE ne sont pas une fatalité, j'ai opté pour un Composant embarquant un serveur HTTP autonome.

public class RestangularComponent extends Component {
 
    public static void main(String[] args) throws Exception {
        new RestangularComponent().start();
    }
 
    public RestangularComponent() {
        Server server = new Server(Protocol.HTTP, 8000);
        getServers().add(server);
 
        getDefaultHost().attachDefault(new RestangularApplication());
 
        System.out.println("Server started on port 8000.");
        System.out.println("Application is now available on http://localhost:8000/web/index.html");
    }
}

Côté serveur : terminé !

A ce point de l'article, la partie serveur est fonctionnelle.

Pour lancer le serveur, vous pouvez :

  • importer le code dans votre IDE et lancer la classe RestangularComponent ;
  • ou utiliser Maven pour construire un jar auto-exécutable, puis lancer ce jar :
mvn package
cd target
java -jar restangular-1.0.jar

Vous pouvez alors utiliser votre client REST favori (curl, le plugin RESTClient pour Firefox, ou le plugin RESTClient pour IntelliJ...) pour interagir avec le serveur.


Côté client

Bien, passons maintenant à la partie cliente de notre application.

Je vous ai déjà parlé d'AngularJS dans un précédent article, je ne m'attarderai donc pas sur les principes de ce chouette framework tout-en-un made in Google.

Présentation de l'interface

Côté interface, l'application se présente sous la forme de 4 écrans :

  • Liste des tâches; cet écran permet d'accéder aux autres, et offre la possibilité de supprimer et de compléter directement une tâche.
  • Ajout d'une tâche
  • Edition d'une tâche
  • Vue détaillée d'une tâche


list.png details.png edit.png


Les écrans d'ajout et d'édition possédant une interface similaire, le même template HTML est utilisé, mais avec des contrôleurs différents.

Implémentation

Routage

Commençons par définir à quelles URLS seront associés les écrans. Ou plutôt, quels fragments d'URL, puisqu'il s'agit d'une application de type "MVC côté client", contenue dans une unique page HTML dont l'apparence est ensuite modifiée dynamiquement. Les ancres HTML (la portion après le #) permettent d'identifier certains états prédéfinis de l'application.

J'ai choisi d'associer les fragments d'URL suivants aux différentes états (ou "pages") de l'application :

  • /list  : liste des tâches
  • /add  : création d'une nouvelle tâche
  • /{id}  : affichage de la tâche d'ID {id}
  • /{id}/edit : modification de la tâche d'ID {id}
  • Toute autre URL doit renvoyer vers la liste des tâches.

Cela se traduit directement par la configuration de routage suivante :

var app = angular.module('restangular.app', ['ngResource']);
 
app.config(function ($routeProvider, $locationProvider) {
    $routeProvider.when('/list',     {templateUrl: 'view/list.html',    controller: 'ListController'});
    $routeProvider.when('/add',      {templateUrl: 'view/add.html',     controller: 'AddController'});
    $routeProvider.when('/:id/edit', {templateUrl: 'view/add.html',     controller: 'EditController'});
    $routeProvider.when('/:id',      {templateUrl: 'view/display.html', controller: 'DisplayController'});
    $routeProvider.otherwise({redirectTo: '/list'});
    $locationProvider.hashPrefix('!'); // Enable ajax crawling
});

Vous remarquerez que la route /:id/edit est déclarée avant la route /:id. AngularJS teste les règles de routage dans l'ordre de leur déclaration ; il est donc nécessaire de placer les URLs les plus spécifiques en premier.

Interaction avec le serveur

Pour interagir avec la ressource REST exposée par notre serveur, il suffit de la déclarer auprès d'AngularJS, à l'aide du service $resource :

app.factory('Todo', ['$resource', function ($resource) {
    return $resource(
        '/rest/todos/:id', 
        { 'id': '@id'}, 
        {'update': {method: 'PUT'} })
    ;
}]);

Le code ci-dessus déclare que l'objet Javascript Todo est la représentation locale (proxy) d'une ressource REST exposée à l'URL /rest/todos/:id, le marqueur ":id" de l'URL correspondant à la valeur du champ id de l'objet.

Une Ressource AngularJS ainsi créée fournit quelques méthodes (save(), query(), get()...) couvrant les use-cases les plus courants, et effectuant des appels REST aux paramètres prédéfinis. Mais il est parfois nécessaire d'ajuster ces réglages afin de les faire correspondre parfaitement à la façon dont la ressource est exposée côté serveur.

Dans l'application, côté serveur, la création (POST) et la mise à jour (PUT) d'une tâche sont deux opérations distinctes, mais AngularJS ne fournit par défaut qu'une méthode save() effectuant une requête POST. Une méthode personnalisée update() est donc ajoutée pour combler ce manque.

Page de liste

Pour terminer, voyons comment la page listant les tâches est implémentée. C'est à mon avis la plus intéressante de l'application, car elle met en jeu plusieurs fonctionnalités standard d'AngularJS, ainsi que quelques astuces ergonomiques.

Détaillons les fonctionnalités de cette page :

  • Affichage de la liste des tâches.
  • Suppression directe d'une tâche.
  • Modification directe de l'état d'une tâche (à faire / terminée) à l'aide d'une case à cocher ; les tâches terminées sont grisées.
  • Affichage du nombre de tâches restantes.
  • Accès à la page de création d'une nouvelle tâche.
  • Accès à la page d'édition de chaque tâche.

Commençons par le contrôleur :

app.controller('ListController', ['$scope', 'Todo', '$location', function ($scope, Todo, $location) {
 
    // Modèle
    $scope.todos = Todo.query();
 
    $scope.deleteTodo = function (todo) {
        todo.$delete(function () {
            $location.path("/list");
        });
    };
 
    $scope.toggleTodo = function (todo) {
        todo.$update(function () {
            $location.path('/list');
        });
    };
 
    $scope.todosLeft = function () {
        return $scope.todos.filter(function (t) {
            return ! t.done;
        });
    };
 
}]);

Le contrôleur ListController déclare qu'il manipule des ressources de type Todo, et qu'il dépend du service $location permettant de manipuler les URLs et de rediriger l'utilisateur vers d'autres pages.

Il indique ensuite que la variable "todos" fait partie du modèle qu'il manipule, et que cette variable est initialisée par un appel à Todo.query(), qui effectue une requête de type GET sur l'URL racine (/rest/todos) pour récupérer la liste des tâches.

Le contrôleur définit ensuite deux fonctions deleteTodo() et toggleTodo(), qui utilisent les méthodes delete() et update() pour émettre respectivement des requêtes de type DELETE et PUT. L'utilisateur est ensuite redirigé vers la liste des tâches.

Pour finir, la fonction todosLeft() calcule le nombre de tâches encore en attente, afin de l'afficher à l'utilisateur.

Le code HTML associé est présenté ci-dessous (les classes CSS sont omises pour une meilleure lisibilité) :

[html]
<h2>
    <ng-pluralize count="todosLeft().length" 
                  when="{ '0':'Nothing left to do !', 
                          '1':'Only 1 thing left to do !', 
                          'other':'Still {} things to do !'}">
    </ng-pluralize>
</h2>

<table>
    <tr ng-repeat="todo in todos" ng-class="{active:todo.done}">
        <td>
            <input type="checkbox" ng-model="todo.done" ng-change="toggleTodo(todo);"/>
            <a ng-href="#!/{{todo.id}}">{{todo.title}}</a>
            <a ng-href="#!/{{todo.id}}/edit">Edit</a>
            <a href="#" ng-click="deleteTodo(todo);">Delete</a>
        </td>
    </tr>
</table>

<hr/>
<a href="#!/add">Add a new item</a>

Le code des autres écrans est trivial et ne sera pas détaillé ici.

Déploiement

La partie client n'est composée que de quelques ressources statiques : pages HTML, scripts Javascript, et ressources graphiques. Il serait donc très facile de la déployer sur un serveur léger de type Apache Httpd ou node.js, ou sur un serveur d'applications au sein d'une archive war.

Mais je vous propose ici une troisième solution : la déployer directement aux côtés des ressources REST !

Restlet fournit une classe Directory (un type particulier de Restlet), permettant de servir des ressources statiques à partir d'un répertoire ou d'un package. Il suffi de choisir l'URL à laquelle les ressources seront exposées, de la déclarer au niveau du routeur de requêtes, et le tour est joué !

Voici la classe RestangularApplication complétée :

public class RestangularApplication extends Application {
 
    @Override
    public Restlet createInboundRoot() {
        Directory directory = new Directory(getContext(), "clap://class/static/");
        directory.setDeeplyAccessible(true);
 
        Router router = new Router(getContext());
        router.attach("/web", directory);
        router.attach("/rest/todos", TodoListResource.class);
        router.attach("/rest/todos/{todoId}", TodoResource.class);
        return router;
    }
 
}

Un Directory peut servir des ressources à partir de plusieurs sources, définies par un protocole et un chemin de base. Ici, j'ai sélectionné le protocole CLAP (ClassLoader Access Protocol : "clap://"), qui permet d'accéder à toutes les ressources appartenant au classpath (ici, au répertoire /static contenant la partie cliente de l'application).

Quel que soit le protocole retenu, il ne faut pas oublier de l'activer au niveau du Composant :

public class RestangularComponent extends Component {
 
    public static void main(String[] args) throws Exception {
        new RestangularComponent().start();
    }
 
    public RestangularComponent() {
        Server server = new Server(Protocol.HTTP, 8000);
        getServers().add(server);
 
        getClients().add(Protocol.CLAP); // Activation du protocole CLAP
 
        getDefaultHost().attachDefault(new RestangularApplication());
 
        System.out.println("Server started on port 8000.");
        System.out.println("Application is now available on http://localhost:8000/web/index.html");
    }
}

Cette solution de déploiement est simple et élégante. Elle offre surtout l'immense l'avantage de pouvoir livrer une application auto-contenue, portable, et exécutable en quelques secondes. Pour une démonstration de produit, la première impression est souvent déterminante, et la facilité de lancement est un facteur important !

Côté client : terminé !

La partie client est maintenant terminée !

Vous pouvez recompiler et relancer le serveur, et accéder à l'URL http://localhost:8000/web/index/html pour jouer avec l'application.

Conclusion

La combinaison de Restlet, qui expose des ressources REST, et d'AngularJS, qui les consomme, semble très puissante. Ces deux frameworks sont naturellement complémentaires, et permettent de développer des applications riches et scalables avec un minimum de code.

Je débute avec Restlet, mais ses fonctionnalités riches (sécurité, clustering, logging...) et sa capacité à s'intégrer à n'importe quel environnement - compris une application Jva EE préexistante - en font à mon avis un framework majeur dans l'écosystème Java, et sur lequel il convient de garder un oeil attentif.

Mes projets professionnels actuels ne me permettent pas de le mettre en place, mais la combinaison Restlet + AngularJS prendra sûrement une place importante dans mes projets personnels !

Pour aller plus loin...

Vous pouvez retrouver tout le code de cet article en pièce jointe, ou sur GitHub :
https://github.com/OlivierCroisier/restangular.
N'hésitez pas à le forker et/ou y contribuer !

Un peu de lecture sur Restlet...

Et un peu de lecture sur AngularJS :

Et pour apprendre AngularJS en s'amusant : inscrivez-vous dès maintenant à ma formation AngularJS ! Entièrement développée par votre serviteur !

Note

[1] Petit détail d'implémentation : une ConcurrentSkipListMap a pour particularité de trier ses entrées selon l'ordre naturel de leurs clés (ici, les identifiants des Todos), ce qui nous permet de présenter les tâches toujours dans le même ordre. C'est en quelque sorte l'enfant caché d'une ConcurrentHashMap et d'une LinkedHashMap :)


Commentaires

1. Le lundi 12 août 2013, 08:58 par Adrien

Très bon article comme d'habitude. Une seule chose me chagrine c'est l'identifiant de la todo à récupérer. Il se trouve dans une méthode annoté de @init. Est-ce que cela veut dire que si dans la même classe j'ai plusieurs méthodes permettant de récupérer des éléments alors tous les paramètres, si différents, sont récupérés dans cette méthode pour être ajouté en tant que champ de la classe ? Cela peut rendre la classe difficile à lire et/ou debugguer, non?

Merci en tout cas.

2. Le lundi 12 août 2013, 09:56 par doanduyhai

Intéressante cette présentation. Pour mes pocs je faisais souvent du Spring MVC embarqué dans un Jetty côté serveur. Du coup avec Restlet on n'a plus besoin de server web. Par contre à titre purement d'information, qu'en est il des perf ? Est ce que Restlet est assez light et ne tire pas la terre entière en terme de dépendance maven ?

3. Le lundi 12 août 2013, 10:44 par Olivier Croisier

@Adrien : La méthode getAttribute() est accessible depuis toutes les méthodes, tu n'es pas obligé de récupérer toutes les variables dans la méthode doInit(). C'était le pattern utilisé dans le livre, je l'ai simplement réutilisé dans mon code. Mais en pratique, ça ne devrait pas être très gênant, il est rare d'avoir plus d'une ou deux variables dans l'URL.

@Doanduyhai : D'après Maven, le coeur de Restlet ne tire aucune dépendance. Si tu ajoutes le support de Jackson, de Velocity, l'intégration Java EE, etc., évidemment la liste s'allonge, mais on reste loin de la gourmandise d'un Spring par exemple. Si tu ne sers que des ressources statiques (en plus des ressources REST), Restlet semble donc une solution simple, légère et pratique.

Pour les perfs, je n'ai pas fait de tests.

4. Le mardi 13 août 2013, 09:33 par FabienM

Article bien détaillé.

Néanmoins j'ai un doute : entre JAX-RS est ses implémentations (à commencer par CXF et la RI) puis avec Spring-MVC et son @ResponseBody, le moins qu'on puisse dire c'est qu'on ne manque pas de solutions en Java pour exposer des services REST.

Je suppose que le footprint de Restlet est plus léger qu'une stack Spring dans le cas d'une application simple, mais ça me semble être une plus value un peu juste. Y'a-t-il d'autres avantages à l'emploi de Restlet, notamment par rapport à JAX-RS ?

5. Le mardi 13 août 2013, 11:07 par Olivier Croisier

Effectivement, on ne manque pas de solutions. Mais Restlet a quelques atouts dans sa manche.

L'application montrée ici est simplissime, et ne tire pas partie des fonctionnaliltés avancées de Restlet, comme :

  • Le fait qu'on peut construire un pipeline personnalisé (et potentiellement différent) entre le Router et chaque Restlet terminale (qui traite une requête), par exemple pour ajouter des filtres de sécurité, de logging, etc. à granularité très fine, et réutilisables (pattern Decorator).
  • Le fait qu'une Application peut être réutilisée telle quelle sur différents environnements - et j'ai montré ici que le fait de proposer un serveur embarqué léger fournissait une alternative intéressante au traditionnel couple Jetty + war, qui tire un bon paquet de dépendances.
  • Le fait que Restlet pousse à concevoir son SI comme un ensemble de ressources exposées et consommées. Une Application peut gérer les Users, une autre tout ce qui est financier, une troisième tout ce qui touche aux produits vendus... Les Applications peuvent alors être déployées sur différents Composants, ou sur le même (dans ce cas, elles peuvent communiquer directement entre elles sans repasser par toute la couche réseau).
  • Le fait qu'une Application peut être intégrée dans une application Java EE classique sur un serveur d'applications. On peut donc parfaitement faire cohabiter Spring MVC pour la partie présentation utilisateur, et Restlet pour la partie exposition des ressources en REST.
  • Le fait que Restlet peut également consommer des ressources REST. Le jar basique est petit et sans dépendances, et offre déjà beaucoup de services, dont le mapping Representation - Objet.

Pour finir, mais c'est totalement subjectif, mon pifomètre personnel a réagi positivement à cette technologie. Comme pour AngularJS bien avant sa présentation à la JavaOne, et comme pour Spring 1.1 il y a... longtemps :) Je peux me tromper, mais disons que Restlet me paraît sain et utile.

6. Le mardi 13 août 2013, 11:30 par Frédéric Camblor

Je me suis timeboxé à 1h pour refactorer la partie serverside en restx, visible ici pour les curieux : https://github.com/fcamblor/restang...

Ce qui m'a le plus surpris durant ce refactoring, c'est l'utilisation systématique des Representation. J'ai même cru au début que c'était quelque chose d'obligatoire dans restlet, et puis il y a eu la discussion sur twitter avec fabien (https://twitter.com/fcamblor/status...) qui m'a rassuré sur ce point.

En terme de temps de démarrage, restx est perdant : 600ms vs 27ms pour restlet ... mais ça reste acceptable :-)
Il faudrait bencher les temps que prennent une requête en moyenne.

Par contre en terme de design, j'avoue avoir quelques préférences envers restx (bon ok, je ne suis pas forcément la personne la plus objective sur le sujet ;-)) :
- Très peu de réflexion (la seule réflexion utilisée est celle de Jackson pour les entrées/sorties) : aussi bien l'injection/résolution de dépendances que la déclaration des routes/paramètres se fait via de la génération de code basé sur de l'annotation processing (meilleures perfs, debugging plus aisé dans l'IDE à l'aide des "call hierarchy")
- Pas besoin d'hériter de quoique ce soit pour que ça marche (dans tes exemples, souvent, tu as besoin d'hériter d'une classe : que ce soit sur l'Application ou sur les Resources)
- Le fait de déclarer les routes sur chaque méthode et non de manière centralisée (comme on le fait avec spring mvc). Là, c'est plus du subjectif car il y a des pros & cons dans les 2 philosophies.
- L'utilisation des Optional Guava en entrée/sortie pour définir les beans optionnels
- La validation des beans via bean validation
- La compilation à la volée (à la Play!), fonctionnalité à venir dans la 0.2.9 :-)

J'aurais aimé mettre en place davantage de choses (tests basés sur des fichiers de specs, bean validation, cache et sécurité), mais en 1h, c'était compliqué :)

7. Le samedi 21 septembre 2013, 10:30 par Thomas Pons

En parlant de RestAngular : https://github.com/mgonto/restangul...

C'est pas mal fait et ça utilise des promises :)

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.