Mikro Hizmetlere Başlayın: Bir Dropwizard Eğitimi
Yayınlanan: 2022-03-11Hepimiz mikro hizmet mimarilerinin popülaritesinde bir artışa tanık oluyoruz. Bir mikro hizmet mimarisinde Dropwizard çok önemli bir yere sahiptir. RESTful web hizmetleri oluşturmak için bir çerçeve veya daha doğrusu RESTful web hizmetleri oluşturmak için bir dizi araç ve çerçevedir.
Geliştiricilerin hızlı proje önyüklemesine olanak tanır. Bu, uygulamalarınızı üretim ortamında bağımsız hizmetler olarak kolayca dağıtılabilecek şekilde paketlemenize yardımcı olur. Örneğin, Spring çerçevesinde bir projeyi yeniden başlatmanız gereken bir durumdaysanız, muhtemelen bunun ne kadar acı verici olabileceğini biliyorsunuzdur.
Dropwizard ile, sadece bir Maven bağımlılığı eklemek meselesi.
Bu blogda, basit bir Dropwizard RESTful hizmeti yazma sürecinin tamamı boyunca size rehberlik edeceğim. Bitirdikten sonra, “parçalar” üzerinde temel CRUD işlemleri için bir hizmetimiz olacak. “Parça”nın ne olduğu gerçekten önemli değil; her şey olabilir. Sadece ilk aklıma geldi.
Verileri sorgulamak için JDBI kullanarak bir MySQL veritabanında saklayacağız ve aşağıdaki uç noktaları kullanacağız:
-
GET /parts
-tüm parçaları DB'den almak için - DB'den belirli bir parçayı almak için
GET /part/{id}
-
POST /parts
-yeni parça oluşturmak için -
PUT /parts/{id}
-mevcut bir parçayı düzenlemek için -
DELETE /parts/{id}
-bir veritabanından parçayı silmek için
Hizmetimizin kimliğini doğrulamak için OAuth'u kullanacağız ve son olarak ona bazı birim testleri ekleyeceğiz.
Varsayılan Dropwizard Kitaplıkları
Dropwizard, bir REST hizmeti oluşturmak için gereken tüm kitaplıkları ayrı ayrı dahil etmek ve her birini yapılandırmak yerine, bunu bizim için yapar. Varsayılan olarak Dropwizard ile birlikte gelen kitaplıkların listesi:
- İskele: Bir web uygulamasını çalıştırmak için HTTP'ye ihtiyacınız olacaktır. Dropwizard, web uygulamalarını çalıştırmak için Jetty sunucu uygulaması kapsayıcısını gömer. Dropwizard, uygulamalarınızı bir uygulama sunucusuna veya web sunucusuna dağıtmak yerine, Jetty sunucusunu bağımsız bir işlem olarak çağıran bir ana yöntem tanımlar. Şu an itibariyle Dropwizard, uygulamanın yalnızca Jetty ile çalıştırılmasını önermektedir; Tomcat gibi diğer web hizmetleri resmi olarak desteklenmemektedir.
- Jersey: Jersey, piyasadaki en iyi REST API uygulamalarından biridir. Ayrıca, standart JAX-RS belirtimini takip eder ve JAX-RS belirtimi için referans uygulamasıdır. Dropwizard, RESTful web uygulamaları oluşturmak için varsayılan çerçeve olarak Jersey'i kullanır.
- Jackson: Jackson, JSON format işleme için fiili standarttır. JSON formatı için en iyi nesne eşleyici API'lerinden biridir.
- Metrikler: Dropwizard, HTTP uç noktaları aracılığıyla uygulama metriklerini göstermek için kendi metrik modülüne sahiptir.
- Guava: Yüksek düzeyde optimize edilmiş değişmez veri yapılarına ek olarak Guava, Java'da geliştirmeyi hızlandırmak için artan sayıda sınıf sağlar.
- Logback ve Slf4j: Bu ikisi daha iyi loglama mekanizmaları için kullanılır.
- Freemarker ve Bıyık: Uygulamanız için şablon motorları seçmek önemli kararlardan biridir. Daha iyi komut dosyaları yazmak için seçilen şablon motorunun daha esnek olması gerekir. Dropwizard, kullanıcı arayüzlerini oluşturmak için iyi bilinen ve popüler şablon motorları Freemarker ve Mustache'i kullanır.
Yukarıdaki listenin dışında, Dropwizard tarafından REST hizmetleri oluşturmak için kullanılan Joda Time, Liquibase, Apache HTTP İstemcisi ve Hibernate Validator gibi birçok kitaplık vardır.
Maven Yapılandırması
Dropwizard resmen Maven'i destekliyor. Diğer oluşturma araçlarını kullanabilseniz bile, kılavuzların ve belgelerin çoğu Maven'i kullanır, bu yüzden onu burada da kullanacağız. Maven'e aşina değilseniz, bu Maven eğitimine göz atabilirsiniz.
Bu, Dropwizard uygulamanızı oluşturmanın ilk adımıdır. Lütfen aşağıdaki girişi Maven'inizin pom.xml
dosyasına ekleyin:
<dependencies> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-core</artifactId> <version>${dropwizard.version}</version> </dependency> </dependencies>
Yukarıdaki girişi eklemeden önce, dropwizard.version
aşağıdaki gibi ekleyebilirsiniz:
<properties> <dropwizard.version>1.1.0</dropwizard.version> </properties>
Bu kadar. Maven yapılandırmasını yazmayı bitirdiniz. Bu, gerekli tüm bağımlılıkları projenize indirecektir. Mevcut Dropwizard sürümü 1.1.0'dır, bu yüzden bu kılavuzu kullanacağız.
Şimdi ilk gerçek Dropwizard uygulamamızı yazmaya geçebiliriz.
Yapılandırma Sınıfını Tanımla
Dropwizard, yapılandırmaları YAML dosyalarında saklar. Uygulama kök klasörünüzde configuration.yml
dosyasına sahip olmanız gerekir. Bu dosya daha sonra uygulamanızın yapılandırma sınıfının bir örneğine seri hale getirilecek ve doğrulanacaktır. Uygulamanızın yapılandırma dosyası, Dropwizard'ın yapılandırma sınıfının ( io.dropwizard.Configuration
) alt sınıfıdır.
Basit bir konfigürasyon sınıfı oluşturalım:
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; } }
YAML yapılandırma dosyası şöyle görünür:
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
Yukarıdaki sınıf, YAML dosyasından seri durumdan çıkarılacak ve YAML dosyasındaki değerleri bu nesneye koyacaktır.
Bir Uygulama Sınıfı Tanımlayın
Şimdi gidip ana uygulama sınıfını oluşturmalıyız. Bu sınıf, tüm paketleri bir araya getirecek ve uygulamayı çalıştıracak ve kullanıma hazır hale getirecektir.
Dropwizard'da bir uygulama sınıfı örneği:
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))); } }
Aslında yukarıda yapılan, Dropwizard çalıştırma yöntemini geçersiz kılmaktır. Bu yöntemde, bir DB bağlantısı başlatıyoruz, özel durum kontrolümüzü kaydediyoruz (bunun hakkında daha sonra konuşacağız), hizmetimiz için OAuth kimlik doğrulamasını başlatıyoruz ve son olarak bir Dropwizard kaynağı kaydediyoruz.
Bunların hepsi ileride anlatılacaktır.
Bir Temsil Sınıfı Tanımlayın
Şimdi REST API'mizi ve kaynağımızın temsilinin ne olacağını düşünmeye başlamalıyız. JSON formatını ve istenen JSON formatına dönüştüren ilgili temsil sınıfını tasarlamamız gerekiyor.
Bu basit temsil sınıfı örneği için örnek JSON formatına bakalım:
{ "code": 200, "data": { "id": 1, "name": "Part 1", "code": "PART_1_CODE" } }
Yukarıdaki JSON formatı için temsil sınıfını aşağıdaki gibi oluştururuz:
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; } }
Bu oldukça basit bir POJO'dur.
Kaynak Sınıfı Tanımlama
Kaynak, REST hizmetlerinin neyle ilgili olduğudur. Sunucudaki kaynağa erişmek için bir uç nokta URI'sinden başka bir şey değildir. Bu örnekte, istek URI eşlemesi için birkaç ek açıklama içeren bir kaynak sınıfımız olacak. Dropwizard JAX-RS uygulamasını kullandığından, URI yolunu @Path
açıklamasını kullanarak tanımlayacağız.
Dropwizard örneğimiz için bir kaynak sınıfı:
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)); } }
Tüm bitiş noktalarının aslında bu sınıfta tanımlandığını görebilirsiniz.
Bir Kaynağı Kaydetme
Şimdi ana uygulama sınıfına geri dönecektim. Hizmet çalıştırması ile başlatılmak üzere kaynağımızı kaydettirdiğimizi o sınıfın sonunda görebilirsiniz. Bunu uygulamamızda sahip olabileceğimiz tüm kaynaklarla yapmamız gerekiyor. Bu, bundan sorumlu kod parçacığıdır:
// Register resources environment.jersey().register(new PartsResource(dbi.onDemand(PartsService.class)));
Hizmet Katmanı
Uygun istisna işleme ve veri depolama motorundan bağımsız olma yeteneği için bir "orta katman" hizmet sınıfı tanıtacağız. Bu, kaynak katmanımızdan çağıracağımız sınıftır ve altında yatanın ne olduğu umurumuzda değildir. Bu yüzden kaynak ve DAO katmanları arasında bu katmana sahibiz. İşte hizmet sınıfımız:
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(); } } }
Son kısmı aslında daha sonra bahsedeceğimiz bir sağlık kontrolü uygulamasıdır.
DAO katmanı, JDBI ve Eşleştirici
Dropwizard, JDBI ve Hibernate'i destekler. Bu ayrı bir Maven modülüdür, bu yüzden önce onu MySQL bağlayıcısının yanı sıra bir bağımlılık olarak ekleyelim:
<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>
Basit bir CRUD hizmeti için, uygulanması daha basit ve çok daha hızlı olduğu için kişisel olarak JDBI'yi tercih ederim. Örneğimizde kullanılmak üzere tek tablolu basit bir MySQL şeması oluşturdum. Şema için başlangıç betiğini kaynak içinde bulabilirsiniz. JDBI, okuma için @SqlQuery ve veri yazmak için @SqlUpdate gibi ek açıklamaları kullanarak basit sorgu yazma sunar. İşte DAO arayüzümüz:
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(); }
Gördüğünüz gibi, oldukça basit. Ancak, bir mapper sınıfı kaydederek yaptığımız SQL sonuç kümelerimizi bir modele eşleştirmemiz gerekiyor. İşte mapper sınıfımız:
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)); } }
Ve modelimiz:
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 Sağlık Kontrolü
Dropwizard, durum denetimi için yerel destek sunar. Bizim durumumuzda, hizmetimizin sağlıklı olduğunu söylemeden önce muhtemelen veritabanının çalışır durumda olup olmadığını kontrol etmek isteriz. Yaptığımız şey, DB'den parçalar almak ve olası sonuçları (başarılı veya istisnalar) ele almak gibi bazı basit DB eylemlerini gerçekleştirmektir.

