Premiers pas avec les microservices : un didacticiel Dropwizard

Publié: 2022-03-11

Nous assistons tous à une augmentation de la popularité des architectures de microservices. Dans une architecture microservice, Dropwizard occupe une place très importante. Il s'agit d'un cadre pour la construction de services Web RESTful ou, plus précisément, d'un ensemble d'outils et de cadres pour la construction de services Web RESTful.

Il permet aux développeurs de démarrer rapidement le projet. Cela vous aide à conditionner vos applications pour qu'elles soient facilement déployables dans un environnement de production en tant que services autonomes. Si vous avez déjà été dans une situation où vous devez démarrer un projet dans le framework Spring, par exemple, vous savez probablement à quel point cela peut être pénible.

Illustration : Exemple de microservices dans le didacticiel Dropwizard.

Avec Dropwizard, il suffit d'ajouter une dépendance Maven.

Dans ce blog, je vais vous guider tout au long du processus d'écriture d'un simple service Dropwizard RESTful. Une fois que nous aurons terminé, nous aurons un service pour les opérations CRUD de base sur les "pièces". Peu importe ce qu'est la « partie » ; ça peut être n'importe quoi. Cela m'est venu à l'esprit en premier.

Nous stockerons les données dans une base de données MySQL, en utilisant JDBI pour les interroger, et utiliserons les points de terminaison suivants :

  • GET /parts - pour récupérer toutes les pièces de la base de données
  • GET /part/{id} pour obtenir une partie particulière de DB
  • POST /parts -pour créer une nouvelle pièce
  • PUT /parts/{id} -pour modifier une pièce existante
  • DELETE /parts/{id} -pour supprimer la pièce d'une base de données

Nous allons utiliser OAuth pour authentifier notre service, et enfin, y ajouter quelques tests unitaires.

Bibliothèques Dropwizard par défaut

Au lieu d'inclure toutes les bibliothèques nécessaires pour créer un service REST séparément et de configurer chacune d'elles, Dropwizard le fait pour nous. Voici la liste des bibliothèques fournies par défaut avec Dropwizard :

  • Jetty : vous auriez besoin de HTTP pour exécuter une application Web. Dropwizard intègre le conteneur de servlets Jetty pour exécuter des applications Web. Au lieu de déployer vos applications sur un serveur d'applications ou un serveur Web, Dropwizard définit une méthode principale qui appelle le serveur Jetty en tant que processus autonome. Pour l'instant, Dropwizard recommande de n'exécuter l'application qu'avec Jetty ; d'autres services Web comme Tomcat ne sont pas officiellement pris en charge.
  • Jersey : Jersey est l'une des meilleures implémentations d'API REST sur le marché. En outre, il suit la spécification JAX-RS standard et constitue l'implémentation de référence pour la spécification JAX-RS. Dropwizard utilise Jersey comme framework par défaut pour créer des applications Web RESTful.
  • Jackson : Jackson est la norme de facto pour la gestion du format JSON. C'est l'une des meilleures API de mappage d'objets pour le format JSON.
  • Métriques : Dropwizard possède son propre module de métriques pour exposer les métriques de l'application via des points de terminaison HTTP.
  • Guava : Outre des structures de données immuables hautement optimisées, Guava fournit un nombre croissant de classes pour accélérer le développement en Java.
  • Logback et Slf4j : ces deux éléments sont utilisés pour de meilleurs mécanismes de journalisation.
  • Freemarker et Moustache : Le choix des moteurs de modèles pour votre application est l'une des décisions clés. Le moteur de template choisi doit être plus flexible pour écrire de meilleurs scripts. Dropwizard utilise les moteurs de modèles bien connus et populaires Freemarker et Moustache pour créer les interfaces utilisateur.

Outre la liste ci-dessus, il existe de nombreuses autres bibliothèques telles que Joda Time, Liquibase, Apache HTTP Client et Hibernate Validator utilisées par Dropwizard pour créer des services REST.

MavenConfiguration

Dropwizard prend officiellement en charge Maven. Même si vous pouvez utiliser d'autres outils de construction, la plupart des guides et de la documentation utilisent Maven, nous allons donc l'utiliser également ici. Si vous n'êtes pas familier avec Maven, vous pouvez consulter ce tutoriel Maven.

Il s'agit de la première étape de la création de votre application Dropwizard. Veuillez ajouter l'entrée suivante dans le fichier pom.xml de votre Maven :

 <dependencies> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-core</artifactId> <version>${dropwizard.version}</version> </dependency> </dependencies>

