Dropwizard : REST services made easy

Encore un framework web Java ?

Dropwizard, c’est quoi ? Et bien c’est un framework Java pour réaliser des webservices REST développé par Coda Hale, un des architectes de Yammer. Jusque là rien de neuf… enfin pas tout à fait. Il existe en effet pléthore de bibliothèques pour réaliser des webservices REST : CXF, Jersey, Axis, …  Mais de vrais frameworks qui viennent à la fois structurer le code et offrir un certain nombre de services plus évolués, voila qui réduit considérablement le spectre des possibles.

Le but de Dropwizard est d’offrir “out of the box” des webservices prêts à être mis en production sans avoir besoin d’ajouter d’autres bibliothèques ou frameworks. Ainsi nous sont offerts un système de configuration, un monitoring de l’application, une gestion des logs, … et tout cela sans avoir besoin d’un conteneur de servlets, et encore moins d’un serveur d’applications. Vous avez dit léger ?

Pour réaliser cela, Dropwizard vient agglomérer un certain nombre de bibliothèques Java éprouvées, et fournit le “glue code” nécessaire pour que tout ce beau monde fonctionne correctement. Il y a donc sous le capot et par ordre d’apparition :

  • Jetty pour le serveur HTTP embarqué
  • Jersey pour la partie webservice REST
  • Jakson pour la dé/sérialisation du JSON
  • Metrics pour le monitoring de l’application
  • Mais aussi slf4j/logback, Guava, Joda Time, Liquibase, Freemarker, Hibernate validator, HttpClient, JDBI,…

 Un peu de magie dans la todo list !

Assez parlé, rentrons dans le code et pour cela quoi de mieux qu’une très originale application de gestion de Todos, bien évidemment hébergé sur Github.

Nous allons utiliser Maven pour la construction du projet et la gestion des dépendances. Nous utiliserons aussi la structure de packages encouragée  par les créateurs du framework :