Dropwizard'daki durum denetimi uygulamamız:
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); } } }
Kimlik Doğrulama Ekleme
Dropwizard, temel kimlik doğrulamayı ve OAuth'u destekler. Burada. Size hizmetinizi OAuth ile nasıl koruyacağınızı göstereceğim. Ancak, karmaşıklık nedeniyle, altta yatan bir DB yapısını çıkardım ve nasıl sarıldığını gösterdim. Tam ölçekte uygulamak, buradan başlayarak bir sorun olmamalıdır. Dropwizard'ın uygulamamız gereken iki önemli arayüzü var.
İlki Kimlik Doğrulayıcıdır. Sınıfımız, verilen erişim belirtecinin geçerli olup olmadığını kontrol etmesi gereken authenticate
yöntemini uygulamalıdır. Bu yüzden bunu uygulamaya ilk kapı olarak adlandırırdım. Başarılı olursa, bir anapara döndürmesi gerekir. Bu müdür, rolü ile gerçek kullanıcımızdır. Rol, uygulamamız gereken başka bir Dropwizard arayüzü için önemlidir. Bu, Yetkilendirendir ve kullanıcının belirli bir kaynağa erişmek için yeterli izne sahip olup olmadığını kontrol etmekten sorumludur. Dolayısıyla, geri dönüp kaynak sınıfımızı kontrol ederseniz, uç noktalarına erişmek için yönetici rolü gerektirdiğini göreceksiniz. Bu açıklamalar yöntem başına da olabilir. Dropwizard yetkilendirme desteği ayrı bir Maven modülüdür, bu yüzden onu bağımlılıklara eklememiz gerekiyor:
<dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-auth</artifactId> <version>${dropwizard.version}</version> </dependency>
Örneğimizdeki, aslında akıllıca bir şey yapmayan, ancak tam ölçekli bir OAuth yetkilendirmesi için bir iskelet olan sınıflar şunlardır:
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; } }
Dropwizard'da Birim Testleri
Uygulamamıza bazı birim testleri ekleyelim. Bizim durumumuzda Temsil ve Kaynak örneğinde, Dropwizard'ın kodun belirli bölümlerini test etmeye devam edeceğim. Maven dosyamıza aşağıdaki bağımlılıkları eklememiz gerekecek:
<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>
Temsili test etmek için, test edilecek örnek bir JSON dosyasına da ihtiyacımız olacak. Öyleyse src/test/resources
altında fixtures/part.json
oluşturalım:
{ "id": 1, "name": "testPartName", "code": "testPartCode" }
Ve işte JUnit test sınıfı:
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()); } }
Kaynakları test etmeye gelince, Dropwizard'ı test etmenin ana noktası, aslında bir HTTP istemcisi gibi davranıp kaynaklara HTTP istekleri göndermenizdir. Yani, genellikle ortak bir durumda yaptığınız gibi yöntemleri test etmiyorsunuz. PartsResource
sınıfımız için örnek:
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> { } }
Dropwizard Uygulamanızı Oluşturun
En iyi uygulama, uygulamanızı çalıştırmak için gereken tüm .class dosyalarını içeren tek FAT JAR dosyasını oluşturmaktır. Aynı JAR dosyası, bağımlılık kitaplıklarında herhangi bir değişiklik olmaksızın testten üretime kadar farklı ortamlara dağıtılabilir. Örnek uygulamamızı fat JAR olarak oluşturmaya başlamak için maven-shade adlı bir Maven eklentisi yapılandırmamız gerekiyor. pom.xml dosyanızın eklentiler bölümüne aşağıdaki girdileri eklemeniz gerekir.
JAR dosyasını oluşturmak için örnek Maven yapılandırması.
<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>
Uygulamanızı Çalıştırma
Şimdi, servisi çalıştırabilmemiz gerekiyor. JAR dosyanızı başarıyla oluşturduysanız, tek yapmanız gereken komut istemini açmak ve JAR dosyanızı yürütmek için aşağıdaki komutu çalıştırmaktır:
java -jar target/dropwizard-blog-1.0.0.jar server configuration.yml
Her şey yolunda giderse, şöyle bir şey görürsünüz:
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.
Harika! 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.