Zacznij korzystać z mikroserwisów: samouczek Dropwizard
Opublikowany: 2022-03-11Wszyscy jesteśmy świadkami wzrostu popularności architektur mikrousług. W architekturze mikroserwisowej Dropwizard zajmuje bardzo ważne miejsce. Jest to framework do budowania usług internetowych zgodnych z REST, a dokładniej zestaw narzędzi i frameworków do budowania usług internetowych zgodnych z REST.
Pozwala programistom na szybkie uruchamianie projektów. Pomaga to spakować aplikacje, aby można je było łatwo wdrożyć w środowisku produkcyjnym jako samodzielne usługi. Jeśli kiedykolwiek byłeś w sytuacji, w której musisz załadować projekt we frameworku Spring, prawdopodobnie wiesz, jak bolesne może to być.
W przypadku Dropwizard to tylko kwestia dodania jednej zależności od Mavena.
W tym blogu przeprowadzę Cię przez cały proces pisania prostej usługi Dropwizard RESTful. Gdy skończymy, będziemy mieli serwis do podstawowych operacji CRUD na „częściach”. Tak naprawdę nie ma znaczenia, czym jest „część”; może to być wszystko. Po prostu przyszło mi to do głowy jako pierwsze.
Przechowamy dane w bazie danych MySQL, używając JDBI do odpytywania i użyjemy następujących punktów końcowych:
-
GET /parts
– aby pobrać wszystkie części z DB -
GET /part/{id}
, aby pobrać konkretną część z DB -
POST /parts
– aby utworzyć nową część -
PUT /parts/{id}
– aby edytować istniejącą część -
DELETE /parts/{id}
– aby usunąć część z bazy danych
Użyjemy OAuth do uwierzytelnienia naszej usługi, a na koniec dodamy do niej kilka testów jednostkowych.
Domyślne biblioteki Dropwizard
Zamiast dołączać wszystkie biblioteki potrzebne do zbudowania usługi REST osobno i konfigurować każdą z nich, Dropwizard robi to za nas. Oto lista bibliotek, które są domyślnie dostarczane z Dropwizard:
- Jetty: Do uruchomienia aplikacji internetowej potrzebny byłby protokół HTTP. Dropwizard osadza kontener serwletów Jetty do uruchamiania aplikacji internetowych. Zamiast wdrażać aplikacje na serwerze aplikacji lub serwerze internetowym, Dropwizard definiuje główną metodę, która wywołuje serwer Jetty jako samodzielny proces. Obecnie Dropwizard zaleca uruchamianie aplikacji tylko z Jetty; inne usługi sieciowe, takie jak Tomcat, nie są oficjalnie obsługiwane.
- Jersey: Jersey to jedna z najlepszych implementacji REST API na rynku. Ponadto jest zgodny ze standardową specyfikacją JAX-RS i jest implementacją referencyjną dla specyfikacji JAX-RS. Dropwizard używa Jersey jako domyślnej platformy do tworzenia aplikacji internetowych zgodnych z REST.
- Jackson: Jackson to de facto standard obsługi formatu JSON. Jest to jeden z najlepszych interfejsów API do mapowania obiektów dla formatu JSON.
- Metryki: Dropwizard ma własny moduł metryk do ujawniania metryk aplikacji za pośrednictwem punktów końcowych HTTP.
- Guava: Oprócz wysoce zoptymalizowanych, niezmiennych struktur danych, Guava zapewnia rosnącą liczbę klas, które przyspieszają rozwój w Javie.
- Logback i Slf4j: Te dwa są używane do lepszych mechanizmów logowania.
- Freemarker and Mustache: Wybór silników szablonów dla Twojej aplikacji to jedna z kluczowych decyzji. Wybrany silnik szablonów musi być bardziej elastyczny, aby pisać lepsze skrypty. Dropwizard wykorzystuje znane i popularne silniki szablonów Freemarker i Mustache do tworzenia interfejsów użytkownika.
Oprócz powyższej listy istnieje wiele innych bibliotek, takich jak Joda Time, Liquibase, Apache HTTP Client i Hibernate Validator, z których korzysta Dropwizard do budowania usług REST.
Konfiguracja Maven
Dropwizard oficjalnie wspiera Mavena. Nawet jeśli możesz używać innych narzędzi do budowania, większość przewodników i dokumentacji używa Mavena, więc użyjemy go również tutaj. Jeśli nie znasz Mavena, możesz zapoznać się z tym samouczkiem Mavena.
To pierwszy krok w tworzeniu aplikacji Dropwizard. Dodaj następujący wpis w pliku pom.xml
Twojego Mavena:
<dependencies> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-core</artifactId> <version>${dropwizard.version}</version> </dependency> </dependencies>
Przed dodaniem powyższego wpisu możesz dodać dropwizard.version
jak poniżej:
<properties> <dropwizard.version>1.1.0</dropwizard.version> </properties>
Otóż to. Skończyłeś pisać konfigurację Mavena. Spowoduje to pobranie wszystkich wymaganych zależności do twojego projektu. Obecna wersja Dropwizard to 1.1.0, więc użyjemy jej w tym przewodniku.
Teraz możemy przejść do pisania naszej pierwszej prawdziwej aplikacji Dropwizard.
Zdefiniuj klasę konfiguracji
Dropwizard przechowuje konfiguracje w plikach YAML. Musisz mieć plik configuration.yml
w katalogu głównym aplikacji. Ten plik zostanie następnie zdeserializowany do instancji klasy konfiguracyjnej aplikacji i zweryfikowany. Plik konfiguracyjny aplikacji jest podklasą klasy konfiguracyjnej Dropwizard ( io.dropwizard.Configuration
).
Stwórzmy prostą klasę konfiguracyjną:
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; } }
Plik konfiguracyjny YAML wyglądałby tak:
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
Powyższa klasa zostanie zdeserializowana z pliku YAML i wstawi wartości z pliku YAML do tego obiektu.
Zdefiniuj klasę aplikacji
Powinniśmy teraz iść i stworzyć główną klasę aplikacji. Ta klasa połączy wszystkie pakiety i uruchomi aplikację i uruchomi ją do użytku.
Oto przykład klasy aplikacji w 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))); } }
To, co faktycznie zostało zrobione powyżej, zastępuje metodę uruchamiania Dropwizard. W tej metodzie tworzymy wystąpienie połączenia z bazą danych, rejestrujemy naszą niestandardową kontrolę kondycji (porozmawiamy o tym później), inicjujemy uwierzytelnianie OAuth dla naszej usługi, a na koniec rejestrujemy zasób Dropwizard.
Wszystko to zostanie wyjaśnione później.
Zdefiniuj klasę reprezentacji
Teraz musimy zacząć myśleć o naszym REST API i o tym, jaka będzie reprezentacja naszego zasobu. Musimy zaprojektować format JSON i odpowiednią klasę reprezentacji, która konwertuje do pożądanego formatu JSON.
Spójrzmy na przykładowy format JSON dla tego prostego przykładu klasy reprezentacji:
{ "code": 200, "data": { "id": 1, "name": "Part 1", "code": "PART_1_CODE" } }
Dla powyższego formatu JSON stworzylibyśmy klasę reprezentacji jak poniżej:
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; } }
To jest dość proste POJO.
Definiowanie klasy zasobów
Zasób to podstawa usług REST. To nic innego jak identyfikator URI punktu końcowego umożliwiający dostęp do zasobu na serwerze. W tym przykładzie będziemy mieli klasę zasobów z kilkoma adnotacjami do mapowania identyfikatora URI żądania. Ponieważ Dropwizard korzysta z implementacji JAX-RS, zdefiniujemy ścieżkę URI za pomocą adnotacji @Path
.
Oto klasa zasobów dla naszego przykładu 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)); } }
Możesz zobaczyć, że wszystkie punkty końcowe są faktycznie zdefiniowane w tej klasie.
Rejestracja zasobu
Wróciłbym teraz do głównej klasy aplikacji. Możesz zobaczyć na końcu tej klasy, że zarejestrowaliśmy nasz zasób do zainicjowania z uruchomieniem usługi. Musimy to zrobić ze wszystkimi zasobami, jakie możemy mieć w naszej aplikacji. Oto fragment kodu odpowiedzialny za to:
// Register resources environment.jersey().register(new PartsResource(dbi.onDemand(PartsService.class)));
Warstwa usług
Aby zapewnić odpowiednią obsługę wyjątków i możliwość niezależności od silnika przechowywania danych, wprowadzimy klasę usług „mid-layer”. To jest klasa, którą będziemy wywoływać z naszej warstwy zasobów i nie obchodzi nas, co jest pod nią. Dlatego mamy tę warstwę pomiędzy warstwami zasobów i DAO. Oto nasza klasa usług:
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(); } } }
Ostatnia część to właściwie wdrożenie kontroli stanu, o której będziemy rozmawiać później.
Warstwa DAO, JDBI i Mapper
Dropwizard obsługuje JDBI i Hibernate. Jest to osobny moduł Maven, więc najpierw dodajmy go jako zależność, a także jako łącznik 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>
W przypadku prostej usługi CRUD osobiście wolę JDBI, ponieważ jest prostsza i znacznie szybsza do wdrożenia. Stworzyłem prosty schemat MySQL z tylko jedną tabelą do wykorzystania w naszym przykładzie. Skrypt startowy dla schematu można znaleźć w źródle. JDBI oferuje proste pisanie zapytań przy użyciu adnotacji, takich jak @SqlQuery do odczytu i @SqlUpdate do zapisywania danych. Oto nasz interfejs 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(); }
Jak widać, jest to dość proste. Jednak musimy zmapować nasze zbiory wyników SQL do modelu, co robimy rejestrując klasę mapowania. Oto nasza klasa maperów:
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)); } }
A nasz model:
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; } }
Kontrola stanu Dropwizard
Dropwizard oferuje natywną obsługę sprawdzania stanu zdrowia. W naszym przypadku prawdopodobnie chcielibyśmy sprawdzić, czy baza danych jest uruchomiona, zanim stwierdzimy, że nasza usługa jest zdrowa. To, co robimy, to w rzeczywistości wykonanie prostej akcji DB, takiej jak pobieranie części z DB i obsługa potencjalnych wyników (pomyślnych lub wyjątków).

