Un Comienzo con Microservicios: Un Tutorial de Dropwizard

Total
1
Shares

Todos somos testigos de un aumento en la popularidad de las arquitecturas de microservicios. En una arquitectura de microservicios, Dropwizard ocupa un lugar muy importante. Es un marco para la creación de servicios web RESTful o, para ser más específicos, un conjunto de herramientas y frameworks para la creación de servicios web RESTful.

Permite a los desarrolladores un arranque más rápido del proyecto. Esto te ayuda a empaquetar tus aplicaciones, para que se puedan desplegar fácilmente en un entorno de producción como servicios independientes. Si alguna vez has estado en una situación en la que necesitas arrancar un proyecto en el framework Spring, por ejemplo, probablemente sabes lo doloroso que puede ser. Si no sabes cómo empezar un blog, considera consultar 501words.

Ilustración: Ejemplo de Microservicios en el Tutorial de Dropwizard

Con Dropwizard, solo se trata de añadir una dependencia de Maven.

En este blog, te guiaré a través del proceso completo de escribir un simple servicio Dropwizard RESTful. Cuando terminemos, tendremos un servicio para operaciones básicas de CRUD en “partes”. Realmente no importa que es “parte”; puede ser cualquier cosa, pero fue lo primero que se me ocurrió.

Almacenaremos la data en una base de datos MySQL, usando JDBI para consultarla y usaremos los siguientes endpoints:

  • GET /parts -to retrieve all parts from DB
  • GET /part/{id} to get a particular part from DB
  • POST /parts -to create new part
  • PUT /parts/{id} -to edit an existing part
  • DELETE /parts/{id} -to delete the part from a DB

Usaremos OAuth para autenticar nuestro servicio y luego agregarle algunas pruebas de unidad

Bibliotecas Dropwizard Predeterminadas

En lugar de incluir todas las bibliotecas necesarias para crear un servicio REST por separado y configurar cada una de ellas, Dropwizard lo hace por nosotros. Aquí está la lista de bibliotecas que vienen predeterminadas con Dropwizard:

  • Jetty: Necesitarás HTTP para ejecutar una aplicación web. Dropwizard incorpora el contenedor servletJetty para ejecutar aplicaciones web. En lugar de implementar tus aplicaciones en un servidor de aplicaciones o servidor web, Dropwizard define un método principal que invoca al servidor Jetty como un proceso autónomo. A partir de ahora, Dropwizard recomienda sólo ejecutar la aplicación con Jetty; otros servicios web como Tomcat no son oficialmente compatibles.
  • Jersey: Jersey es una de las mejores implementaciones API de REST en el mercado. Además, sigue la especificación estándar de JAX-RS y es la implementación de referencia para la especificación JAX-RS. Dropwizard utiliza Jersey como el marco predeterminado para la creación de aplicaciones web RESTful.
  • Jackson: Jackson es el estándar de facto para el manejo del formato JSON. Es una de las mejores API de asignación de objetos para el formato JSON.
  • Métricas: Dropwizard tiene su propio módulo de métricas para exponer las métricas de la aplicación a través de los endpoints HTTP.
  • Guava: Además de estructuras de data inmutables y altamente optimizadas, Guava proporciona un número creciente de clases para acelerar el desarrollo en Java.
  • Logback y Slf4j: Estos dos se utilizan para mejorar los mecanismos de registro.
  • Freemarker y Mustache: La elección de motores de plantilla para su aplicación es una de las decisiones clave. El motor de plantilla elegido tiene que ser más flexible para escribir mejores secuencias de comandos. Dropwizard utiliza motores de plantilla Freemarker y Mustache, los cuales son bien conocidos y populares, para la construcción de las interfaces de usuario.

Aparte de la lista anterior, hay muchas otras bibliotecas como Joda Time, Liquibase, Apache HTTP Client y Hibernate Validator utilizadas por Dropwizard para la creación de servicios REST.

Configuración Maven

Dropwizard apoya, de manera oficial, a Maven. Incluso si puedes usar otras herramientas de compilación, la mayoría de las guías y documentación utilizan Maven, así que también lo usaremos aquí. Si no estás familiarizado con Maven, puedes consultar este tutorial Maven.

Este es el primer paso para crear tu aplicación Dropwizard. Agrega la siguiente entrada en el archivo pom.xml de Maven:

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

Antes de añadir la entrada anterior, puedes añadir dropwizard.versión como se muestra a continuación:

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

Eso es todo. Ya terminaste de escribir la configuración de Maven. Esto descargará todas las dependencias necesarias para tu proyecto. La versión de esta fecha de Dropwizard es 1.1.0, así que la estaremos usando en esta guía.

Ahora podemos pasar a escribir nuestra primera aplicación Dropwizard real.

Definir Clase de Configuración

Dropwizard almacena configuraciones en archivos YAML. Vas a necesitar el archivo configuration.yml en la carpeta raíz de la aplicación. Este archivo se deserializará a una instancia de la clase de configuración de tu aplicación y se validará. El archivo de configuración de tu aplicación es la subclase de la clase de configuración de Dropwizard (io.dropwizard.Configuration).

Vamos a crear una clase configuración 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;
 }
}

El archivo de configuración YAML se vería así:

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 clase anterior se deserializará del archivo YAML y colocará los valores del archivo YAML en este objeto.

Definir una Clase de Aplicación

Ahora debemos ir y crear la clase de aplicación principal. Esta clase reunirá todos los paquetes, llevará la aplicación y la pondrá en funcionamiento para tu uso.

A continuación se muestra un ejemplo de una clase de aplicación en 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)));
 }
}

Lo que hicimos anteriormente es anular el método de ejecución de Dropwizard. En este método, estamos instanciando una conexión de DB (Base de datos), registrando nuestro chequeo de salud personalizado (hablaremos de eso más adelante), inicializando la autenticación OAuth para nuestro servicio y finalmente, registrando un recurso Dropwizard.

Todo esto se explicará más adelante.

Define una Clase de Representación

Ahora tenemos que empezar a pensar en nuestra REST API y cuál será la representación de nuestro recurso. Tenemos que diseñar el formato JSON y la clase de representación correspondiente que se convierte al formato JSON deseado.

Veamos el formato JSON de muestra para este ejemplo de clase de representación simple:

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

Para el formato JSON anterior, crearemos la clase de representación de la siguiente manera:

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;
 }
}

Esto es POJO de una manera muy simple.

Definición de una Clase de Recursos

Un recurso es en lo que se basan los servicios REST. No es nada más que un URI de endpoint para acceder al recurso en el servidor. En este ejemplo, tendremos una clase de recurso con pocas anotaciones para el mapeo de URI de solicitud. Dado que Dropwizard utiliza la implementación JAX-RS, definiremos la ruta URI utilizando la anotación @Path.

Aquí te muestro una clase de recurso para nuestro ejemplo de 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));
 }
}

Puedes ver como todos los endpoints realmente se definen en esta clase.

Registro de un Recurso

Volvería ahora a la clase de aplicación principal. Puedes ver al final de esa clase que hemos registrado nuestro recurso para ser inicializado con la ejecución del servicio. Tenemos que hacerlo con todos los recursos que podamos tener en nuestra aplicación. Éste es el fragmento de código responsable de eso:

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

Capa de Servicio

Para un manejo de excepciones adecuado y la capacidad de ser independiente del motor de almacenamiento de data, introduciremos una clase de servicio de “capa-media”. Esta es la clase que llamaremos desde nuestra capa de recursos, sin importar lo que está subyacente. Es por eso que tenemos esta capa en particular entre capas de recursos y DAO. Ésta es nuestra clase de servicio:

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 última parte de ésta es en realidad una implementación de control de salud, de la que hablaremos más adelante.

Capa DAO, JDBI y Mapper

