开始使用微服务:Dropwizard 教程

已发表: 2022-03-11

我们都目睹了微服务架构的普及。 在微服务架构中,Dropwizard 占据着非常重要的位置。 它是用于构建 RESTful Web 服务的框架,或者更准确地说,是一组用于构建 RESTful Web 服务的工具和框架。

它允许开发人员快速启动项目。 这有助于您将应用程序打包,以便在生产环境中轻松部署为独立服务。 例如,如果您曾经遇到过需要在 Spring 框架中引导项目的情况,那么您可能知道这有多痛苦。

插图:Dropwizard 教程中的微服务示例。

使用 Dropwizard,只需添加一个 Maven 依赖项即可。

在这篇博客中,我将指导您完成编写一个简单的 Dropwizard RESTful 服务的完整过程。 完成后,我们将为“部件”上的基本 CRUD 操作提供服务。 “部分”是什么并不重要。 它可以是任何东西。 它只是首先想到的。

我们将数据存储在 MySQL 数据库中,使用 JDBI 进行查询,并将使用以下端点:

  • GET /parts - 从数据库中检索所有部分
  • GET /part/{id}从数据库中获取特定部分
  • POST /parts - 创建新零件
  • PUT /parts/{id} - 编辑现有部分
  • DELETE /parts/{id} - 从数据库中删除部分

我们将使用 OAuth 来验证我们的服务,最后,向它添加一些单元测试。

默认 Dropwizard 库

Dropwizard 没有包含单独构建 REST 服务所需的所有库并配置它们,而是为我们完成了这些。 以下是 Dropwizard 默认附带的库列表:

  • Jetty:您需要 HTTP 来运行 Web 应用程序。 Dropwizard 嵌入了用于运行 Web 应用程序的 Jetty servlet 容器。 Dropwizard 没有将您的应用程序部署到应用程序服务器或 Web 服务器,而是定义了一个主方法,该方法将 Jetty 服务器作为独立进程调用。 截至目前,Dropwizard 建议仅使用 Jetty 运行应用程序; Tomcat 等其他 Web 服务不受官方支持。
  • Jersey: Jersey 是市场上最好的 REST API 实现之一。 此外,它遵循标准 JAX-RS 规范,是 JAX-RS 规范的参考实现。 Dropwizard 使用 Jersey 作为构建 RESTful Web 应用程序的默认框架。
  • Jackson: Jackson 是 JSON 格式处理的事实标准。 它是 JSON 格式的最佳对象映射器 API 之一。
  • 指标: Dropwizard 有自己的指标模块,用于通过 HTTP 端点公开应用程序指标。
  • Guava:除了高度优化的不可变数据结构外,Guava 还提供了越来越多的类来加速 Java 的开发。
  • Logback 和 Slf4j:这两个用于更好的日志记录机制。
  • Freemarker 和 Mustache:为您的应用程序选择模板引擎是关键决定之一。 选择的模板引擎必须更灵活才能编写更好的脚本。 Dropwizard 使用著名和流行的模板引擎 Freemarker 和 Mustache 来构建用户界面。

除了上面的列表之外,Dropwizard 还使用许多其他库,如 Joda Time、Liquibase、Apache HTTP Client 和 Hibernate Validator 来构建 REST 服务。

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 运行方法。 在这个方法中,我们实例化一个数据库连接,注册我们的自定义健康检查(我们稍后会谈到),为我们的服务初始化 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 映射的少量注释。 由于 Dropwizard 使用 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 和 Mapper

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,因为它实现起来更简单、更快。 我创建了一个简单的 MySQL 模式,其中只有一个表用于我们的示例。 您可以在源代码中找到架构的初始化脚本。 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 为健康检查提供本机支持。 在我们的例子中,我们可能想在说我们的服务是健康的之前检查数据库是否启动并运行。 我们所做的实际上是执行一些简单的数据库操作,例如从数据库中获取部件并处理潜在结果(成功或异常)。

这是我们在 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 保护您的服务。 但是,由于复杂性,我省略了底层数据库结构,只是展示了它是如何包装的。 从这里开始,全面实施应该不是问题。 Dropwizard 有两个我们需要实现的重要接口。

第一个是身份验证器。 我们的类应该实现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 特定部分,在我们的例子中是 Representation 和 Resource。 我们需要将以下依赖项添加到我们的 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 应用程序

最佳实践是构建单个 FAT JAR 文件,其中包含运行应用程序所需的所有 .class 文件。 相同的 JAR 文件可以部署到从测试到生产的不同环境,而无需更改依赖库。 要开始将我们的示例应用程序构建为胖 JAR,我们需要配置一个名为 maven-shade 的 Maven 插件。 您必须在 pom.xml 文件的 plugins 部分中添加以下条目。

这是用于构建 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.

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.