Erste Schritte mit Microservices: Ein Dropwizard-Tutorial
Veröffentlicht: 2022-03-11Wir alle erleben einen Anstieg der Popularität von Microservice-Architekturen. In einer Microservice-Architektur nimmt Dropwizard einen sehr wichtigen Platz ein. Es ist ein Framework zum Erstellen von RESTful-Webdiensten oder genauer gesagt eine Reihe von Tools und Frameworks zum Erstellen von RESTful-Webdiensten.
Es ermöglicht Entwicklern ein schnelles Projekt-Bootstrapping. Dies hilft Ihnen, Ihre Anwendungen so zu verpacken, dass sie in einer Produktionsumgebung einfach als eigenständige Dienste bereitgestellt werden können. Wenn Sie jemals in einer Situation waren, in der Sie beispielsweise ein Projekt im Spring-Framework booten mussten, wissen Sie wahrscheinlich, wie schmerzhaft dies sein kann.
Bei Dropwizard muss lediglich eine Maven-Abhängigkeit hinzugefügt werden.
In diesem Blog werde ich Sie durch den gesamten Prozess des Schreibens eines einfachen Dropwizard RESTful-Dienstes führen. Nachdem wir fertig sind, werden wir einen Dienst für grundlegende CRUD-Operationen auf „Teilen“ haben. Es spielt keine Rolle, was „Teil“ ist; es kann alles sein. Es kam mir nur zuerst in den Sinn.
Wir speichern die Daten in einer MySQL-Datenbank, verwenden JDBI zum Abfragen und verwenden die folgenden Endpunkte:
-
GET /parts
- um alle Teile aus der DB abzurufen -
GET /part/{id}
um einen bestimmten Teil von DB zu erhalten -
POST /parts
-um ein neues Teil zu erstellen -
PUT /parts/{id}
-um einen bestehenden Teil zu bearbeiten -
DELETE /parts/{id}
- um den Teil aus einer DB zu löschen
Wir werden OAuth verwenden, um unseren Dienst zu authentifizieren, und schließlich einige Einheitentests hinzufügen.
Standard-Dropwizard-Bibliotheken
Anstatt alle Bibliotheken, die zum Erstellen eines REST-Dienstes erforderlich sind, separat einzuschließen und jede von ihnen zu konfigurieren, erledigt Dropwizard dies für uns. Hier ist die Liste der Bibliotheken, die standardmäßig mit Dropwizard geliefert werden:
- Jetty: Sie würden HTTP zum Ausführen einer Webanwendung benötigen. Dropwizard bettet den Jetty-Servlet-Container zum Ausführen von Webanwendungen ein. Anstatt Ihre Anwendungen auf einem Anwendungsserver oder Webserver bereitzustellen, definiert Dropwizard eine Hauptmethode, die den Jetty-Server als eigenständigen Prozess aufruft. Ab sofort empfiehlt Dropwizard, die Anwendung nur mit Jetty auszuführen; andere Webdienste wie Tomcat werden nicht offiziell unterstützt.
- Jersey: Jersey ist eine der besten REST-API-Implementierungen auf dem Markt. Außerdem folgt es der standardmäßigen JAX-RS-Spezifikation und ist die Referenzimplementierung für die JAX-RS-Spezifikation. Dropwizard verwendet Jersey als Standard-Framework zum Erstellen von RESTful-Webanwendungen.
- Jackson: Jackson ist der De-facto-Standard für die Handhabung des JSON-Formats. Es ist eine der besten Object-Mapper-APIs für das JSON-Format.
- Metriken: Dropwizard verfügt über ein eigenes Metrikmodul zum Offenlegen der Anwendungsmetriken über HTTP-Endpunkte.
- Guava: Neben hochgradig optimierten unveränderlichen Datenstrukturen bietet Guava eine wachsende Anzahl von Klassen, um die Entwicklung in Java zu beschleunigen.
- Logback und Slf4j: Diese beiden werden für bessere Protokollierungsmechanismen verwendet.
- Freemarker und Moustache: Die Auswahl von Template-Engines für Ihre Anwendung ist eine der wichtigsten Entscheidungen. Die gewählte Template-Engine muss flexibler sein, um bessere Skripte schreiben zu können. Dropwizard verwendet die bekannten und beliebten Template-Engines Freemarker und Moustache zum Erstellen der Benutzeroberflächen.
Abgesehen von der obigen Liste gibt es viele andere Bibliotheken wie Joda Time, Liquibase, Apache HTTP Client und Hibernate Validator, die von Dropwizard zum Erstellen von REST-Diensten verwendet werden.
Maven-Konfiguration
Dropwizard unterstützt offiziell Maven. Auch wenn Sie andere Build-Tools verwenden können, verwenden die meisten Leitfäden und Dokumentationen Maven, also werden wir es auch hier verwenden. Wenn Sie mit Maven nicht vertraut sind, können Sie sich dieses Maven-Tutorial ansehen.
Dies ist der erste Schritt bei der Erstellung Ihrer Dropwizard-Anwendung. Bitte fügen Sie den folgenden Eintrag in die pom.xml
-Datei Ihres Maven ein:
<dependencies> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-core</artifactId> <version>${dropwizard.version}</version> </dependency> </dependencies>
Bevor Sie den obigen Eintrag hinzufügen, können Sie die dropwizard.version
wie folgt hinzufügen:
<properties> <dropwizard.version>1.1.0</dropwizard.version> </properties>
Das ist es. Sie haben die Maven-Konfiguration fertig geschrieben. Dadurch werden alle erforderlichen Abhängigkeiten in Ihr Projekt heruntergeladen. Die aktuelle Dropwizard-Version ist 1.1.0, daher werden wir sie in diesem Handbuch verwenden.
Jetzt können wir unsere erste echte Dropwizard-Anwendung schreiben.
Konfigurationsklasse definieren
Dropwizard speichert Konfigurationen in YAML-Dateien. Sie müssen die Datei configuration.yml
in Ihrem Anwendungsstammordner haben. Diese Datei wird dann in eine Instanz der Konfigurationsklasse Ihrer Anwendung deserialisiert und validiert. Die Konfigurationsdatei Ihrer Anwendung ist die Unterklasse der Konfigurationsklasse des Dropwizard ( io.dropwizard.Configuration
).
Lassen Sie uns eine einfache Konfigurationsklasse erstellen:
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; } }
Die YAML-Konfigurationsdatei würde folgendermaßen aussehen:
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
Die obige Klasse wird aus der YAML-Datei deserialisiert und legt die Werte aus der YAML-Datei in diesem Objekt ab.
Definieren Sie eine Anwendungsklasse
Wir sollten jetzt die Hauptanwendungsklasse erstellen. Diese Klasse bringt alle Bundles zusammen und bringt die Anwendung zum Laufen und zur Verwendung.
Hier ist ein Beispiel für eine Anwendungsklasse 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))); } }
Was oben tatsächlich getan wird, ist das Überschreiben der Dropwizard-Run-Methode. Bei dieser Methode instanziieren wir eine DB-Verbindung, registrieren unsere benutzerdefinierte Zustandsprüfung (darüber sprechen wir später), initialisieren die OAuth-Authentifizierung für unseren Dienst und registrieren schließlich eine Dropwizard-Ressource.
All dies wird später erklärt.
Definieren Sie eine Repräsentationsklasse
Jetzt müssen wir anfangen, über unsere REST-API nachzudenken und wie unsere Ressource dargestellt werden soll. Wir müssen das JSON-Format und die entsprechende Darstellungsklasse entwerfen, die in das gewünschte JSON-Format konvertiert.
Sehen wir uns das JSON-Beispielformat für dieses einfache Beispiel einer Repräsentationsklasse an:
{ "code": 200, "data": { "id": 1, "name": "Part 1", "code": "PART_1_CODE" } }
Für das obige JSON-Format würden wir die Repräsentationsklasse wie folgt erstellen:
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; } }
Das ist ziemlich einfaches POJO.
Definieren einer Ressourcenklasse
Eine Ressource ist das, worum es bei REST-Diensten geht. Es ist nichts anderes als ein Endpunkt-URI für den Zugriff auf die Ressource auf dem Server. In diesem Beispiel haben wir eine Ressourcenklasse mit wenigen Anmerkungen für die Zuordnung von Anforderungs-URIs. Da Dropwizard die JAX-RS-Implementierung verwendet, definieren wir den URI-Pfad mit der Annotation @Path
.
Hier ist eine Ressourcenklasse für unser Dropwizard-Beispiel:
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)); } }
Sie können sehen, dass alle Endpunkte tatsächlich in dieser Klasse definiert sind.
Registrieren einer Ressource
Ich würde jetzt zur Hauptanwendungsklasse zurückkehren. Sie können am Ende dieser Klasse sehen, dass wir unsere Ressource registriert haben, um mit der Dienstausführung initialisiert zu werden. Wir müssen dies mit allen Ressourcen tun, die wir möglicherweise in unserer Anwendung haben. Dies ist das dafür verantwortliche Code-Snippet:
// Register resources environment.jersey().register(new PartsResource(dbi.onDemand(PartsService.class)));
Service-Schicht
Für eine ordnungsgemäße Ausnahmebehandlung und die Fähigkeit, von der Datenspeicher-Engine unabhängig zu sein, werden wir eine „Mid-Layer“-Serviceklasse einführen. Dies ist die Klasse, die wir von unserer Ressourcenschicht aufrufen, und es ist uns egal, was zugrunde liegt. Deshalb haben wir diese Schicht zwischen Ressourcen- und DAO-Schichten. Hier ist unsere Serviceklasse:
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(); } } }
Der letzte Teil davon ist eigentlich eine Health-Check-Implementierung, über die wir später sprechen werden.
DAO-Schicht, JDBI und Mapper
Dropwizard unterstützt JDBI und Hibernate. Es ist ein separates Maven-Modul, also fügen wir es zuerst als Abhängigkeit sowie den MySQL-Connector hinzu:
<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>
Für einen einfachen CRUD-Dienst bevorzuge ich persönlich JDBI, da es einfacher und viel schneller zu implementieren ist. Ich habe ein einfaches MySQL-Schema mit nur einer Tabelle erstellt, die in unserem Beispiel verwendet werden soll. Sie finden das Init-Skript für das Schema in der Quelle. JDBI bietet einfaches Schreiben von Abfragen durch Verwendung von Annotationen wie @SqlQuery zum Lesen und @SqlUpdate zum Schreiben von Daten. Hier ist unsere DAO-Schnittstelle:
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(); }
Wie Sie sehen können, ist es ziemlich einfach. Allerdings müssen wir unsere SQL-Ergebnismengen einem Modell zuordnen, was wir tun, indem wir eine Mapper-Klasse registrieren. Hier ist unsere Mapper-Klasse:
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)); } }
Und unser Modell:

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; } }
Dropwizard Health Check
Dropwizard bietet native Unterstützung für die Zustandsprüfung. In unserem Fall möchten wir wahrscheinlich überprüfen, ob die Datenbank betriebsbereit ist, bevor wir sagen, dass unser Dienst fehlerfrei ist. Was wir tun, ist tatsächlich einige einfache DB-Aktionen wie das Abrufen von Teilen aus der DB und das Behandeln der potenziellen Ergebnisse (erfolgreich oder Ausnahmen).
Hier ist unsere Health-Check-Implementierung 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); } } }
Authentifizierung hinzufügen
Dropwizard unterstützt grundlegende Authentifizierung und OAuth. Hier. Ich zeige Ihnen, wie Sie Ihren Dienst mit OAuth schützen. Aufgrund der Komplexität habe ich jedoch eine zugrunde liegende DB-Struktur weggelassen und nur gezeigt, wie sie umschlossen ist. Die Implementierung in vollem Umfang sollte von hier aus kein Problem mehr darstellen. Dropwizard hat zwei wichtige Schnittstellen, die wir implementieren müssen.
Der erste ist Authenticator. Unsere Klasse sollte die Methode „ authenticate
“ implementieren, die prüfen soll, ob das angegebene Zugriffstoken gültig ist. Ich würde dies also als erstes Tor zur Anwendung bezeichnen. Bei Erfolg sollte ein Prinzipal zurückgegeben werden. Dieser Prinzipal ist unser eigentlicher Benutzer mit seiner Rolle. Die Rolle ist wichtig für eine andere Dropwizard-Schnittstelle, die wir implementieren müssen. Dieser ist Autorisierer und dafür verantwortlich zu prüfen, ob der Benutzer über ausreichende Berechtigungen für den Zugriff auf eine bestimmte Ressource verfügt. Wenn Sie also zurückgehen und unsere Ressourcenklasse überprüfen, werden Sie feststellen, dass für den Zugriff auf ihre Endpunkte die Administratorrolle erforderlich ist. Diese Anmerkungen können auch pro Methode sein. Die Dropwizard-Autorisierungsunterstützung ist ein separates Maven-Modul, daher müssen wir es zu den Abhängigkeiten hinzufügen:
<dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-auth</artifactId> <version>${dropwizard.version}</version> </dependency>
Hier sind die Klassen aus unserem Beispiel, die eigentlich nichts Intelligentes tun, aber ein Skelett für eine vollständige OAuth-Autorisierung sind:
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-Tests im Dropwizard
Fügen wir unserer Anwendung einige Komponententests hinzu. Ich werde beim Testen von Dropwizard-spezifischen Teilen des Codes bleiben, in unserem Fall Representation und Resource. Wir müssen unserer Maven-Datei die folgenden Abhängigkeiten hinzufügen:
<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>
Zum Testen der Darstellung benötigen wir auch eine Beispiel-JSON-Datei zum Testen. Lassen Sie uns also fixtures/part.json
unter src/test/resources
erstellen:
{ "id": 1, "name": "testPartName", "code": "testPartCode" }
Und hier ist die JUnit-Testklasse:
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()); } }
Wenn es um das Testen von Ressourcen geht, besteht der Hauptpunkt beim Testen von Dropwizard darin, dass Sie sich tatsächlich als HTTP-Client verhalten und HTTP-Anforderungen an Ressourcen senden. Sie testen also keine Methoden, wie Sie es normalerweise in einem gewöhnlichen Fall tun würden. Hier ist das Beispiel für unsere PartsResource
-Klasse:
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> { } }
Erstellen Sie Ihre Dropwizard-Anwendung
Es empfiehlt sich, die einzelne FAT-JAR-Datei zu erstellen, die alle .class-Dateien enthält, die zum Ausführen Ihrer Anwendung erforderlich sind. Dieselbe JAR-Datei kann ohne Änderung der Abhängigkeitsbibliotheken in unterschiedlichen Umgebungen vom Testen bis zur Produktion bereitgestellt werden. Um unsere Beispielanwendung als Fat-JAR zu erstellen, müssen wir ein Maven-Plugin namens maven-shade konfigurieren. Sie müssen die folgenden Einträge im Plugin-Abschnitt Ihrer pom.xml-Datei hinzufügen.
Hier ist die Maven-Beispielkonfiguration zum Erstellen der JAR-Datei.
<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>
Ausführen Ihrer Anwendung
Jetzt sollten wir in der Lage sein, den Dienst auszuführen. Wenn Sie Ihre JAR-Datei erfolgreich erstellt haben, müssen Sie nur noch die Eingabeaufforderung öffnen und einfach den folgenden Befehl ausführen, um Ihre JAR-Datei auszuführen:
java -jar target/dropwizard-blog-1.0.0.jar server configuration.yml
Wenn alles geklappt hat, würdest du so etwas sehen:
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.
Exzellent! 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.