Dropwizard es compatible con JDBI e Hibernate. Es un módulo Maven separado, por lo que primero vamos a agregarlo como una dependencia, así como el conector 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>

Para un simple servicio CRUD, personalmente prefiero JDBI, ya que es más sencillo y mucho más rápido de implementar. He creado un esquema simple de MySQL con una tabla solamente para ser utilizada en nuestro ejemplo. Puedes encontrar el script init para el esquema dentro del origen. JDBI ofrece escritura simple de preguntas, usando anotaciones tales como @SqlQuery para la lectura y @SqlUpdate para escribir data. Aquí está nuestra interfaz 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();
}

Como puedes ver, es bastante simple. Sin embargo, necesitamos asignar nuestros conjuntos de resultados de SQL a un modelo, lo cual se hace mediante el registro de una clase mapper. Aquí está nuestra clase 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 ID = "id";
 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));
 }
}

Y nuestro modelo:

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;
 }
}

Chequeo de Salud de Dropwizard

Dropwizard ofrece soporte nativo para la comprobación de la salud. En nuestro caso, probablemente vamos a comprobar si la base de datos está funcionando antes de decir que nuestro servicio es saludable. Lo que hacemos es realizar alguna acción sencilla de Base de Datos, como obtener partes de la Base de Datos y manejar los resultados potenciales (exitosos o excepciones).

Aquí está nuestra implementación de chequeo de salud en 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);
   }
 }
}

Adición de Autenticación

Dropwizard admite la autenticación básica y OAuth. Aquí, te mostraré cómo proteger tu servicio con OAuth. Sin embargo, debido a la complejidad, he omitido una estructura de Base de Datos subyacente y sólo mostraré cómo se desarrolla. Implementar a escala completa no debería ser un problema a partir de aquí. Dropwizard tiene dos interfaces importantes que necesitamos implementar.

El primero es Authenticator. Nuestra clase debe implementar el método authenticate, que debe comprobar si el identificador de acceso dado es válido. Así que yo llamaría esto como una primera puerta a la aplicación. Si se da con éxito, debería dar como resultado un principal. Este principal es nuestro usuario real con su rol. Éste es importante para otra interfaz de Dropwizard que necesitamos implementar. Éste es el Authorizer y es responsable de comprobar si el usuario tiene suficientes permisos para acceder a un determinado recurso. Por lo tanto, si regresas y revisas nuestra clase de recursos, verás que requiere el rol de administrador para acceder a sus endpoints. Estas anotaciones también pueden ser por método. El soporte de autorización Dropwizard es un módulo Maven separado, por lo que debemos agregarlo a dependencias:

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

Aquí están las clases de nuestro ejemplo que realmente no hacen nada inteligente pero es un esqueleto para una autorización de OAuth a gran escala:

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;
 }
}

Pruebas de Unidad en DropWizard

Vamos a agregar algunas pruebas de unidad a nuestra aplicación. Me atengo a probar partes específicas del código de Dropwizard, en nuestro caso: Representación y Recurso. Necesitaremos agregar las siguientes dependencias a nuestro archivo 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>

Para probar la representación, también necesitaremos un archivo JSON de ejemplo para probar contra éste. Así que vamos a crear fixtures/part.json bajo src/test/resources:

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

Y aquí está la clase de prueba 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());
 }
}

Cuando se trata de probar los recursos, el punto principal de la prueba de Dropwizard es que realmente se está comportando como un cliente HTTP, enviando solicitudes HTTP contra los recursos. Por lo tanto, no estás probando métodos como lo harías normalmente en un caso común. Aquí está el ejemplo de nuestra clase 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> {

 }
}

Construye tu Aplicación Dropwizard

La mejor práctica es crear el archivo único FAR JAR que contenga todos los archivos .class necesarios para ejecutar la aplicación. El mismo archivo JAR puede implementarse en un entorno diferente desde la prueba a la producción, sin ningún cambio en las bibliotecas de dependencias. Para comenzar a construir nuestra aplicación de ejemplo como un fat JAR, necesitamos configurar un complemento de Maven llamado maven-shade. Debes agregar las siguientes entradas en la sección de complementos de tu archivo pom.xml.