Oto nasza implementacja kontroli stanu w 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); } } }
Dodawanie uwierzytelniania
Dropwizard obsługuje uwierzytelnianie podstawowe i OAuth. Tutaj. Pokażę Ci, jak chronić swoją usługę za pomocą OAuth. Jednak ze względu na złożoność pominąłem podstawową strukturę DB i właśnie pokazałem, jak jest opakowana. Wdrożenie na pełną skalę nie powinno być problemem od tego momentu. Dropwizard ma dwa ważne interfejsy, które musimy zaimplementować.
Pierwszym z nich jest Authenticator. Nasza klasa powinna zaimplementować metodę authenticate
, która powinna sprawdzić, czy podany token dostępu jest poprawny. Więc nazwałbym to jako pierwszą bramkę do aplikacji. Jeśli się powiedzie, powinien zwrócić zleceniodawcę. Tym zleceniodawcą jest nasz rzeczywisty użytkownik ze swoją rolą. Rola jest ważna dla innego interfejsu Dropwizard, który musimy zaimplementować. Ten jest Autoryzatorem i jest odpowiedzialny za sprawdzenie, czy użytkownik ma wystarczające uprawnienia, aby uzyskać dostęp do określonego zasobu. Tak więc, jeśli wrócisz i sprawdzisz naszą klasę zasobów, zobaczysz, że wymaga ona roli administratora, aby uzyskać dostęp do swoich punktów końcowych. Te adnotacje mogą być również przypisane do metody. Obsługa autoryzacji Dropwizard to osobny moduł Maven, więc musimy dodać go do zależności:
<dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-auth</artifactId> <version>${dropwizard.version}</version> </dependency>
Oto klasy z naszego przykładu, które w rzeczywistości nie robią nic inteligentnego, ale są szkieletem pełnoskalowej autoryzacji OAuth:
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; } }
Testy jednostkowe w Dropwizard
Dodajmy do naszej aplikacji kilka testów jednostkowych. Pozostanę przy testowaniu poszczególnych części kodu Dropwizarda, w naszym przypadku Reprezentacji i Zasobów. Będziemy musieli dodać następujące zależności do naszego pliku 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>
Do testowania reprezentacji będziemy również potrzebować przykładowego pliku JSON do testowania. Stwórzmy więc fixtures/part.json
w src/test/resources
:
{ "id": 1, "name": "testPartName", "code": "testPartCode" }
A oto klasa testowa 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()); } }
Jeśli chodzi o testowanie zasobów, głównym punktem testowania Dropwizard jest to, że faktycznie zachowujesz się jak klient HTTP, wysyłając żądania HTTP do zasobów. Nie testujesz więc metod, jak zwykle robisz to w typowym przypadku. Oto przykład dla naszej klasy 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> { } }
Zbuduj swoją aplikację Dropwizard
Najlepszą praktyką jest utworzenie pojedynczego pliku FAT JAR, który zawiera wszystkie pliki .class wymagane do uruchomienia aplikacji. Ten sam plik JAR można wdrożyć w różnych środowiskach, od testowania po produkcję, bez żadnych zmian w bibliotekach zależności. Aby zacząć budować naszą przykładową aplikację jako gruby JAR, musimy skonfigurować wtyczkę Maven o nazwie maven-shade. Musisz dodać następujące wpisy w sekcji wtyczek w pliku pom.xml.
Oto przykładowa konfiguracja Maven do budowania pliku 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>
Uruchamianie aplikacji
Teraz powinniśmy być w stanie uruchomić usługę. Jeśli pomyślnie zbudowałeś plik JAR, wystarczy otworzyć wiersz polecenia i po prostu uruchomić następujące polecenie, aby wykonać plik JAR:
java -jar target/dropwizard-blog-1.0.0.jar server configuration.yml
Gdyby wszystko poszło dobrze, zobaczyłbyś coś takiego:
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.
Doskonały! 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.