Avant d'ajouter l'entrée ci-dessus, vous pouvez ajouter le dropwizard.version comme ci-dessous :

 <properties> <dropwizard.version>1.1.0</dropwizard.version> </properties>

C'est ça. Vous avez fini d'écrire la configuration Maven. Cela téléchargera toutes les dépendances requises pour votre projet. La version actuelle de Dropwizard est 1.1.0, nous l'utiliserons donc dans ce guide.

Maintenant, nous pouvons passer à l'écriture de notre première véritable application Dropwizard.

Définir la classe de configuration

Dropwizard stocke les configurations dans des fichiers YAML. Vous aurez besoin du fichier configuration.yml dans le dossier racine de votre application. Ce fichier sera ensuite désérialisé en une instance de la classe de configuration de votre application et validé. Le fichier de configuration de votre application est la sous-classe de la classe de configuration de Dropwizard ( io.dropwizard.Configuration ).

Créons une classe de configuration simple :

 import javax.validation.Valid; import javax.validation.constraints.NotNull; import com.fasterxml.jackson.annotation.JsonProperty; import io.dropwizard.Configuration; import io.dropwizard.db.DataSourceFactory; public class DropwizardBlogConfiguration extends Configuration { private static final String DATABASE = "database"; @Valid @NotNull private DataSourceFactory dataSourceFactory = new DataSourceFactory(); @JsonProperty(DATABASE) public DataSourceFactory getDataSourceFactory() { return dataSourceFactory; } @JsonProperty(DATABASE) public void setDataSourceFactory(final DataSourceFactory dataSourceFactory) { this.dataSourceFactory = dataSourceFactory; } }

Le fichier de configuration YAML ressemblerait à ceci :

 database: driverClass: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost/dropwizard_blog user: dropwizard_blog password: dropwizard_blog maxWaitForConnection: 1s validationQuery: "SELECT 1" validationQueryTimeout: 3s minSize: 8 maxSize: 32 checkConnectionWhileIdle: false evictionInterval: 10s minIdleTime: 1 minute checkConnectionOnBorrow: true

La classe ci-dessus sera désérialisée du fichier YAML et placera les valeurs du fichier YAML dans cet objet.

Définir une classe d'application

Nous devrions maintenant aller créer la classe d'application principale. Cette classe rassemblera tous les bundles, lancera l'application et la fera fonctionner.

