Introducción a los microservicios: un tutorial de Dropwizard

Publicado: 2022-03-11

Todos estamos presenciando 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 construir servicios web RESTful o, más precisamente, un conjunto de herramientas y marcos para construir servicios web RESTful.

Permite a los desarrolladores un arranque rápido del proyecto. Esto lo ayuda a empaquetar sus aplicaciones para que puedan implementarse fácilmente en un entorno de producción como servicios independientes. Si alguna vez ha estado en una situación en la que necesita iniciar un proyecto en el marco Spring, por ejemplo, probablemente sepa lo doloroso que puede ser.

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

Con Dropwizard, solo es cuestión de agregar una dependencia de Maven.

En este blog, lo guiaré a través del proceso completo para escribir un servicio RESTful de Dropwizard simple. Una vez que hayamos terminado, tendremos un servicio para operaciones CRUD básicas en "partes". Realmente no importa qué es "parte"; puede ser cualquier cosa. Primero me vino a la mente.

Almacenaremos los datos en una base de datos MySQL, usando JDBI para consultarlos, y usaremos los siguientes puntos finales:

  • GET /parts -para recuperar todas las partes de la base de datos
  • GET /part/{id} para obtener una parte particular de DB
  • POST /parts -para crear una nueva pieza
  • PUT /parts/{id} -para editar una pieza existente
  • DELETE /parts/{id} -para eliminar la parte de una base de datos

Usaremos OAuth para autenticar nuestro servicio y, finalmente, le agregaremos algunas pruebas unitarias.

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 con Dropwizard por defecto:

  • Embarcadero: necesitaría HTTP para ejecutar una aplicación web. Dropwizard incorpora el contenedor de servlet Jetty para ejecutar aplicaciones web. En lugar de implementar sus aplicaciones en un servidor de aplicaciones o un servidor web, Dropwizard define un método principal que invoca el servidor Jetty como un proceso independiente. A partir de ahora, Dropwizard recomienda ejecutar la aplicación solo con Jetty; otros servicios web como Tomcat no son oficialmente compatibles.
  • Jersey: Jersey es una de las mejores implementaciones de API REST del mercado. Además, sigue la especificación JAX-RS estándar y es la implementación de referencia para la especificación JAX-RS. Dropwizard usa Jersey como marco predeterminado para crear aplicaciones web RESTful.
  • Jackson: Jackson es el estándar de facto para el manejo del formato JSON. Es una de las mejores API de mapeo 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 puntos finales HTTP.
  • Guava: además de estructuras de datos inmutables 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 Moustache: elegir motores de plantilla para su aplicación es una de las decisiones clave. El motor de plantillas elegido debe ser más flexible para escribir mejores scripts. Dropwizard utiliza los motores de plantillas conocidos y populares Freemarker y Mustache para crear las interfaces de usuario.

Además de la lista anterior, hay muchas otras bibliotecas como Joda Time, Liquibase, Apache HTTP Client y Hibernate Validator utilizadas por Dropwizard para crear servicios REST.

Configuración experta

Dropwizard admite oficialmente a Maven. Incluso si puede usar otras herramientas de compilación, la mayoría de las guías y la documentación usan Maven, por lo que también lo usaremos aquí. Si no está familiarizado con Maven, puede consultar este tutorial de Maven.

Este es el primer paso para crear su aplicación Dropwizard. Agregue 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 agregar la entrada anterior, puede agregar dropwizard.version como se muestra a continuación:

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

Eso es todo. Ya terminó de escribir la configuración de Maven. Esto descargará todas las dependencias necesarias para su proyecto. La versión actual de Dropwizard es 1.1.0, por lo que la usaremos 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. Deberá tener el archivo configuration.yml en la carpeta raíz de su aplicación. Luego, este archivo se deserializará a una instancia de la clase de configuración de su aplicación y se validará. El archivo de configuración de su aplicación es la subclase de la clase de configuración de Dropwizard ( io.dropwizard.Configuration ).

Vamos a crear una clase de 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 deberíamos ir y crear la clase de aplicación principal. Esta clase reunirá todos los paquetes, activará la aplicación y la pondrá en funcionamiento para su uso.

Aquí hay 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 realmente se hizo arriba es anular el método de ejecución de Dropwizard. En este método, instanciamos una conexión de base de datos, registramos nuestra comprobación de estado personalizada (hablaremos de ello más adelante), inicializamos la autenticación OAuth para nuestro servicio y, por último, registramos un recurso de Dropwizard.

Todo esto se explicará más adelante.

Definir una clase de representación

Ahora tenemos que empezar a pensar en nuestra API REST 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 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, crearíamos 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 bastante simple.

Definición de una clase de recurso

