Începeți cu microservicii: un tutorial Dropwizard
Publicat: 2022-03-11Cu toții asistăm la o creștere a popularității arhitecturilor de microservicii. Într-o arhitectură de microservicii, Dropwizard comandă un loc foarte important. Este un cadru pentru construirea de servicii web RESTful sau, mai precis, un set de instrumente și cadre pentru construirea de servicii web RESTful.
Permite dezvoltatorilor pornirea rapidă a proiectelor. Acest lucru vă ajută să vă împachetați aplicațiile pentru a fi ușor de implementat într-un mediu de producție ca servicii autonome. Dacă ați fost vreodată într-o situație în care trebuie să porniți un proiect în cadrul Spring, de exemplu, probabil știți cât de dureros poate fi.
Cu Dropwizard, este doar o chestiune de adăugare a unei dependențe Maven.
În acest blog, vă voi ghida prin procesul complet de scriere a unui serviciu simplu Dropwizard RESTful. După ce terminăm, vom avea un serviciu pentru operațiunile CRUD de bază privind „piese”. Nu contează cu adevărat ce „parte” este; poate fi orice. Doar că mi-a venit în minte mai întâi.
Vom stoca datele într-o bază de date MySQL, folosind JDBI pentru interogarea lor și vom folosi următoarele puncte finale:
-
GET /parts
-pentru a prelua toate piesele din DB -
GET /part/{id}
pentru a obține o anumită parte din DB -
POST /parts
-pentru a crea o parte nouă -
PUT /parts/{id}
-pentru a edita o parte existentă -
DELETE /parts/{id}
-pentru a șterge partea dintr-o bază de date
Vom folosi OAuth pentru a ne autentifica serviciul și, în final, vom adăuga câteva teste unitare la acesta.
Biblioteci implicite Dropwizard
În loc să includă toate bibliotecile necesare pentru a construi un serviciu REST separat și să configureze fiecare dintre ele, Dropwizard face asta pentru noi. Iată lista bibliotecilor care vin implicit cu Dropwizard:
- Debarcader: veți avea nevoie de HTTP pentru a rula o aplicație web. Dropwizard încorporează containerul servlet Jetty pentru rularea aplicațiilor web. În loc să vă implementeze aplicațiile pe un server de aplicații sau pe un server web, Dropwizard definește o metodă principală care invocă serverul Jetty ca proces independent. De acum, Dropwizard recomandă rularea aplicației numai cu Jetty; alte servicii web precum Tomcat nu sunt acceptate oficial.
- Jersey: Jersey este una dintre cele mai bune implementări REST API de pe piață. De asemenea, urmează specificația standard JAX-RS și este implementarea de referință pentru specificația JAX-RS. Dropwizard folosește Jersey ca cadru implicit pentru construirea de aplicații web RESTful.
- Jackson: Jackson este standardul de facto pentru gestionarea formatului JSON. Este unul dintre cele mai bune API-uri de cartografiere a obiectelor pentru formatul JSON.
- Metrici: Dropwizard are propriul său modul de valori pentru expunerea valorilor aplicației prin punctele finale HTTP.
- Guava: Pe lângă structurile de date imuabile extrem de optimizate, Guava oferă un număr tot mai mare de clase pentru a accelera dezvoltarea în Java.
- Logback și Slf4j: Aceste două sunt folosite pentru mecanisme de înregistrare mai bune.
- Freemarker și Mustache: Alegerea motoarelor de șablon pentru aplicația dvs. este una dintre deciziile cheie. Motorul de șablon ales trebuie să fie mai flexibil pentru a scrie scripturi mai bune. Dropwizard folosește motoarele de șabloane bine-cunoscute și populare Freemarker și Mustache pentru a construi interfețele utilizator.
În afară de lista de mai sus, există multe alte biblioteci precum Joda Time, Liquibase, Apache HTTP Client și Hibernate Validator utilizate de Dropwizard pentru construirea de servicii REST.
Configurație Maven
Dropwizard sprijină oficial Maven. Chiar dacă puteți folosi alte instrumente de construcție, majoritatea ghidurilor și documentației folosesc Maven, așa că îl vom folosi și aici. Dacă nu sunteți familiarizat cu Maven, puteți consulta acest tutorial Maven.
Acesta este primul pas în crearea aplicației Dropwizard. Vă rugăm să adăugați următoarea intrare în fișierul pom.xml
al lui Maven:
<dependencies> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-core</artifactId> <version>${dropwizard.version}</version> </dependency> </dependencies>
Înainte de a adăuga intrarea de mai sus, puteți adăuga dropwizard.version
după cum urmează:
<properties> <dropwizard.version>1.1.0</dropwizard.version> </properties>
Asta e. Ai terminat de scris configurația Maven. Acest lucru va descărca toate dependențele necesare în proiectul dvs. Versiunea actuală Dropwizard este 1.1.0, așa că o vom folosi în acest ghid.
Acum, putem trece la scrierea primei noastre aplicații Dropwizard adevărate.
Definiți clasa de configurare
Dropwizard stochează configurațiile în fișiere YAML. Va trebui să aveți fișierul configuration.yml
în folderul rădăcină al aplicației. Acest fișier va fi apoi deserializat într-o instanță a clasei de configurare a aplicației dvs. și validat. Fișierul de configurare al aplicației dvs. este subclasa clasei de configurare a Dropwizard ( io.dropwizard.Configuration
).
Să creăm o clasă de configurare simplă:
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; } }
Fișierul de configurare YAML ar arăta astfel:
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
Clasa de mai sus va fi deserializată din fișierul YAML și va pune valorile din fișierul YAML la acest obiect.
Definiți o clasă de aplicație
Acum ar trebui să mergem și să creăm clasa principală de aplicație. Această clasă va reuni toate pachetele și va aduce aplicația și o va pune în funcțiune pentru utilizare.
Iată un exemplu de clasă de aplicație în 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))); } }
Ceea ce s-a făcut de fapt mai sus este să suprascrieți metoda de rulare Dropwizard. În această metodă, instanțiem o conexiune DB, înregistrăm verificarea personalizată a stării de sănătate (vom vorbi despre asta mai târziu), inițializam autentificarea OAuth pentru serviciul nostru și, în final, înregistrăm o resursă Dropwizard.
Toate acestea vor fi explicate mai târziu.
Definiți o clasă de reprezentare
Acum trebuie să începem să ne gândim la API-ul nostru REST și care va fi reprezentarea resursei noastre. Trebuie să proiectăm formatul JSON și clasa de reprezentare corespunzătoare care se convertește în formatul JSON dorit.
Să ne uităm la exemplul de format JSON pentru acest exemplu de clasă de reprezentare simplă:
{ "code": 200, "data": { "id": 1, "name": "Part 1", "code": "PART_1_CODE" } }
Pentru formatul JSON de mai sus, vom crea clasa de reprezentare după cum urmează:
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; } }
Acesta este POJO destul de simplu.
Definirea unei clase de resurse
O resursă este ceea ce înseamnă serviciile REST. Nu este altceva decât un URI de punct final pentru accesarea resursei de pe server. În acest exemplu, vom avea o clasă de resurse cu câteva adnotări pentru maparea URI a cererii. Deoarece Dropwizard folosește implementarea JAX-RS, vom defini calea URI folosind adnotarea @Path
.
Iată o clasă de resurse pentru exemplul nostru 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)); } }
Puteți vedea că toate punctele finale sunt de fapt definite în această clasă.
Înregistrarea unei resurse
M-aș întoarce acum la clasa principală de aplicații. Puteți vedea la sfârșitul acelei clase că am înregistrat resursa noastră pentru a fi inițializată cu rularea serviciului. Trebuie să facem acest lucru cu toate resursele pe care le avem în aplicația noastră. Acesta este fragmentul de cod responsabil pentru asta:
// Register resources environment.jersey().register(new PartsResource(dbi.onDemand(PartsService.class)));
Stratul de servicii
Pentru o gestionare adecvată a excepțiilor și abilitatea de a fi independent de motorul de stocare a datelor, vom introduce o clasă de servicii „strat intermediar”. Aceasta este clasa pe care o vom apela din stratul nostru de resurse și nu ne interesează ce este subiacent. De aceea avem acest strat între straturile de resurse și DAO. Iată clasa noastră de servicii:
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(); } } }
Ultima parte a acesteia este de fapt o implementare a verificării stării de sănătate, despre care vom vorbi mai târziu.
Stratul DAO, JDBI și Mapper
Dropwizard acceptă JDBI și Hibernate. Este un modul separat Maven, așa că mai întâi să-l adăugăm ca dependență, precum și conectorul 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>
Pentru un serviciu CRUD simplu, personal prefer JDBI, deoarece este mai simplu și mult mai rapid de implementat. Am creat o schemă MySQL simplă cu un singur tabel pentru a fi folosit în exemplul nostru. Puteți găsi scriptul de pornire pentru schemă în sursă. JDBI oferă scriere simplă a interogărilor folosind adnotări precum @SqlQuery pentru citire și @SqlUpdate pentru scrierea datelor. Iată interfața noastră 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(); }
După cum puteți vedea, este destul de simplu. Cu toate acestea, trebuie să mapam seturile noastre de rezultate SQL la un model, ceea ce facem prin înregistrarea unei clase de cartografiere. Iată clasa noastră de cartografiere:
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)); } }
Și modelul nostru:

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; } }
Verificare de sănătate Dropwizard
Dropwizard oferă suport nativ pentru verificarea stării de sănătate. În cazul nostru, probabil că am dori să verificăm dacă baza de date funcționează înainte de a spune că serviciul nostru este sănătos. Ceea ce facem este de fapt să efectuăm niște acțiuni simple DB, cum ar fi obținerea de părți din DB și gestionarea rezultatelor potențiale (reușite sau excepții).
Iată implementarea noastră de verificare a stării de sănătate în 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); } } }
Adăugarea de autentificare
Dropwizard acceptă autentificarea de bază și OAuth. Aici. Vă voi arăta cum să vă protejați serviciul cu OAuth. Cu toate acestea, din cauza complexității, am omis o structură DB de bază și doar am arătat cum este împachetat. Implementarea la scară completă nu ar trebui să fie o problemă începând de aici. Dropwizard are două interfețe importante pe care trebuie să le implementăm.
Primul este Authenticator. Clasa noastră ar trebui să implementeze metoda de authenticate
, care ar trebui să verifice dacă jetonul de acces dat este unul valid. Așa că aș numi asta ca primă poartă către aplicație. Dacă reușește, ar trebui să returneze un principal. Acest principal este utilizatorul nostru real cu rolul său. Rolul este important pentru o altă interfață Dropwizard pe care trebuie să o implementăm. Acesta este Authorizer și este responsabil pentru verificarea dacă utilizatorul are suficiente permisiuni pentru a accesa o anumită resursă. Deci, dacă vă întoarceți și verificați clasa noastră de resurse, veți vedea că necesită rolul de administrator pentru accesarea punctelor sale finale. Aceste adnotări pot fi, de asemenea, pe metodă. Suportul de autorizare Dropwizard este un modul separat Maven, așa că trebuie să îl adăugăm la dependențe:
<dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-auth</artifactId> <version>${dropwizard.version}</version> </dependency>
Iată clasele din exemplul nostru care de fapt nu face nimic inteligent, dar este un schelet pentru o autorizare OAuth la scară completă:
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; } }
Teste unitare în Dropwizard
Să adăugăm câteva teste unitare la aplicația noastră. Voi rămâne să testez anumite părți ale codului Dropwizard, în cazul nostru Reprezentare și resurse. Va trebui să adăugăm următoarele dependențe la fișierul nostru 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>
Pentru testarea reprezentării, vom avea nevoie și de un exemplu de fișier JSON pentru a testa. Deci, să creăm fixtures/part.json
sub src/test/resources
:
{ "id": 1, "name": "testPartName", "code": "testPartCode" }
Și iată clasa de testare 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()); } }
Când vine vorba de testarea resurselor, principalul punct al testării Dropwizard este că de fapt te comporți ca un client HTTP, trimițând cereri HTTP împotriva resurselor. Deci, nu testați metode așa cum ați face de obicei într-un caz obișnuit. Iată un exemplu pentru clasa noastră 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ți aplicația Dropwizard
Cea mai bună practică este să construiți un singur fișier FAT JAR care conține toate fișierele .class necesare pentru a rula aplicația dvs. Același fișier JAR poate fi implementat în mediul diferit de la testare la producție, fără nicio modificare a bibliotecilor de dependență. Pentru a începe să construim aplicația noastră exemplu ca un JAR gras, trebuie să configuram un plugin Maven numit maven-shade. Trebuie să adăugați următoarele intrări în secțiunea de pluginuri a fișierului dumneavoastră pom.xml.
Iată exemplul de configurație Maven pentru construirea fișierului 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>
Rularea aplicației dvs
Acum, ar trebui să putem rula serviciul. Dacă ați creat cu succes fișierul JAR, tot ce trebuie să faceți este să deschideți promptul de comandă și să executați următoarea comandă pentru a executa fișierul dvs. JAR:
java -jar target/dropwizard-blog-1.0.0.jar server configuration.yml
Dacă totul merge bine, atunci ați vedea ceva de genul acesta:
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.
Excelent! 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.