Introdução aos microsserviços: um tutorial do Dropwizard

Publicados: 2022-03-11

Estamos todos testemunhando um aumento na popularidade das arquiteturas de microsserviços. Em uma arquitetura de microsserviços, o Dropwizard comanda um lugar muito importante. É um framework para construção de web services RESTful ou, mais precisamente, um conjunto de ferramentas e frameworks para construção de web services RESTful.

Ele permite que os desenvolvedores inicializem rapidamente o projeto. Isso ajuda você a empacotar seus aplicativos para serem facilmente implementados em um ambiente de produção como serviços independentes. Se você já esteve em uma situação em que precisa inicializar um projeto no framework Spring, por exemplo, provavelmente sabe como isso pode ser doloroso.

Ilustração: Exemplo de microsserviços no tutorial do Dropwizard.

Com o Dropwizard, é apenas uma questão de adicionar uma dependência do Maven.

Neste blog, vou guiá-lo pelo processo completo de criação de um serviço RESTful simples do Dropwizard. Depois que terminarmos, teremos um serviço para operações básicas de CRUD em “peças”. Não importa realmente qual seja a “parte”; pode ser qualquer coisa. Apenas me veio à mente primeiro.

Armazenaremos os dados em um banco de dados MySQL, usando JDBI para consulta, e usaremos os seguintes endpoints:

  • GET /parts - para recuperar todas as partes do banco de dados
  • GET /part/{id} para obter uma parte específica do banco de dados
  • POST /parts - para criar uma nova parte
  • PUT /parts/{id} -para editar uma parte existente
  • DELETE /parts/{id} -para excluir a parte de um banco de dados

Usaremos o OAuth para autenticar nosso serviço e, finalmente, adicionaremos alguns testes de unidade a ele.

Bibliotecas padrão do Dropwizard

Em vez de incluir todas as bibliotecas necessárias para construir um serviço REST separadamente e configurar cada uma delas, o Dropwizard faz isso por nós. Aqui está a lista de bibliotecas que vêm com o Dropwizard por padrão:

  • Jetty: Você precisaria de HTTP para executar um aplicativo da web. O Dropwizard incorpora o contêiner do servlet Jetty para executar aplicativos da web. Em vez de implantar seus aplicativos em um servidor de aplicativos ou servidor web, o Dropwizard define um método principal que invoca o servidor Jetty como um processo autônomo. A partir de agora, o Dropwizard recomenda apenas executar o aplicativo com o Jetty; outros serviços da web como o Tomcat não são oficialmente suportados.
  • Jersey: Jersey é uma das melhores implementações de API REST do mercado. Além disso, segue a especificação JAX-RS padrão e é a implementação de referência para a especificação JAX-RS. O Dropwizard usa Jersey como a estrutura padrão para criar aplicativos Web RESTful.
  • Jackson: Jackson é o padrão de fato para manipulação do formato JSON. É uma das melhores APIs de mapeador de objetos para o formato JSON.
  • Métricas: O Dropwizard possui seu próprio módulo de métricas para expor as métricas do aplicativo por meio de endpoints HTTP.
  • Guava: Além de estruturas de dados imutáveis ​​altamente otimizadas, o Guava fornece um número crescente de classes para acelerar o desenvolvimento em Java.
  • Logback e Slf4j: Esses dois são usados ​​para melhores mecanismos de log.
  • Freemarker e bigode: Escolher mecanismos de modelo para seu aplicativo é uma das principais decisões. O mecanismo de modelo escolhido deve ser mais flexível para escrever scripts melhores. O Dropwizard usa os mecanismos de modelo conhecidos e populares Freemarker e Mustache para construir as interfaces do usuário.

Além da lista acima, existem muitas outras bibliotecas como Joda Time, Liquibase, Apache HTTP Client e Hibernate Validator usadas pelo Dropwizard para construir serviços REST.

Configuração do Maven

O Dropwizard suporta oficialmente o Maven. Mesmo que você possa usar outras ferramentas de compilação, a maioria dos guias e da documentação usa o Maven, então vamos usá-lo aqui também. Se você não estiver familiarizado com o Maven, confira este tutorial do Maven.

Este é o primeiro passo para criar seu aplicativo Dropwizard. Adicione a seguinte entrada no arquivo pom.xml do seu Maven:

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

Antes de adicionar a entrada acima, você pode adicionar o dropwizard.version como abaixo:

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

É isso. Você terminou de escrever a configuração do Maven. Isso fará o download de todas as dependências necessárias para o seu projeto. A versão atual do Dropwizard é 1.1.0, então vamos usá-la neste guia.

Agora, podemos passar a escrever nosso primeiro aplicativo Dropwizard real.

Definir classe de configuração

O Dropwizard armazena configurações em arquivos YAML. Você precisará ter o arquivo configuration.yml na pasta raiz do seu aplicativo. Este arquivo será então desserializado para uma instância da classe de configuração do seu aplicativo e validado. O arquivo de configuração do seu aplicativo é a subclasse da classe de configuração do Dropwizard ( io.dropwizard.Configuration ).

Vamos criar uma classe de configuração simples:

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

O arquivo de configuração YAML ficaria assim:

 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

A classe acima será desserializada do arquivo YAML e colocará os valores do arquivo YAML nesse objeto.

Definir uma classe de aplicativo

Agora devemos ir e criar a classe principal do aplicativo. Esta classe reunirá todos os pacotes e colocará o aplicativo em funcionamento e o colocará em execução para uso.

Aqui está um exemplo de uma classe de aplicativo no 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))); } }

O que é realmente feito acima é substituir o método de execução do Dropwizard. Nesse método, estamos instanciando uma conexão de banco de dados, registrando nossa verificação de integridade personalizada (falaremos sobre isso mais tarde), inicializando a autenticação OAuth para nosso serviço e, finalmente, registrando um recurso Dropwizard.

Tudo isso será explicado mais adiante.

Definir uma classe de representação

Agora temos que começar a pensar na nossa API REST e qual será a representação do nosso recurso. Temos que projetar o formato JSON e a classe de representação correspondente que converte para o formato JSON desejado.

Vejamos o formato JSON de amostra para este exemplo de classe de representação simples:

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

Para o formato JSON acima, criaríamos a classe de representação conforme abaixo:

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

Isso é POJO bastante simples.

Definindo uma classe de recurso

Um recurso é o que os serviços REST são. Não é nada além de um URI de ponto de extremidade para acessar o recurso no servidor. Neste exemplo, teremos uma classe de recurso com poucas anotações para mapeamento de URI de solicitação. Como o Dropwizard usa a implementação JAX-RS, definiremos o caminho do URI usando a anotação @Path .

Aqui está uma classe de recurso para o nosso exemplo 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)); } }

Você pode ver que todos os endpoints estão realmente definidos nesta classe.

Registrando um recurso

Eu voltaria agora para a classe principal do aplicativo. Você pode ver no final dessa classe que registramos nosso recurso para ser inicializado com a execução do serviço. Precisamos fazer isso com todos os recursos que podemos ter em nosso aplicativo. Este é o trecho de código responsável por isso:

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

Camada de Serviço

Para o tratamento adequado de exceções e a capacidade de ser independente do mecanismo de armazenamento de dados, apresentaremos uma classe de serviço de “camada intermediária”. Esta é a classe que chamaremos de nossa camada de recursos e não nos importamos com o que está subjacente. É por isso que temos essa camada entre as camadas de recursos e DAO. Aqui está nossa classe de serviço:

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

A última parte é, na verdade, uma implementação de verificação de integridade, sobre a qual falaremos mais tarde.

Camada DAO, JDBI e Mapeador

O Dropwizard suporta JDBI e Hibernate. É um módulo Maven separado, então vamos primeiro adicioná-lo como uma dependência, bem como o 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 um serviço CRUD simples, pessoalmente prefiro JDBI, pois é mais simples e muito mais rápido de implementar. Eu criei um esquema MySQL simples com apenas uma tabela para ser usada em nosso exemplo. Você pode encontrar o script de inicialização do esquema na origem. O JDBI oferece escrita de consulta simples usando anotações como @SqlQuery para leitura e @SqlUpdate para gravação de dados. Aqui está nossa interface DAO:

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

Como você pode ver, é bastante simples. No entanto, precisamos mapear nossos conjuntos de resultados SQL para um modelo, o que fazemos registrando uma classe mapeadora. Aqui está nossa classe mapeadora:

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

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

Verificação de integridade do Dropwizard

O Dropwizard oferece suporte nativo para verificação de integridade. No nosso caso, provavelmente gostaríamos de verificar se o banco de dados está funcionando antes de dizer que nosso serviço está íntegro. O que fazemos é realmente executar alguma ação simples do banco de dados, como obter partes do banco de dados e lidar com os resultados potenciais (bem-sucedidos ou exceções).

Aqui está nossa implementação de verificação de integridade no 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); } } }

Adicionando autenticação

O Dropwizard suporta autenticação básica e OAuth. Aqui. Mostrarei como proteger seu serviço com OAuth. No entanto, devido à complexidade, omiti uma estrutura de banco de dados subjacente e apenas mostrei como ela é encapsulada. A implementação em grande escala não deve ser um problema a partir daqui. O Dropwizard tem duas interfaces importantes que precisamos implementar.

O primeiro é o Autenticador. Nossa classe deve implementar o método authenticate , que deve verificar se o token de acesso fornecido é válido. Então, eu chamaria isso como um primeiro portão para o aplicativo. Se for bem-sucedido, ele deve retornar um principal. Este principal é nosso usuário real com sua função. A função é importante para outra interface do Dropwizard que precisamos implementar. Este é o Autorizador, e é responsável por verificar se o usuário possui permissões suficientes para acessar determinado recurso. Portanto, se você voltar e verificar nossa classe de recursos, verá que ela requer a função de administrador para acessar seus terminais. Essas anotações também podem ser por método. O suporte à autorização do Dropwizard é um módulo Maven separado, portanto, precisamos adicioná-lo às dependências:

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

Aqui estão as classes do nosso exemplo que na verdade não fazem nada inteligente, mas são um esqueleto para uma autorização OAuth em grande 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; } }

Testes de unidade no Dropwizard

Vamos adicionar alguns testes de unidade ao nosso aplicativo. Vou continuar testando partes específicas do código do Dropwizard, no nosso caso Representação e Recurso. Precisaremos adicionar as seguintes dependências ao nosso arquivo 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 testar a representação, também precisaremos de um arquivo JSON de amostra para testar. Então vamos criar fixtures/part.json em src/test/resources :

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

E aqui está a classe de teste 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()); } }

Quando se trata de testar recursos, o ponto principal de testar o Dropwizard é que você está realmente se comportando como um cliente HTTP, enviando solicitações HTTP contra recursos. Portanto, você não está testando métodos como faria normalmente em um caso comum. Aqui está o exemplo para nossa classe PartsResource :

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

Crie seu aplicativo Dropwizard

A melhor prática é construir o único arquivo FAT JAR que contém todos os arquivos .class necessários para executar seu aplicativo. O mesmo arquivo JAR pode ser implementado no ambiente diferente do teste para a produção sem nenhuma alteração nas bibliotecas de dependência. Para começar a construir nosso aplicativo de exemplo como um JAR gordo, precisamos configurar um plugin Maven chamado maven-shade. Você deve adicionar as seguintes entradas na seção de plugins do seu arquivo pom.xml.

Aqui está o exemplo de configuração do Maven para construir o arquivo 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>

Executando seu aplicativo

Agora, devemos ser capazes de executar o serviço. Se você construiu seu arquivo JAR com sucesso, tudo o que você precisa fazer é abrir o prompt de comando e apenas executar o seguinte comando para executar seu arquivo JAR:

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

Se tudo der certo, você verá algo assim:

 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.