Un recurso es de lo que se tratan los servicios REST. No es más que un URI de punto final 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 usa la implementación JAX-RS, definiremos la ruta URI usando la anotación @Path .

Aquí hay 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)); } }

Puede ver que todos los puntos finales están realmente definidos en esta clase.

Registro de un recurso

Volvería ahora a la clase de aplicación principal. Puede ver al final de esa clase que hemos registrado nuestro recurso para que se inicialice con la ejecución del servicio. Necesitamos hacerlo con todos los recursos que podamos tener en nuestra aplicación. Este 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 datos, presentaremos una clase de servicio de "capa intermedia". Esta es la clase a la que llamaremos desde nuestra capa de recursos, y no nos importa lo que está subyacente. Es por eso que tenemos esta capa entre las capas de recursos y DAO. Aquí está 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 es en realidad una implementación de verificación de estado, de la que hablaremos más adelante.

Capa DAO, JDBI y Mapper

Dropwizard admite JDBI e Hibernate. Es un módulo Maven separado, así que primero agréguelo 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 servicio CRUD simple, personalmente prefiero JDBI, ya que es más simple y mucho más rápido de implementar. Creé un esquema MySQL simple con una tabla solo para usar en nuestro ejemplo. Puede encontrar el script de inicio para el esquema dentro de la fuente. JDBI ofrece una escritura de consulta simple mediante el uso de anotaciones como @SqlQuery para leer y @SqlUpdate para escribir datos. 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 mapear nuestros conjuntos de resultados de SQL a un modelo, lo que hacemos al registrar una clase de mapeador. Aquí está nuestra clase de mapeador:

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

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

Comprobación de estado de Dropwizard

Dropwizard ofrece soporte nativo para la verificación de estado. En nuestro caso, probablemente nos gustaría verificar si la base de datos está funcionando antes de decir que nuestro servicio está en buen estado. Lo que hacemos en realidad es realizar alguna acción de base de datos simple, como obtener partes de la base de datos y manejar los posibles resultados (exitosos o excepciones).

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

Agregar autenticación

Dropwizard admite autenticación básica y OAuth. Aquí. Le mostraré cómo proteger su servicio con OAuth. Sin embargo, debido a la complejidad, omití una estructura de base de datos subyacente y solo mostré cómo se envuelve. La implementación a gran escala no debería ser un problema a partir de aquí. Dropwizard tiene dos interfaces importantes que debemos implementar.

El primero es el Autenticador. Nuestra clase debe implementar el método de authenticate , que debe verificar si el token de acceso proporcionado es válido. Así que llamaría a esto como una primera puerta a la aplicación. Si tiene éxito, debería devolver un principal. Este principal es nuestro usuario real con su función. El rol es importante para otra interfaz de Dropwizard que necesitamos implementar. Este es Autorizador, y se encarga de comprobar si el usuario tiene suficientes permisos para acceder a un determinado recurso. Entonces, si regresa y verifica nuestra clase de recurso, verá que requiere el rol de administrador para acceder a sus puntos finales. Estas anotaciones también pueden ser por método. El soporte de autorización de Dropwizard es un módulo Maven separado, por lo que debemos agregarlo a las dependencias:

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

Aquí están las clases de nuestro ejemplo que en realidad 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 unitarias en Dropwizard

Agreguemos algunas pruebas unitarias a nuestra aplicación. Me limitaré a probar partes específicas del código de Dropwizard, en nuestro caso Representación y Recurso. Tendremos que 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 muestra para probar. Así que vamos a crear fixtures/part.json en 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 recursos, el punto principal de probar Dropwizard es que en realidad se está comportando como un cliente HTTP, enviando solicitudes HTTP a los recursos. Por lo tanto, no está probando métodos como lo haría normalmente en un caso común. Este es 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> { } }

Cree su aplicación Dropwizard

La mejor práctica es construir el único archivo FAT JAR que contiene todos los archivos .class necesarios para ejecutar su aplicación. El mismo archivo JAR se puede implementar en el entorno diferente desde la prueba hasta la producción sin ningún cambio en las bibliotecas de dependencia. Para comenzar a construir nuestra aplicación de ejemplo como un JAR pesado, debemos configurar un complemento de Maven llamado maven-shade. Debe agregar las siguientes entradas en la sección de complementos de su archivo pom.xml.

Aquí está la configuración de Maven de muestra 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>

Ejecutando su aplicación

Ahora, deberíamos poder ejecutar el servicio. Si ha creado con éxito su archivo JAR, todo lo que necesita hacer es abrir el símbolo del sistema y simplemente ejecutar el siguiente comando para ejecutar su archivo JAR:

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

Si todo salió bien, entonces vería 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 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.

¡Excelente! 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.