マイクロサービスの使用を開始する:Dropwizardチュートリアル
公開: 2022-03-11私たちは皆、マイクロサービスアーキテクチャの人気が高まっているのを目の当たりにしています。 マイクロサービスアーキテクチャでは、Dropwizardが非常に重要な場所を占めています。 これは、RESTful Webサービスを構築するためのフレームワークであり、より正確には、RESTfulWebサービスを構築するためのツールとフレームワークのセットです。
これにより、開発者はプロジェクトをすばやくブートストラップできます。 これにより、アプリケーションをパッケージ化して、スタンドアロンサービスとして本番環境に簡単にデプロイできるようになります。 たとえば、Springフレームワークでプロジェクトをブートストラップする必要がある状況にあったことがある場合は、それがどれほど苦痛であるかをおそらくご存知でしょう。
Dropwizardを使用すると、Mavenの依存関係を1つ追加するだけです。
このブログでは、簡単なDropwizardRESTfulサービスを作成する完全なプロセスについて説明します。 完了したら、「パーツ」の基本的なCRUD操作のサービスを提供します。 「パーツ」が何であるかは実際には重要ではありません。 何でもかまいません。 それが最初に頭に浮かんだ。
データのクエリにJDBIを使用して、データをMySQLデータベースに保存し、次のエンドポイントを使用します。
-
GET /parts
DBからすべてのパーツを取得します GET /part/{id}
を使用して、DBから特定のパーツを取得しますPOST /parts
を作成しますPUT /parts/{id}
-既存のパーツを編集しますDELETE /parts/{id}
をDBから削除します
OAuthを使用してサービスを認証し、最後にいくつかの単体テストを追加します。
デフォルトのDropwizardライブラリ
RESTサービスを個別に構築し、それぞれを構成するために必要なすべてのライブラリを含める代わりに、Dropwizardがそれを行います。 Dropwizardにデフォルトで付属しているライブラリのリストは次のとおりです。
- Jetty: Webアプリケーションを実行するにはHTTPが必要です。 Dropwizardは、Webアプリケーションを実行するためのJettyサーブレットコンテナを埋め込みます。 Dropwizardは、アプリケーションをアプリケーションサーバーまたはWebサーバーにデプロイする代わりに、Jettyサーバーをスタンドアロンプロセスとして呼び出すメインメソッドを定義します。 現在のところ、DropwizardはJettyでのみアプリケーションを実行することを推奨しています。 Tomcatのような他のWebサービスは公式にはサポートされていません。
- ジャージー:ジャージーは、市場で最高のRESTAPI実装の1つです。 また、標準のJAX-RS仕様に準拠しており、JAX-RS仕様のリファレンス実装です。 Dropwizardは、RESTfulWebアプリケーションを構築するためのデフォルトのフレームワークとしてJerseyを使用します。
- ジャクソン:ジャクソンは、JSON形式処理の事実上の標準です。 これは、JSON形式に最適なオブジェクトマッパーAPIの1つです。
- メトリクス: Dropwizardには、HTTPエンドポイントを介してアプリケーションメトリクスを公開するための独自のメトリクスモジュールがあります。
- Guava:高度に最適化された不変のデータ構造に加えて、GuavaはJavaでの開発をスピードアップするためにますます多くのクラスを提供しています。
- LogbackとSlf4j:これら2つは、より優れたロギングメカニズムに使用されます。
- FreemarkerとMustache:アプリケーション用のテンプレートエンジンを選択することは、重要な決定の1つです。 選択したテンプレートエンジンは、より優れたスクリプトを作成するために、より柔軟である必要があります。 Dropwizardは、ユーザーインターフェイスの構築に、よく知られた人気のあるテンプレートエンジンであるFreemarkerとMustacheを使用しています。
上記のリストとは別に、Joda Time、Liquibase、Apache HTTPクライアント、RESTサービスを構築するためにDropwizardによって使用されるHibernateValidatorなどの他の多くのライブラリがあります。
Maven構成
DropwizardはMavenを公式にサポートしています。 他のビルドツールを使用できる場合でも、ほとんどのガイドとドキュメントはMavenを使用しているため、ここでも使用します。 Mavenに慣れていない場合は、このMavenチュートリアルを確認してください。
これは、Dropwizardアプリケーションを作成するための最初のステップです。 Mavenのpom.xml
ファイルに次のエントリを追加してください。
<dependencies> <dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-core</artifactId> <version>${dropwizard.version}</version> </dependency> </dependencies>
上記のエントリを追加する前に、 dropwizard.version
を次のように追加できます。
<properties> <dropwizard.version>1.1.0</dropwizard.version> </properties>
それでおしまい。 これで、Maven構成の作成が完了しました。 これにより、必要なすべての依存関係がプロジェクトにダウンロードされます。 現在のDropwizardのバージョンは1.1.0なので、このガイドで使用します。
これで、最初の実際のDropwizardアプリケーションの作成に進むことができます。
構成クラスの定義
Dropwizardは設定をYAMLファイルに保存します。 アプリケーションのルートフォルダーにconfiguration.yml
ファイルが必要です。 次に、このファイルはアプリケーションの構成クラスのインスタンスに逆シリアル化され、検証されます。 アプリケーションの構成ファイルは、Dropwizardの構成クラス( io.dropwizard.Configuration
)のサブクラスです。
簡単な構成クラスを作成しましょう。
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構成ファイルは次のようになります。
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
上記のクラスはYAMLファイルから逆シリアル化され、YAMLファイルの値がこのオブジェクトに配置されます。
アプリケーションクラスを定義する
次に、メインアプリケーションクラスを作成します。 このクラスは、すべてのバンドルをまとめてアプリケーションを起動し、使用できるようにします。
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))); } }
上記で実際に行われていることは、Dropwizard実行メソッドをオーバーライドすることです。 この方法では、DB接続をインスタンス化し、カスタムヘルスチェックを登録し(後で説明します)、サービスのOAuth認証を初期化し、最後にDropwizardリソースを登録します。
これらはすべて後で説明します。
表現クラスを定義する
次に、REST APIと、リソースの表現について考え始める必要があります。 JSON形式と、目的のJSON形式に変換する対応する表現クラスを設計する必要があります。
この単純な表現クラスの例のサンプルJSON形式を見てみましょう。
{ "code": 200, "data": { "id": 1, "name": "Part 1", "code": "PART_1_CODE" } }
上記のJSON形式の場合、次のように表現クラスを作成します。
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; } }
これはかなり単純なPOJOです。
リソースクラスの定義
リソースとは、RESTサービスのすべてです。 これは、サーバー上のリソースにアクセスするためのエンドポイントURIに他なりません。 この例では、リクエストURIマッピング用のアノテーションがほとんどないリソースクラスがあります。 @Path
はJAX-RS実装を使用するため、@Pathアノテーションを使用してURIパスを定義します。
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)); } }
すべてのエンドポイントが実際にこのクラスで定義されていることがわかります。
リソースの登録
ここで、メインのアプリケーションクラスに戻ります。 そのクラスの最後に、サービスの実行で初期化するリソースを登録したことがわかります。 アプリケーションにある可能性のあるすべてのリソースを使用してこれを行う必要があります。 これは、その原因となるコードスニペットです。
// Register resources environment.jersey().register(new PartsResource(dbi.onDemand(PartsService.class)));
サービスレイヤー
適切な例外処理とデータストレージエンジンからの独立性のために、「中間層」サービスクラスを導入します。 これは、リソースレイヤーから呼び出すクラスであり、基になるものは関係ありません。 そのため、リソースレイヤーとDAOレイヤーの間にこのレイヤーがあります。 これが私たちのサービスクラスです:
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(); } } }
その最後の部分は、実際にはヘルスチェックの実装です。これについては後で説明します。
DAOレイヤー、JDBI、およびマッパー
DropwizardはJDBIとHibernateをサポートしています。 これは別個のMavenモジュールなので、最初に依存関係と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>
単純なCRUDサービスの場合、実装がより簡単ではるかに高速であるため、私は個人的にJDBIを好みます。 この例で使用するテーブルが1つだけの単純なMySQLスキーマを作成しました。 ソース内にスキーマのinitスクリプトがあります。 JDBIは、データの読み取りに@SqlQuery、書き込みに@SqlUpdateなどのアノテーションを使用して、簡単なクエリ書き込みを提供します。 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(); }
ご覧のとおり、これはかなり簡単です。 ただし、SQL結果セットをモデルにマップする必要があります。これは、マッパークラスを登録することによって行います。 マッパークラスは次のとおりです。
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)); } }
そして私たちのモデル:
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ヘルスチェック
Dropwizardは、ヘルスチェックのネイティブサポートを提供します。 私たちの場合、サービスが正常であると言う前に、データベースが稼働しているかどうかを確認したいと思います。 私たちが実際に行うことは、DBからパーツを取得し、潜在的な結果(成功または例外)を処理するなど、いくつかの単純なDBアクションを実行することです。
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); } } }
認証の追加
Dropwizardは基本認証とOAuthをサポートしています。 ここ。 OAuthでサービスを保護する方法を紹介します。 ただし、複雑さのために、基礎となるDB構造を省略し、それがどのようにラップされるかを示しました。 ここから本格的に実装することは問題ではありません。 Dropwizardには、実装する必要のある2つの重要なインターフェースがあります。