Aquí está la configuración Maven de ejemplo para construir el archivo 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>

Ejecución de la Aplicación

Ahora deberíamos poder ejecutar el servicio. Si construiste correctamente tu archivo JAR, lo que necesitas hacer es abrir el símbolo del sistema y ejecutar el siguiente comando para ejecutar tu archivo JAR:

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

Si todo salió bien, deberías ver algo como esto:

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 i.d.j.MutableServletContextHandler@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 i.d.j.MutableServletContextHandler@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

Ahora tienes tu propia aplicación Dropwizard escuchando en puertos 8080 para solicitudes de aplicación y 8081 para solicitudes de administración.

Ten en cuenta que server configuration.yml se utiliza para iniciar el servidor HTTP y pasar la ubicación del archivo de configuración YAML al servidor.

¡Excelente! Finalmente hemos implementado un microservicio usando el framework Dropwizard. Ahora vamos a tomar un descanso y tomar una taza de té. Hiciste un buen trabajo.

Acceso a los Recursos

Puedes utilizar cualquier cliente HTTP como POSTMAN o cualquier otro. Deberías poder acceder a tu servidor presionando http://localhost:8080/parts. Debes recibir un mensaje indicando que las credenciales son necesarias para acceder al servicio. Para autenticar, agrega el encabezado Authorization con el valor support_test_token. Si se realiza correctamente, deberías ver algo como:

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

Lo que significa que tu base de datos está vacía. Crea tu primera parte cambiando el método HTTP de GET a POST y suministra esta carga útil:

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

Todos los demás endpoints funcionan de la misma manera, así que sigue jugando y disfruta.

Cómo Cambiar la Ruta del Contexto

La aplicación Dropwizard, de forma predeterminada, se iniciará y se ejecutará en /. Por ejemplo, si no menciona nada sobre la ruta de contexto de la aplicación de forma predeterminada, se puede acceder a la aplicación desde la URL http://localhost: 8080/. Si deseas configurar tu propia ruta de contexto para tu aplicación, añade las siguientes entradas a tu archivo YAML. ~~~ server: applicationContextPath: /application ~~~

Finiquitando Nuestro Tutorial Dropwizard

Ahora, cuando tengas instalado el servicio REST de Dropwizard, es momento de resumir algunas de las ventajas o desventajas clave de utilizar Dropwizard como framework REST. Es absolutamente obvio, por este post, que Dropwizard ofrece un bootstrap extremadamente rápido de tu proyecto. Y eso es probablemente la mayor ventaja de usar Dropwizard.

Además incluirá todas las bibliotecas/herramientas de vanguardia que necesitarás para desarrollar tu servicio. Así que definitivamente no necesitas preocuparte por eso. También te da una gestión de configuración muy agradable. Por supuesto, Dropwizard tiene algunas desventajas también. Usando Dropwizard estás restringido a usar lo que Dropwizard ofrece o apoya. Pierdes parte de la libertad a la que puedes estar acostumbrado cuando desarrollas. Pero aun así ni siquiera lo llamaría una desventaja ya que esto es exactamente lo que hace que Dropwizard sea lo que es—fácil de configurar, fácil de desarrollar, pero aún un marco REST muy robusto y de alto rendimiento.

En mi opinión, la adición de complejidad al framework mediante el apoyo de más y más bibliotecas de terceras partes también introduciría una complejidad innecesaria en el desarrollo.

Este tutorial de Java fue escrito por Dusan Simonovic. Originalmente publicado en Toptal.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.


Recibe los trucos más ocultos de tecnología 🤫

Aprende trucos como la técnica 'correo+1' para recibir correos en tu misma cuenta principal. ¡Únete ahora y accede a información exclusiva!

¡No hacemos spam! Lee nuestra política de privacidad para obtener más información.


Puede que también te interese