Voici un exemple de classe d'application dans Dropwizard :

 import io.dropwizard.Application; import io.dropwizard.auth.AuthDynamicFeature; import io.dropwizard.auth.oauth.OAuthCredentialAuthFilter; import io.dropwizard.setup.Environment; import javax.sql.DataSource; import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature; import org.skife.jdbi.v2.DBI; import com.toptal.blog.auth.DropwizardBlogAuthenticator; import com.toptal.blog.auth.DropwizardBlogAuthorizer; import com.toptal.blog.auth.User; import com.toptal.blog.config.DropwizardBlogConfiguration; import com.toptal.blog.health.DropwizardBlogApplicationHealthCheck; import com.toptal.blog.resource.PartsResource; import com.toptal.blog.service.PartsService; public class DropwizardBlogApplication extends Application<DropwizardBlogConfiguration> { private static final String SQL = "sql"; private static final String DROPWIZARD_BLOG_SERVICE = "Dropwizard blog service"; private static final String BEARER = "Bearer"; public static void main(String[] args) throws Exception { new DropwizardBlogApplication().run(args); } @Override public void run(DropwizardBlogConfiguration configuration, Environment environment) { // Datasource configuration final DataSource dataSource = configuration.getDataSourceFactory().build(environment.metrics(), SQL); DBI dbi = new DBI(dataSource); // Register Health Check DropwizardBlogApplicationHealthCheck healthCheck = new DropwizardBlogApplicationHealthCheck(dbi.onDemand(PartsService.class)); environment.healthChecks().register(DROPWIZARD_BLOG_SERVICE, healthCheck); // Register OAuth authentication environment.jersey() .register(new AuthDynamicFeature(new OAuthCredentialAuthFilter.Builder<User>() .setAuthenticator(new DropwizardBlogAuthenticator()) .setAuthorizer(new DropwizardBlogAuthorizer()).setPrefix(BEARER).buildAuthFilter())); environment.jersey().register(RolesAllowedDynamicFeature.class); // Register resources environment.jersey().register(new PartsResource(dbi.onDemand(PartsService.class))); } }

Ce qui est fait ci-dessus est de remplacer la méthode d'exécution Dropwizard. Dans cette méthode, nous instancions une connexion DB, enregistrons notre bilan de santé personnalisé (nous en reparlerons plus tard), initialisons l'authentification OAuth pour notre service et enfin, enregistrons une ressource Dropwizard.

Tout cela sera expliqué plus tard.

Définir une classe de représentation

Nous devons maintenant commencer à réfléchir à notre API REST et à ce que sera la représentation de notre ressource. Nous devons concevoir le format JSON et la classe de représentation correspondante qui convertit au format JSON souhaité.

Examinons l'exemple de format JSON pour cet exemple de classe de représentation simple :

 { "code": 200, "data": { "id": 1, "name": "Part 1", "code": "PART_1_CODE" } }

Pour le format JSON ci-dessus, nous créerions la classe de représentation comme ci-dessous :

 import org.hibernate.validator.constraints.Length; import com.fasterxml.jackson.annotation.JsonProperty; public class Representation<T> { private long code; @Length(max = 3) private T data; public Representation() { // Jackson deserialization } public Representation(long code, T data) { this.code = code; this.data = data; } @JsonProperty public long getCode() { return code; } @JsonProperty public T getData() { return data; } }

C'est un POJO assez simple.

Définition d'une classe de ressources

Une ressource est ce que sont les services REST. Ce n'est rien d'autre qu'un URI de point de terminaison pour accéder à la ressource sur le serveur. Dans cet exemple, nous aurons une classe de ressources avec peu d'annotations pour le mappage d'URI de demande. Étant donné que Dropwizard utilise l'implémentation JAX-RS, nous définirons le chemin URI à l'aide de l'annotation @Path .

Voici une classe de ressources pour notre exemple Dropwizard :

 import java.util.List; import javax.annotation.security.RolesAllowed; import javax.validation.Valid; import javax.validation.constraints.NotNull; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.eclipse.jetty.http.HttpStatus; import com.codahale.metrics.annotation.Timed; import com.toptal.blog.model.Part; import com.toptal.blog.representation.Representation; import com.toptal.blog.service.PartsService; @Path("/parts") @Produces(MediaType.APPLICATION_JSON) @RolesAllowed("ADMIN") public class PartsResource { private final PartsService partsService;; public PartsResource(PartsService partsService) { this.partsService = partsService; } @GET @Timed public Representation<List<Part>> getParts() { return new Representation<List<Part>>(HttpStatus.OK_200, partsService.getParts()); } @GET @Timed @Path("{id}") public Representation<Part> getPart(@PathParam("id") final int id) { return new Representation<Part>(HttpStatus.OK_200, partsService.getPart(id)); } @POST @Timed public Representation<Part> createPart(@NotNull @Valid final Part part) { return new Representation<Part>(HttpStatus.OK_200, partsService.createPart(part)); } @PUT @Timed @Path("{id}") public Representation<Part> editPart(@NotNull @Valid final Part part, @PathParam("id") final int id) { part.setId(id); return new Representation<Part>(HttpStatus.OK_200, partsService.editPart(part)); } @DELETE @Timed @Path("{id}") public Representation<String> deletePart(@PathParam("id") final int id) { return new Representation<String>(HttpStatus.OK_200, partsService.deletePart(id)); } }

Vous pouvez voir que tous les points de terminaison sont réellement définis dans cette classe.

Enregistrer une ressource

Je reviendrais maintenant à la classe d'application principale. Vous pouvez voir à la fin de cette classe que nous avons enregistré notre ressource pour qu'elle soit initialisée avec l'exécution du service. Nous devons le faire avec toutes les ressources que nous pourrions avoir dans notre application. Voici l'extrait de code responsable de cela :

 // Register resources environment.jersey().register(new PartsResource(dbi.onDemand(PartsService.class)));

Couche de service

Pour une bonne gestion des exceptions et la possibilité d'être indépendant du moteur de stockage de données, nous allons introduire une classe de service « couche intermédiaire ». C'est la classe que nous appellerons depuis notre couche de ressources, et nous ne nous soucions pas de ce qui est sous-jacent. C'est pourquoi nous avons cette couche entre les couches de ressources et DAO. Voici notre classe de service :

 import java.util.List; import java.util.Objects; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response.Status; import org.skife.jdbi.v2.exceptions.UnableToExecuteStatementException; import org.skife.jdbi.v2.exceptions.UnableToObtainConnectionException; import org.skife.jdbi.v2.sqlobject.CreateSqlObject; import com.toptal.blog.dao.PartsDao; import com.toptal.blog.model.Part; public abstract class PartsService { private static final String PART_NOT_FOUND = "Part id %s not found."; private static final String DATABASE_REACH_ERROR = "Could not reach the MySQL database. The database may be down or there may be network connectivity issues. Details: "; private static final String DATABASE_CONNECTION_ERROR = "Could not create a connection to the MySQL database. The database configurations are likely incorrect. Details: "; private static final String DATABASE_UNEXPECTED_ERROR = "Unexpected error occurred while attempting to reach the database. Details: "; private static final String SUCCESS = "Success..."; private static final String UNEXPECTED_ERROR = "An unexpected error occurred while deleting part."; @CreateSqlObject abstract PartsDao partsDao(); public List<Part> getParts() { return partsDao().getParts(); } public Part getPart(int id) { Part part = partsDao().getPart(id); if (Objects.isNull(part)) { throw new WebApplicationException(String.format(PART_NOT_FOUND, id), Status.NOT_FOUND); } return part; } public Part createPart(Part part) { partsDao().createPart(part); return partsDao().getPart(partsDao().lastInsertId()); } public Part editPart(Part part) { if (Objects.isNull(partsDao().getPart(part.getId()))) { throw new WebApplicationException(String.format(PART_NOT_FOUND, part.getId()), Status.NOT_FOUND); } partsDao().editPart(part); return partsDao().getPart(part.getId()); } public String deletePart(final int id) { int result = partsDao().deletePart(id); switch (result) { case 1: return SUCCESS; case 0: throw new WebApplicationException(String.format(PART_NOT_FOUND, id), Status.NOT_FOUND); default: throw new WebApplicationException(UNEXPECTED_ERROR, Status.INTERNAL_SERVER_ERROR); } } public String performHealthCheck() { try { partsDao().getParts(); } catch (UnableToObtainConnectionException ex) { return checkUnableToObtainConnectionException(ex); } catch (UnableToExecuteStatementException ex) { return checkUnableToExecuteStatementException(ex); } catch (Exception ex) { return DATABASE_UNEXPECTED_ERROR + ex.getCause().getLocalizedMessage(); } return null; } private String checkUnableToObtainConnectionException(UnableToObtainConnectionException ex) { if (ex.getCause() instanceof java.sql.SQLNonTransientConnectionException) { return DATABASE_REACH_ERROR + ex.getCause().getLocalizedMessage(); } else if (ex.getCause() instanceof java.sql.SQLException) { return DATABASE_CONNECTION_ERROR + ex.getCause().getLocalizedMessage(); } else { return DATABASE_UNEXPECTED_ERROR + ex.getCause().getLocalizedMessage(); } } private String checkUnableToExecuteStatementException(UnableToExecuteStatementException ex) { if (ex.getCause() instanceof java.sql.SQLSyntaxErrorException) { return DATABASE_CONNECTION_ERROR + ex.getCause().getLocalizedMessage(); } else { return DATABASE_UNEXPECTED_ERROR + ex.getCause().getLocalizedMessage(); } } }

La dernière partie est en fait une implémentation de bilan de santé, dont nous parlerons plus tard.

Couche DAO, JDBI et Mapper

Dropwizard prend en charge JDBI et Hibernate. C'est un module Maven séparé, donc ajoutons-le d'abord en tant que dépendance ainsi que le connecteur MySQL :

 <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-jdbi</artifactId> <version>${dropwizard.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.connector.version}</version> </dependency>

Pour un service CRUD simple, je préfère personnellement JDBI, car il est plus simple et beaucoup plus rapide à mettre en œuvre. J'ai créé un schéma MySQL simple avec une seule table à utiliser dans notre exemple. Vous pouvez trouver le script d'initialisation du schéma dans la source. JDBI offre une écriture de requête simple en utilisant des annotations telles que @SqlQuery pour la lecture et @SqlUpdate pour l'écriture de données. Voici notre interface DAO :

 import java.util.List; import org.skife.jdbi.v2.sqlobject.Bind; import org.skife.jdbi.v2.sqlobject.BindBean; import org.skife.jdbi.v2.sqlobject.SqlQuery; import org.skife.jdbi.v2.sqlobject.SqlUpdate; import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapper; import com.toptal.blog.mapper.PartsMapper; import com.toptal.blog.model.Part; @RegisterMapper(PartsMapper.class) public interface PartsDao { @SqlQuery("select * from parts;") public List<Part> getParts(); @SqlQuery("select * from parts where id = :id") public Part getPart(@Bind("id") final int id); @SqlUpdate("insert into parts(name, code) values(:name, :code)") void createPart(@BindBean final Part part); @SqlUpdate("update parts set name = coalesce(:name, name), code = coalesce(:code, code) where id = :id") void editPart(@BindBean final Part part); @SqlUpdate("delete from parts where id = :id") int deletePart(@Bind("id") final int id); @SqlQuery("select last_insert_id();") public int lastInsertId(); }

Comme vous pouvez le voir, c'est assez simple. Cependant, nous devons mapper nos jeux de résultats SQL sur un modèle, ce que nous faisons en enregistrant une classe de mappeur. Voici notre classe mapper :

 import java.sql.ResultSet; import java.sql.SQLException; import org.skife.jdbi.v2.StatementContext; import org.skife.jdbi.v2.tweak.ResultSetMapper; import com.toptal.blog.model.Part; public class PartsMapper implements ResultSetMapper<Part> { private static final String; private static final String NAME = "name"; private static final String CODE = "code"; public Part map(int i, ResultSet resultSet, StatementContext statementContext) throws SQLException { return new Part(resultSet.getInt(ID), resultSet.getString(NAME), resultSet.getString(CODE)); } }

Et notre modèle :

 import org.hibernate.validator.constraints.NotEmpty; public class Part { private int id; @NotEmpty private String name; @NotEmpty private String code; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public Part() { super(); } public Part(int id, String name, String code) { super(); this.id = id; this.name = name; this.code = code; } }

Bilan de santé Dropwizard

Dropwizard offre un support natif pour la vérification de l'état. Dans notre cas, nous aimerions probablement vérifier si la base de données est opérationnelle avant de dire que notre service est sain. Ce que nous faisons est en fait d'effectuer une action simple sur la base de données, comme obtenir des parties de la base de données et gérer les résultats potentiels (succès ou exceptions).

Voici notre mise en œuvre de la vérification de l'état dans Dropwizard :

 import com.codahale.metrics.health.HealthCheck; import com.toptal.blog.service.PartsService; public class DropwizardBlogApplicationHealthCheck extends HealthCheck { private static final String HEALTHY = "The Dropwizard blog Service is healthy for read and write"; private static final String UNHEALTHY = "The Dropwizard blog Service is not healthy. "; private static final String MESSAGE_PLACEHOLDER = "{}"; private final PartsService partsService; public DropwizardBlogApplicationHealthCheck(PartsService partsService) { this.partsService = partsService; } @Override public Result check() throws Exception { String mySqlHealthStatus = partsService.performHealthCheck(); if (mySqlHealthStatus == null) { return Result.healthy(HEALTHY); } else { return Result.unhealthy(UNHEALTHY + MESSAGE_PLACEHOLDER, mySqlHealthStatus); } } }

Ajouter une authentification

Dropwizard prend en charge l'authentification de base et OAuth. Ici. Je vais vous montrer comment protéger votre service avec OAuth. Cependant, en raison de la complexité, j'ai omis une structure de base de données sous-jacente et j'ai juste montré comment elle est enveloppée. La mise en œuvre à grande échelle ne devrait pas être un problème à partir d'ici. Dropwizard a deux interfaces importantes que nous devons implémenter.

Le premier est Authenticator. Notre classe doit implémenter la méthode authenticate , qui doit vérifier si le jeton d'accès donné est valide. J'appellerais donc cela comme une première porte d'accès à l'application. En cas de succès, il doit renvoyer un principal. Ce principal est notre utilisateur réel avec son rôle. Le rôle est important pour une autre interface Dropwizard que nous devons implémenter. Celui-ci est Authorizer, et il est chargé de vérifier si l'utilisateur dispose des autorisations suffisantes pour accéder à une certaine ressource. Donc, si vous revenez en arrière et vérifiez notre classe de ressources, vous verrez qu'elle nécessite le rôle d'administrateur pour accéder à ses points de terminaison. Ces annotations peuvent également être par méthode. La prise en charge de l'autorisation Dropwizard est un module Maven distinct, nous devons donc l'ajouter aux dépendances :

 <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-auth</artifactId> <version>${dropwizard.version}</version> </dependency>

Voici les classes de notre exemple qui ne font rien d'intelligent, mais c'est un squelette pour une autorisation OAuth à grande échelle :

 import java.util.Optional; import io.dropwizard.auth.AuthenticationException; import io.dropwizard.auth.Authenticator; public class DropwizardBlogAuthenticator implements Authenticator<String, User> { @Override public Optional<User> authenticate(String token) throws AuthenticationException { if ("test_token".equals(token)) { return Optional.of(new User()); } return Optional.empty(); } }
 import java.util.Objects; import io.dropwizard.auth.Authorizer; public class DropwizardBlogAuthorizer implements Authorizer<User> { @Override public boolean authorize(User principal, String role) { // Allow any logged in user. if (Objects.nonNull(principal)) { return true; } return false; } }
 import java.security.Principal; public class User implements Principal { private int id; private String username; private String password; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String getName() { return username; } }

Tests unitaires dans Dropwizard

Ajoutons quelques tests unitaires à notre application. Je m'en tiendrai aux tests des parties spécifiques du code de Dropwizard, dans notre cas Representation and Resource. Nous devrons ajouter les dépendances suivantes à notre fichier Maven :

 <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-testing</artifactId> <version>${dropwizard.version}</version> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>${mockito.version}</version> <scope>test</scope> </dependency>

Pour tester la représentation, nous aurons également besoin d'un exemple de fichier JSON à tester. Créons donc fixtures/part.json sous src/test/resources :

 { "id": 1, "name": "testPartName", "code": "testPartCode" }

Et voici la classe de test JUnit :

 import static io.dropwizard.testing.FixtureHelpers.fixture; import static org.assertj.core.api.Assertions.assertThat; import org.junit.Test; import com.fasterxml.jackson.databind.ObjectMapper; import com.toptal.blog.model.Part; import io.dropwizard.jackson.Jackson; public class RepresentationTest { private static final ObjectMapper MAPPER = Jackson.newObjectMapper(); private static final String PART_JSON = "fixtures/part.json"; private static final String TEST_PART_NAME = "testPartName"; private static final String TEST_PART_CODE = "testPartCode"; @Test public void serializesToJSON() throws Exception { final Part part = new Part(1, TEST_PART_NAME, TEST_PART_CODE); final String expected = MAPPER.writeValueAsString(MAPPER.readValue(fixture(PART_JSON), Part.class)); assertThat(MAPPER.writeValueAsString(part)).isEqualTo(expected); } @Test public void deserializesFromJSON() throws Exception { final Part part = new Part(1, TEST_PART_NAME, TEST_PART_CODE); assertThat(MAPPER.readValue(fixture(PART_JSON), Part.class).getId()).isEqualTo(part.getId()); assertThat(MAPPER.readValue(fixture(PART_JSON), Part.class).getName()) .isEqualTo(part.getName()); assertThat(MAPPER.readValue(fixture(PART_JSON), Part.class).getCode()) .isEqualTo(part.getCode()); } }

Lorsqu'il s'agit de tester des ressources, le point principal du test de Dropwizard est que vous vous comportez en fait comme un client HTTP, en envoyant des requêtes HTTP contre des ressources. Ainsi, vous ne testez pas des méthodes comme vous le feriez habituellement dans un cas courant. Voici l'exemple de notre classe PartsResource :

 public class PartsResourceTest { private static final String SUCCESS = "Success..."; private static final String TEST_PART_NAME = "testPartName"; private static final String TEST_PART_CODE = "testPartCode"; private static final String PARTS_ENDPOINT = "/parts"; private static final PartsService partsService = mock(PartsService.class); @ClassRule public static final ResourceTestRule resources = ResourceTestRule.builder().addResource(new PartsResource(partsService)).build(); private final Part part = new Part(1, TEST_PART_NAME, TEST_PART_CODE); @Before public void setup() { when(partsService.getPart(eq(1))).thenReturn(part); List<Part> parts = new ArrayList<>(); parts.add(part); when(partsService.getParts()).thenReturn(parts); when(partsService.createPart(any(Part.class))).thenReturn(part); when(partsService.editPart(any(Part.class))).thenReturn(part); when(partsService.deletePart(eq(1))).thenReturn(SUCCESS); } @After public void tearDown() { reset(partsService); } @Test public void testGetPart() { Part partResponse = resources.target(PARTS_ENDPOINT + "/1").request() .get(TestPartRepresentation.class).getData(); assertThat(partResponse.getId()).isEqualTo(part.getId()); assertThat(partResponse.getName()).isEqualTo(part.getName()); assertThat(partResponse.getCode()).isEqualTo(part.getCode()); verify(partsService).getPart(1); } @Test public void testGetParts() { List<Part> parts = resources.target(PARTS_ENDPOINT).request().get(TestPartsRepresentation.class).getData(); assertThat(parts.size()).isEqualTo(1); assertThat(parts.get(0).getId()).isEqualTo(part.getId()); assertThat(parts.get(0).getName()).isEqualTo(part.getName()); assertThat(parts.get(0).getCode()).isEqualTo(part.getCode()); verify(partsService).getParts(); } @Test public void testCreatePart() { Part newPart = resources.target(PARTS_ENDPOINT).request() .post(Entity.entity(part, MediaType.APPLICATION_JSON_TYPE), TestPartRepresentation.class) .getData(); assertNotNull(newPart); assertThat(newPart.getId()).isEqualTo(part.getId()); assertThat(newPart.getName()).isEqualTo(part.getName()); assertThat(newPart.getCode()).isEqualTo(part.getCode()); verify(partsService).createPart(any(Part.class)); } @Test public void testEditPart() { Part editedPart = resources.target(PARTS_ENDPOINT + "/1").request() .put(Entity.entity(part, MediaType.APPLICATION_JSON_TYPE), TestPartRepresentation.class) .getData(); assertNotNull(editedPart); assertThat(editedPart.getId()).isEqualTo(part.getId()); assertThat(editedPart.getName()).isEqualTo(part.getName()); assertThat(editedPart.getCode()).isEqualTo(part.getCode()); verify(partsService).editPart(any(Part.class)); } @Test public void testDeletePart() { assertThat(resources.target(PARTS_ENDPOINT + "/1").request() .delete(TestDeleteRepresentation.class).getData()).isEqualTo(SUCCESS); verify(partsService).deletePart(1); } private static class TestPartRepresentation extends Representation<Part> { } private static class TestPartsRepresentation extends Representation<List<Part>> { } private static class TestDeleteRepresentation extends Representation<String> { } }

Créez votre application Dropwizard

La meilleure pratique consiste à créer le fichier FAT JAR unique qui contient tous les fichiers .class requis pour exécuter votre application. Le même fichier JAR peut être déployé dans différents environnements, du test à la production, sans aucune modification des bibliothèques de dépendances. Pour commencer à construire notre exemple d'application en tant que fat JAR, nous devons configurer un plugin Maven appelé maven-shade. Vous devez ajouter les entrées suivantes dans la section plugins de votre fichier pom.xml.

Voici l'exemple de configuration Maven pour la construction du fichier JAR.

 <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.endava</groupId> <artifactId>dropwizard-blog</artifactId> <version>0.0.1-SNAPSHOT</version> <name>Dropwizard Blog example</name> <properties> <dropwizard.version>1.1.0</dropwizard.version> <mockito.version>2.7.12</mockito.version> <mysql.connector.version>6.0.6</mysql.connector.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-core</artifactId> <version>${dropwizard.version}</version> </dependency> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-jdbi</artifactId> <version>${dropwizard.version}</version> </dependency> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-auth</artifactId> <version>${dropwizard.version}</version> </dependency> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-testing</artifactId> <version>${dropwizard.version}</version> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>${mockito.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.connector.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>2.3</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>com.endava.blog.DropwizardBlogApplication</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>

Exécution de votre application

Maintenant, nous devrions être en mesure d'exécuter le service. Si vous avez réussi à créer votre fichier JAR, il vous suffit d'ouvrir l'invite de commande et d'exécuter simplement la commande suivante pour exécuter votre fichier JAR :

 java -jar target/dropwizard-blog-1.0.0.jar server configuration.yml

Si tout allait bien, alors vous verriez quelque chose comme ceci :

 INFO [2017-04-23 22:51:14,471] org.eclipse.jetty.util.log: Logging initialized @962ms to org.eclipse.jetty.util.log.Slf4jLog INFO [2017-04-23 22:51:14,537] io.dropwizard.server.DefaultServerFactory: Registering jersey handler with root path prefix: / INFO [2017-04-23 22:51:14,538] io.dropwizard.server.DefaultServerFactory: Registering admin handler with root path prefix: / INFO [2017-04-23 22:51:14,681] io.dropwizard.server.DefaultServerFactory: Registering jersey handler with root path prefix: / INFO [2017-04-23 22:51:14,681] io.dropwizard.server.DefaultServerFactory: Registering admin handler with root path prefix: / INFO [2017-04-23 22:51:14,682] io.dropwizard.server.ServerFactory: Starting DropwizardBlogApplication INFO [2017-04-23 22:51:14,752] org.eclipse.jetty.setuid.SetUIDListener: Opened application@7d57dbb5{HTTP/1.1,[http/1.1]}{0.0.0.0:8080} INFO [2017-04-23 22:51:14,752] org.eclipse.jetty.setuid.SetUIDListener: Opened admin@630b6190{HTTP/1.1,[http/1.1]}{0.0.0.0:8081} INFO [2017-04-23 22:51:14,753] org.eclipse.jetty.server.Server: jetty-9.4.2.v20170220 INFO [2017-04-23 22:51:15,153] io.dropwizard.jersey.DropwizardResourceConfig: The following paths were found for the configured resources: GET /parts (com.toptal.blog.resource.PartsResource) POST /parts (com.toptal.blog.resource.PartsResource) DELETE /parts/{id} (com.toptal.blog.resource.PartsResource) GET /parts/{id} (com.toptal.blog.resource.PartsResource) PUT /parts/{id} (com.toptal.blog.resource.PartsResource) INFO [2017-04-23 22:51:15,154] org.eclipse.jetty.server.handler.ContextHandler: Started idjMutableServletContextHandler@58fa5769{/,null,AVAILABLE} INFO [2017-04-23 22:51:15,158] io.dropwizard.setup.AdminEnvironment: tasks = POST /tasks/log-level (io.dropwizard.servlets.tasks.LogConfigurationTask) POST /tasks/gc (io.dropwizard.servlets.tasks.GarbageCollectionTask) INFO [2017-04-23 22:51:15,162] org.eclipse.jetty.server.handler.ContextHandler: Started idjMutableServletContextHandler@3fdcde7a{/,null,AVAILABLE} INFO [2017-04-23 22:51:15,176] org.eclipse.jetty.server.AbstractConnector: Started application@7d57dbb5{HTTP/1.1,[http/1.1]}{0.0.0.0:8080} INFO [2017-04-23 22:51:15,177] org.eclipse.jetty.server.AbstractConnector: Started admin@630b6190{HTTP/1.1,[http/1.1]}{0.0.0.0:8081} INFO [2017-04-23 22:51:15,177] org.eclipse.jetty.server.Server: Started @1670ms

Now you have your own Dropwizard application listening on ports 8080 for application requests and 8081 for administration requests.

Note that server configuration.yml is used for starting the HTTP server and passing the YAML configuration file location to the server.

Excellent! Finally, we have implemented a microservice using Dropwizard framework. Now let's go for a break and have a cup of tea. You have done really good job.

Accessing Resources

You can use any HTTP client like POSTMAN or any else. You should be able to access your server by hitting http://localhost:8080/parts . You should be receiving a message that the credentials are required to access the service. To authenticate, add Authorization header with bearer test_token value. If done successfully, you should see something like:

 { "code": 200, "data": [] }

meaning that your DB is empty. Create your first part by switching HTTP method from GET to POST, and supply this payload:

 { "name":"My first part", "code":"code_of_my_first_part" }

All other endpoints work in the same manner, so keep playing and enjoy.

How to Change Context Path

By default, Dropwizard application will start and running in the / . For example, if you are not mentioning anything about the context path of the application, by default, the application can be accessed from the URL http://localhost:8080/ . If you want to configure your own context path for your application, then please add the following entries to your YAML file.

 server: applicationContextPath: /application

Wrapping up our Dropwizard Tutorial

Now when you have your Dropwizard REST service up and running, let's summarize some key advantages or disadvantages of using Dropwizard as a REST framework. It's absolutely obvious from this post that Dropwizard offers extremely fast bootstrap of your project. And that's probably the biggest advantage of using Dropwizard.

Also, it will include all the cutting-edge libraries/tools that you will ever need in developing your service. So you definitely do not need to worry about that. It also gives you very nice configuration management. Of course, Dropwizard has some disadvantages as well. By using Dropwizard, you're kind of restricted to using what Dropwizard offers or supports. You lose some of the freedom you may be used to when developing. But still, I wouldn't even call it a disadvantage, as this is exactly what makes Dropwizard what it is—easy to set up, easy to develop, but yet a very robust and high-performance REST framework.

In my opinion, adding complexity to the framework by supporting more and more third party libraries would also introduce unnecessary complexity in development.