|- Service.java
|- Configuration.java
|- api (définition de l'api)
|- core (logique métier)
|- jdbi (DAO)
|- resources (WS REST)

Commençons par définir nos entités métiers, au nombre de deux car nous allons garder les choses simples :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Todo {

private final long id;
private final String title;
private final Optional<String> content;

private Todo(long id, String title, Optional<String> content) {
this.id = id;
this.title = title;
this.content = content;
}

// + getters, equals, hashcode, builder

}
1
2
3
4
5
6
7
8
9
10
11
public class User {

private final String username;

private User(String username) {
this.username = username;
}

// + getters, equals, hashcode, builder

}

Dropwizard ayant une structure modulaire, nous allons introduire les modules un a un pour voir ce qu’offre chacun d’entre eux.

Dropwizard-core

Le module core est comme son nom l’indique le module principal de Dropwizard qui inclut tout ce qui est nécessaire pour réaliser des webservices REST.

Commençons par importer la dépendance dans notre pom.xml :

1
2
3
4
5
6
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-core</artifactId>
<version>${dropwizard.version}</version>
<scope>compile</scope>
</dependency>

Ensuite chaque application Dropwizard doit définir une configuration. Celle ci sera fournie par un fichier YAML qui sera désérialisé à l’exécution. Cette configuration pourra de plus être validée grâce à Hibernate validation.

1
2
// Doit étendre Configuration
public class TodoConfiguration extends Configuration {}

Pour l’instant notre configuration est vide mais nous viendrons la compléter au fur et à mesure.

Nous pouvons maintenant définir la classe qui représente notre service Dropwizard, c’est dans celle-ci que nous allons pouvoir faire la tuyauterie de notre application et c’est elle qui contiendra notre main.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Doit étendre Service et etre paramétrée avec notre Configuration
public class TodoService extends Service<TodoConfiguration> {

public static void main(String[] args) throws Exception {
new TodoService().run(args);
}

// Permet d'initialiser notre service
@Override
public void initialize(Bootstrap<TodoConfiguration> bootstrap) {
bootstrap.setName("todo");
}

// Permet d'ajouter des ressources (i.e. des webservices), des fournisseurs de services (authentification, accès à la base de donnée) et de réaliser la tuyauterie
@Override
public void run(TodoConfiguration configuration, Environment environment) throws Exception {}

}

Il est maintenant temps de définir notre API. Pour cela nous allons modéliser les objets JSON que nous allons échanger par des classes Java que nous allons dé/sérialiser avec Jackson et valider avec Hibernate validation. Voici donc la classe qui représente un objet JSON représentant un Todo :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ExternalTodo {

@NotNull // Impose que la propriété de soit pas null
@JsonProperty // Indique que la propriété est a prendre en compte lors de la sérialisation en JSON
private final long id;
@NotNull
@JsonProperty
private final String title;
@JsonProperty
private final String content;
@NotNull
@JsonProperty
private final String selfUrl;

@JsonCreator // Permet d'indiquer que ce constructeur doit être utilisé pour la désérialisation d'un objet JSON
public ExternalTodo(@JsonProperty("id") long id, @JsonProperty("title") String title, @JsonProperty("content") String content, @JsonProperty("selfUrl") String selfUrl) {
this.id = id;
this.title = title;
this.content = content;
this.selfUrl = selfUrl;
}

// + getters, equals, hashcode, builder

}

Maintenant que nous avons défini ce que nous allons échanger, nous pouvons définir notre classe qui représente un webservice, appelé ici ressource. Cette classe est un simple POJO, annoté avec les annotations JAX-RS. Nous allons donc proposer les opérations de type CRUD pour nos chers Todos :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Path("/todos") // Path du service
@Produces(MediaType.APPLICATION_JSON) // Type de média produit
@Consumes(MediaType.APPLICATION_JSON) // Type de média accepté
public class TodoResource {

private final TodoDAO todoDAO;

public TodoResource(TodoDAO todoDAO) {
this.todoDAO = todoDAO;
}

@Timed // Permet d'instrumenter la méthode avec Metrics (moyenne, min, max,... du temps d'exécution, nombre d'exécutions,...)
@POST // Méthode HTTP
// @Valid Demande la validation du POJO passé en paramètre
public ExternalTodoLight createTodo(@Context UriInfo uriInfo, @Valid TodoCreationRequest todoCreationRequest) {
Long createdTodoId = this.todoDAO.createTodo(new TodoCreationRequest.Mapper().toTodo(todoCreationRequest));
return new ExternalTodoLight.Mapper().fromId(uriInfo.getBaseUri(),createdTodoId);
}

@Timed
@Path("{id}") // Path de la méthode
@GET
public ExternalTodo getTodo(@Context UriInfo uriInfo, @PathParam("id") LongParam id) {
Optional<Todo> todo = this.todoDAO.getTodo(id.get());
if (todo.isPresent()) {
return new ExternalTodo.Mapper().fromTodo(uriInfo.getBaseUri(),todo.get());
} else {
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
}

// + getTodos, update

}

Notre ressource est prête, il ne reste plus qu’à l’enregistrer dans notre service :

1
2
3
4
5
@Override
public void run(TodoConfiguration configuration, Environment environment) throws Exception {
final TodoDAO todoDAO = // Implémentation à venir
environment.addResource(new TodoResource(todoDAO));
}

A partir de maintenant nous avons un service fonctionnel (mise à part notre DAO), il ne nous reste plus qu’à le packager et le lancer. Pour le packaging il est conseillé de faire un fat jar (jar de l’application avec ses dépendances) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.0</version>
<configuration>
<createDependencyReducedPom>true</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>fr.blemale.dropwizard.todo.TodoService</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>

Un ‘mvn clean install’ plus tard nous avons notre jar bien joufflu d’une petite dizaine de Mo. Et après un ‘java -jar target/dropwizard-todo-1.0-SNAPSHOT.jar server todo.yml’ notre service tourne. On peut alors interagir avec notre application grâce à une application comme curl et faire des requêtes http sur le port 8080. Le port 8081 permet d’accéder aux données d’administration comme les métriques remontées par Metrics ou bien le monitoring des threads.

Maintenant que nous avons vu comment réaliser un webservices basique ajoutons quelques fonctionnalités sympathiques en ajoutant des modules.

Dropwizard-auth

Comme son nom l’indique, ce module permet d’ajouter une couche d’authentification, celle-ci reste néanmoins basique. Ajoutant le module en dépendance de notre projet :

1
2
3
4
5
6
<dependency>
<groupId>com.yammer.dropwizard</groupId>
<artifactId>dropwizard-auth</artifactId>
<version>${dropwizard.version}</version>
<scope>compile</scope>
</dependency>

L’authentification se met en place facilement, et en trois temps. Il faut d’abord créer la classe contenant la logique de l’authentification, en voici une très basique :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Doit étendre Authenticator
public class DummyAuthenticator implements Authenticator<BasicCredentials, User> {
private final String password;

public DummyAuthenticator(String password) {
this.password = password;
}

// Retourne un Optional contenant un User si l'autentification a réussi, ou rien dans le cas contraire
@Override
public Optional<User> authenticate(BasicCredentials credentials) throws AuthenticationException {
if (password.equals(credentials.getPassword()) && !Strings.isNullOrEmpty(credentials.getUsername())) {
return Optional.of(User.Builder.anUser(credentials.getUsername()).build());
}
return Optional.absent();
}
}

Il faut maintenant l’enregistrer dans notre Service, et ajouter la propriété password à notre configuration :

1
2
3
4
5
6
@Override
public void run(TodoConfiguration configuration, Environment environment) throws Exception {
final String password = configuration.getPassword();
environment.addProvider(new BasicAuthProvider&lt;User&gt;(new DummyAuthenticator(password), "Protect Area"));
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class TodoConfiguration extends Configuration {
@NotBlank // Ne doit être ni null, ni vide, ni composée uniquement d'espace
@JsonProperty
private final String password;

public TodoConfiguration(@JsonProperty("password") String password) {
this.password = password;
}

public String getPassword() {
return password;
}
}

On peut maintenant protéger nos ressources :

1
2
3
4
5
6
7
8
public class TodoResource {
// ...

// L'annotation @Auth permet d'activer l'authentification, et de récupérer l'utilisateur authentifié ou d'envoyer une réponse appropriée (401 Unauthorized)
public ExternalTodoLight createTodo(@Context UriInfo uriInfo, @Auth User user, @Valid TodoCreationRequest todoCreationRequest) {
// ...
}
}

Nous venons donc de mettre en place une authentification http basic très simple. Il faut savoir que ce module offre aussi un support de OAuth.

Dropwizard-jdbi

Ce module offre le support de jdbi. Mais qu’est ce donc ? Jdbi est une surcouche à jdbc, qui a pour but de rendre les choses un peu moins douloureuses, tout en gardant un contrôle total, ce n’est donc pas un ORM. Voyons donc à quoi cela ressemble t’il en réalisant notre Dao :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RegisterMapper(TodoMapper.class) // Enregistrement d'un Mapper pour récupérer directement des Todos à partir du result set
public interface JDBITodoDAO extends TodoDAO {
@Override
@GetGeneratedKeys // Permet de récupérer la ou les clés des lignes mis à jours
@SqlUpdate("INSERT INTO todo (title, content) VALUES (:title, :content)") // Instruction SQL d'update
Long createTodo(@BindBean Todo todo); // @BindBean permet d'injecter les propriétés du bean dans les paramètres de la requête

@Override
@SqlUpdate("UPDATE todo SET title = :title, content = :content WHERE id = :id")
int updateTodo(@BindBean Todo todo);

@Override
@SqlQuery("SELECT id, title, content FROM todo")
ImmutableList&lt;Todo&gt; getTodos(); // Support des ImmutableList et Set de Guava

@Override
@SingleValueResult(Todo.class) // Permet de récupérer un seul résultat
@SqlQuery("SELECT id, title, content FROM todo WHERE id = :id")
Optional&lt;Todo&gt; getTodo(@Bind("id") long id); // Support des Optionals des Guava
}

Et voici le TodoMapper :

1
2
3
4
5
6
7
public class TodoMapper implements ResultSetMapper&lt;Todo&gt; {

@Override
public Todo map(int i, ResultSet resultSet, StatementContext statementContext) throws SQLException {
return Todo.Builder.aTodo(resultSet.getLong("id"), resultSet.getString("title")).withContent(Optional.fromNullable(resultSet.getString("content"))).build();
}
}

On peut remarquer que notre Dao est une interface et qu’il ne peut donc pas être utilisé en l’état. Pour ce faire nous allons d’abord avoir besoin de la configuration de notre base de données :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TodoConfiguration extends Configuration {
// ...

@Valid // Demande la validation de la databaseConfiguration
@NotNull
@JsonProperty("database")
private final DatabaseConfiguration databaseConfiguration;

public TodoConfiguration(@JsonProperty("password") String password, @JsonProperty("database") DatabaseConfiguration databaseConfiguration) {
this.password = password;
this.databaseConfiguration = databaseConfiguration;
}

// ...
}

Ceci étant fait nous pouvons maintenant instancier notre Dao et l’injecter dans notre ressource :

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TodoService extends Service&lt;TodoConfiguration&gt; {
//...

@Override
public void run(TodoConfiguration configuration, Environment environment) throws Exception {
//...

final DBIFactory factory = new DBIFactory();
final DBI jdbi = factory.build(environment, configuration.getDatabaseConfiguration(), "postgresql");
final TodoDAO todoDAO = jdbi.onDemand(JDBITodoDAO.class);
environment.addResource(new TodoResource(todoDAO));
}
}

Pour plus de détail sur jdbi je vous invite à lire la documentation qui n’est pas très épaisse.

Dropwizard-testing

Dropwizard a aussi mis l’accent sur la testabilité et offre un certain nombre de fonctionnalités pour facilité les tests.

Dropwizard encourage à tester la dé/sérialisation des classes représentant les objets JSON échangés. Pour cela rien de plus simple, on vient mettre le JSON attendu dans les ressources de test et on utilise les helpers mis à disposition :

1
2
3
4
5
6
7
8
9
10
public class ExternalTodoTest {
@Test
public void producesTheExpectedJson() throws Exception {
ExternalTodo externalTodo = new ExternalTodo(1L, "title", "content", "selfUrl");

assertThat("rendering a externalTodo as JSON produces a valid API representation",
asJson(externalTodo),
is(jsonFixture("fixtures/externalTodo.json")));
}
}

Ce module permet aussi de réaliser des tests “full-stack” en lançant un service donné dans un Jetty en mémoire.

Les autres modules

Dropwizard a encore un certain nombre de modules qui ne sont pas utilisés dans l’application, mais dont voilà un rapide apperçu :

  • Migration : Support de liquibase pour le versionnement du schéma de base de données.
  • Hibernate : Support de Hibernate pour la persistance
  • Views : Templating html avec FreeMarker ou Mustache
  • Scala : Sucre syntaxique pour l’utilisation de Dropwizard en Scala

Conclusion

Les webservices REST sont entrain de devenir le standard de facto pour la communication entre un back et un front, que ce soit une application mobile ou une single page application en JavaScript. Ainsi Dropwizard vient répondre à ce besoin en offrant un cadre de développement solide pour réaliser rapidement de tels websevices. On est pas ici en présence d’un énième framework MVC, mais bien d’un framework entièrement axé sur la réalisation d’APIs REST.

VN:R_U [1.9.22_1171]
Rating: 0 (from 0 votes)
Share
Ce contenu a été publié dans Java, avec comme mot(s)-clef(s) , , , . Vous pouvez le mettre en favoris avec ce permalien.

Laisser un commentaire