Un Comienzo con Microservicios: Un Tutorial de Dropwizard
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.
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 DBGET /part/{id}
to get a particular part from DBPOST /parts
-to create new partPUT /parts/{id}
-to edit an existing partDELETE /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 servlet Jetty 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 actual 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.
UNDERSTANDING THE BASICS
What is the meaning of representational state transfer (REST)?
What is a REST end point?
What is a REST web service?
What is a microservice architecture?
Por: Dusan Simonovic
Articulo vÃa: Toptal
No hay comentarios: