Inizia con i microservizi: un tutorial su Dropwizard

Pubblicato: 2022-03-11

Stiamo tutti assistendo a un aumento della popolarità delle architetture di microservizi. In un'architettura di microservizi, Dropwizard occupa un posto molto importante. È un framework per la creazione di servizi Web RESTful o, più precisamente, un insieme di strumenti e framework per la creazione di servizi Web RESTful.

Consente agli sviluppatori di avviare rapidamente i progetti. Questo ti aiuta a creare pacchetti delle tue applicazioni per essere facilmente distribuibili in un ambiente di produzione come servizi standalone. Se ti sei mai trovato in una situazione in cui devi avviare un progetto nel framework Spring, ad esempio, probabilmente sai quanto può essere doloroso.

Illustrazione: Esempio di microservizi nel tutorial di Dropwizard.

Con Dropwizard, è solo questione di aggiungere una dipendenza Maven.

In questo blog, ti guiderò attraverso il processo completo di scrittura di un semplice servizio Dropwizard RESTful. Al termine, avremo un servizio per le operazioni CRUD di base sulle "parti". Non importa davvero quale sia la "parte"; può essere qualsiasi cosa. Mi è venuto in mente prima.

Archivieremo i dati in un database MySQL, utilizzando JDBI per interrogarli e utilizzeremo i seguenti endpoint:

  • GET /parts -per recuperare tutte le parti dal DB
  • GET /part/{id} per ottenere una parte particolare dal DB
  • POST /parts -per creare una nuova parte
  • PUT /parts/{id} -per modificare una parte esistente
  • DELETE /parts/{id} -per eliminare la parte da un DB

Utilizzeremo OAuth per autenticare il nostro servizio e, infine, aggiungeremo alcuni unit test.

Librerie predefinite di Dropwizard

Invece di includere tutte le librerie necessarie per creare un servizio REST separatamente e configurarle ciascuna, Dropwizard lo fa per noi. Ecco l'elenco delle librerie fornite con Dropwizard per impostazione predefinita:

  • Jetty: per eseguire un'applicazione Web è necessario HTTP. Dropwizard incorpora il contenitore servlet Jetty per l'esecuzione di applicazioni Web. Invece di distribuire le applicazioni su un server delle applicazioni o un server Web, Dropwizard definisce un metodo principale che richiama il server Jetty come processo autonomo. A partire da ora, Dropwizard consiglia di eseguire l'applicazione solo con Jetty; altri servizi web come Tomcat non sono ufficialmente supportati.
  • Jersey: Jersey è una delle migliori implementazioni API REST sul mercato. Inoltre, segue la specifica JAX-RS standard ed è l'implementazione di riferimento per la specifica JAX-RS. Dropwizard utilizza Jersey come framework predefinito per la creazione di applicazioni Web RESTful.
  • Jackson: Jackson è lo standard de facto per la gestione dei formati JSON. È una delle migliori API di mappatura di oggetti per il formato JSON.
  • Metriche: Dropwizard dispone di un proprio modulo delle metriche per esporre le metriche dell'applicazione tramite gli endpoint HTTP.
  • Guava: oltre a strutture di dati immutabili altamente ottimizzate, Guava fornisce un numero crescente di classi per accelerare lo sviluppo in Java.
  • Logback e Slf4j: questi due sono usati per migliorare i meccanismi di registrazione.
  • Freemarker e Moustache: la scelta dei motori di template per la tua applicazione è una delle decisioni chiave. Il motore del modello scelto deve essere più flessibile per scrivere script migliori. Dropwizard utilizza i noti e popolari motori di template Freemarker e Moustache per creare le interfacce utente.

Oltre all'elenco sopra, ci sono molte altre librerie come Joda Time, Liquibase, Apache HTTP Client e Hibernate Validator utilizzate da Dropwizard per la creazione di servizi REST.

Configurazione Maven

Dropwizard supporta ufficialmente Maven. Anche se puoi utilizzare altri strumenti di compilazione, la maggior parte delle guide e della documentazione utilizza Maven, quindi lo useremo anche qui. Se non hai familiarità con Maven, puoi dare un'occhiata a questo tutorial Maven.

Questo è il primo passo per creare la tua applicazione Dropwizard. Aggiungi la seguente voce nel file pom.xml del tuo Maven:

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

Prima di aggiungere la voce sopra, puoi aggiungere dropwizard.version come di seguito:

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

Questo è tutto. Hai finito di scrivere la configurazione di Maven. Questo scaricherà tutte le dipendenze richieste nel tuo progetto. L'attuale versione di Dropwizard è 1.1.0, quindi la useremo in questa guida.

Ora possiamo passare alla scrittura della nostra prima vera applicazione Dropwizard.

Definisci classe di configurazione

Dropwizard memorizza le configurazioni nei file YAML. Dovrai avere il file configuration.yml nella cartella principale dell'applicazione. Questo file verrà quindi deserializzato in un'istanza della classe di configurazione dell'applicazione e convalidato. Il file di configurazione dell'applicazione è la sottoclasse della classe di configurazione di Dropwizard ( io.dropwizard.Configuration ).

Creiamo una semplice classe di configurazione:

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

Il file di configurazione YAML sarebbe simile a questo:

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

La classe di cui sopra verrà deserializzata dal file YAML e metterà i valori dal file YAML a questo oggetto.

Definire una classe di applicazione

Ora dovremmo andare a creare la classe dell'applicazione principale. Questa classe riunirà tutti i bundle, avvierà l'applicazione e la farà funzionare per l'uso.

Ecco un esempio di una classe di applicazione in 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))); } }

Ciò che è stato effettivamente fatto sopra è sovrascrivere il metodo di esecuzione di Dropwizard. In questo metodo, stiamo istanziando una connessione DB, registrando il nostro controllo di integrità personalizzato (ne parleremo più avanti), inizializzando l'autenticazione OAuth per il nostro servizio e, infine, registrando una risorsa Dropwizard.

Tutti questi saranno spiegati più avanti.

Definire una classe di rappresentazione

Ora dobbiamo iniziare a pensare alla nostra API REST ea quale sarà la rappresentazione della nostra risorsa. Dobbiamo progettare il formato JSON e la classe di rappresentazione corrispondente che converte nel formato JSON desiderato.

Diamo un'occhiata al formato JSON di esempio per questo semplice esempio di classe di rappresentazione:

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

Per il formato JSON sopra, creeremo la classe di rappresentazione come di seguito:

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

Questo è abbastanza semplice POJO.

Definizione di una classe di risorse

Una risorsa è ciò che riguardano i servizi REST. Non è altro che un URI dell'endpoint per l'accesso alla risorsa sul server. In questo esempio, avremo una classe di risorse con poche annotazioni per la mappatura dell'URI della richiesta. Poiché Dropwizard utilizza l'implementazione JAX-RS, definiremo il percorso URI utilizzando l'annotazione @Path .

Ecco una classe di risorse per il nostro esempio 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)); } }

Puoi vedere che tutti gli endpoint sono effettivamente definiti in questa classe.

Registrazione di una risorsa

Vorrei tornare ora alla classe dell'applicazione principale. Puoi vedere alla fine di quella classe che abbiamo registrato la nostra risorsa da inizializzare con l'esecuzione del servizio. Dobbiamo farlo con tutte le risorse che potremmo avere nella nostra applicazione. Questo è lo snippet di codice responsabile di ciò:

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

Livello di servizio

Per una corretta gestione delle eccezioni e la capacità di essere indipendenti dal motore di archiviazione dei dati, introdurremo una classe di servizio di "livello intermedio". Questa è la classe che chiameremo dal nostro livello di risorse e non ci interessa quale sia il sottostante. Ecco perché abbiamo questo livello tra i livelli di risorse e DAO. Ecco la nostra classe di servizio:

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

L'ultima parte è in realtà un'implementazione del controllo dello stato di salute, di cui parleremo più avanti.

Livello DAO, JDBI e Mapper

Dropwizard supporta JDBI e Hibernate. È un modulo Maven separato, quindi aggiungiamolo prima come dipendenza e come connettore 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>

Per un semplice servizio CRUD, personalmente preferisco JDBI, in quanto è più semplice e molto più veloce da implementare. Ho creato un semplice schema MySQL con una sola tabella da utilizzare nel nostro esempio. È possibile trovare lo script init per lo schema all'interno dell'origine. JDBI offre una semplice scrittura di query utilizzando annotazioni come @SqlQuery per la lettura e @SqlUpdate per la scrittura dei dati. Ecco la nostra interfaccia 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(); }

Come puoi vedere, è abbastanza semplice. Tuttavia, dobbiamo mappare i nostri set di risultati SQL su un modello, cosa che facciamo registrando una classe mapper. Ecco la nostra classe di mappatura:

 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 il nostro modello:

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

Controllo dello stato di Dropwizard

Dropwizard offre supporto nativo per il controllo dello stato. Nel nostro caso, probabilmente vorremmo verificare se il database è attivo e funzionante prima di dire che il nostro servizio è integro. Quello che facciamo in realtà è eseguire alcune semplici azioni DB come ottenere parti dal DB e gestire i potenziali risultati (riusciti o eccezioni).

Ecco la nostra implementazione del controllo dello stato in 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); } } }

Aggiunta dell'autenticazione

Dropwizard supporta l'autenticazione di base e OAuth. Qui. Ti mostrerò come proteggere il tuo servizio con OAuth. Tuttavia, a causa della complessità, ho omesso una struttura DB sottostante e ho appena mostrato come è avvolto. L'implementazione su vasta scala non dovrebbe essere un problema a partire da qui. Dropwizard ha due importanti interfacce che dobbiamo implementare.

Il primo è Authenticator. La nostra classe dovrebbe implementare il metodo di authenticate , che dovrebbe verificare se il token di accesso fornito è valido. Quindi lo chiamerei come un primo gate per l'applicazione. In caso di esito positivo, dovrebbe restituire un principal. Questo principale è il nostro utente effettivo con il suo ruolo. Il ruolo è importante per un'altra interfaccia di Dropwizard che dobbiamo implementare. Questo è Authorizer ed è responsabile del controllo se l'utente dispone di autorizzazioni sufficienti per accedere a una determinata risorsa. Quindi, se torni indietro e controlli la nostra classe di risorse, vedrai che richiede il ruolo di amministratore per accedere ai suoi endpoint. Queste annotazioni possono essere anche per metodo. Il supporto per l'autorizzazione di Dropwizard è un modulo Maven separato, quindi è necessario aggiungerlo alle dipendenze:

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

Ecco le classi del nostro esempio che in realtà non fanno nulla di intelligente, ma sono uno scheletro per un'autorizzazione OAuth su vasta scala:

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

Unit test in Dropwizard

Aggiungiamo alcuni unit test alla nostra applicazione. Mi atterrò a testare parti specifiche del codice di Dropwizard, nel nostro caso Rappresentazione e Risorsa. Dovremo aggiungere le seguenti dipendenze al nostro file 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>

Per testare la rappresentazione, avremo anche bisogno di un file JSON di esempio su cui eseguire il test. Quindi creiamo fixtures/part.json in src/test/resources :

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

Ed ecco la classe di test JUnit:

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

Quando si tratta di testare le risorse, il punto principale del testare Dropwizard è che ti stai effettivamente comportando come un client HTTP, inviando richieste HTTP contro risorse. Quindi, non stai testando metodi come faresti di solito in un caso comune. Ecco l'esempio per la nostra 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> { } }

Crea la tua applicazione Dropwizard

La procedura consigliata consiste nel creare il singolo file JAR FAT che contiene tutti i file .class necessari per eseguire l'applicazione. Lo stesso file JAR può essere distribuito in un ambiente diverso dal test alla produzione senza alcuna modifica nelle librerie delle dipendenze. Per iniziare a creare la nostra applicazione di esempio come un JAR grasso, dobbiamo configurare un plug-in Maven chiamato maven-shade. Devi aggiungere le seguenti voci nella sezione dei plugin del tuo file pom.xml.

Ecco la configurazione di esempio di Maven per la creazione del file 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>

Esecuzione della tua applicazione

Ora dovremmo essere in grado di eseguire il servizio. Se hai creato correttamente il tuo file JAR, tutto ciò che devi fare è aprire il prompt dei comandi ed eseguire semplicemente il seguente comando per eseguire il tuo file JAR:

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

Se tutto è andato bene, vedresti qualcosa del genere:

 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.

Eccellente! 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.