最初のものはオーセンティケーターです。 クラスはauthenticate
メソッドを実装する必要があります。このメソッドは、指定されたアクセストークンが有効なものであるかどうかをチェックする必要があります。 したがって、これをアプリケーションへの最初のゲートと呼びます。 成功した場合は、プリンシパルを返す必要があります。 このプリンシパルは、その役割を持つ実際のユーザーです。 この役割は、実装する必要のある別のDropwizardインターフェースにとって重要です。 これはAuthorizerであり、ユーザーが特定のリソースにアクセスするための十分な権限を持っているかどうかを確認する責任があります。 したがって、戻ってリソースクラスを確認すると、エンドポイントにアクセスするために管理者ロールが必要であることがわかります。 これらのアノテーションは、メソッドごとにすることもできます。 Dropwizard認証サポートは別個のMavenモジュールであるため、依存関係に追加する必要があります。
<dependency> <groupId>io.dropwizard</groupId> <artifactId>dropwizard-auth</artifactId> <version>${dropwizard.version}</version> </dependency>
これがこの例のクラスで、実際には何も賢くはありませんが、フルスケールの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; } }
Dropwizardでのユニットテスト
アプリケーションにいくつかの単体テストを追加しましょう。 私は、コードのDropwizard固有の部分、この場合は表現とリソースのテストに固執します。 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>
表現をテストするために、テストするサンプルJSONファイルも必要になります。 それでは、 src/test/resources
の下にfixtures/part.json
を作成しましょう:
{ "id": 1, "name": "testPartName", "code": "testPartCode" }
そして、これが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()); } }
リソースのテストに関して、Dropwizardをテストする主なポイントは、実際にはHTTPクライアントとして動作し、リソースに対してHTTP要求を送信しているということです。 したがって、一般的なケースで通常行うように、メソッドをテストしているわけではありません。 これが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> { } }
Dropwizardアプリケーションを構築する
ベストプラクティスは、アプリケーションの実行に必要なすべての.classファイルを含む単一のFATJARファイルを作成することです。 依存関係ライブラリを変更せずに、テストから本番環境まで、同じJARファイルを異なる環境にデプロイできます。 サンプルアプリケーションのファットJARとしての構築を開始するには、maven-shadeと呼ばれるMavenプラグインを構成する必要があります。 pom.xmlファイルのプラグインセクションに次のエントリを追加する必要があります。
これは、JARファイルをビルドするためのサンプルMaven構成です。
<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>
アプリケーションの実行
これで、サービスを実行できるようになります。 JARファイルの作成に成功した場合は、コマンドプロンプトを開き、次のコマンドを実行してJARファイルを実行するだけです。
java -jar target/dropwizard-blog-1.0.0.jar server configuration.yml
すべてがうまくいけば、次のようなものが表示されます。
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.
優れた! 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.
リソースへのアクセス
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.