个性化阅读
专注于IT技术分析

微服务入门:Dropwizard教程

本文概述

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

它使开发人员可以快速进行项目自举。这可以帮助你打包应用程序, 以便将其作为独立服务轻松部署到生产环境中。例如, 如果你曾经遇到过需要在Spring框架中自举项目的情况, 那么你可能知道它可能会很痛苦。

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

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

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

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

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

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

默认的Dropwizard库

Dropwizard不需要为单独构建REST服务并配置它们而需要的所有库, 而是为我们做到了。这是默认情况下Dropwizard附带的库的列表:

  • 码头:运行Web应用程序需要HTTP。 Dropwizard嵌入了Jetty servlet容器, 用于运行Web应用程序。 Dropwizard定义了一种将Jetty服务器作为独立进程调用的主要方法, 而不是将应用程序部署到应用程序服务器或Web服务器。到目前为止, Dropwizard建议仅使用Jetty运行该应用程序。正式不支持其他Web服务(例如Tomcat)。
  • 泽西岛:泽西岛是市场上最好的REST API实现之一。另外, 它遵循标准的JAX-RS规范, 并且是JAX-RS规范的参考实现。 Dropwizard使用Jersey作为构建RESTful Web应用程序的默认框架。
  • Jackson:Jackson是JSON格式处理的事实上的标准。它是JSON格式的最佳对象映射器API之一。
  • 指标:Dropwizard拥有自己的指标模块, 用于通过HTTP端点公开应用程序指标。
  • Guava:除了高度优化的不可变数据结构外, Guava还提供了越来越多的类来加速Java开发。
  • Logback和Slf4j:这两个用于更好的日志记录机制。
  • Freemarker和Moustache:为你的应用程序选择模板引擎是关键决定之一。所选的模板引擎必须更灵活以编写更好的脚本。 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.srcmini.blog.auth.DropwizardBlogAuthenticator;
import com.srcmini.blog.auth.DropwizardBlogAuthorizer;
import com.srcmini.blog.auth.User;
import com.srcmini.blog.config.DropwizardBlogConfiguration;
import com.srcmini.blog.health.DropwizardBlogApplicationHealthCheck;
import com.srcmini.blog.resource.PartsResource;
import com.srcmini.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.srcmini.blog.model.Part;
import com.srcmini.blog.representation.Representation;
import com.srcmini.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.srcmini.blog.dao.PartsDao;
import com.srcmini.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.srcmini.blog.mapper.PartsMapper;
import com.srcmini.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.srcmini.blog.model.Part;

public class PartsMapper implements ResultSetMapper<Part> {
  private static final String ID = "id";
  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.srcmini.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有两个我们需要实现的重要接口。

第一个是Authenticator。我们的类应实现authenticate方法, 该方法应检查给定的访问令牌是否有效。因此, 我将其称为应用程序的第一步。如果成功, 它应该返回一个主体。该主体是我们实际的用户及其角色。该角色对于我们需要实现的另一个Dropwizard接口很重要。这是授权者, 它负责检查用户是否具有足够的权限来访问特定资源。因此, 如果你返回并检查我们的资源类, 你将看到它需要admin角色才能访问其端点。这些注释也可以按方法使用。 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.srcmini.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 [email protected]{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 [email protected]{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.srcmini.blog.resource.PartsResource)
    POST    /parts (com.srcmini.blog.resource.PartsResource)
    DELETE  /parts/{id} (com.srcmini.blog.resource.PartsResource)
    GET     /parts/{id} (com.srcmini.blog.resource.PartsResource)
    PUT     /parts/{id} (com.srcmini.blog.resource.PartsResource)

INFO  [2017-04-23 22:51:15, 154] org.eclipse.jetty.server.handler.ContextHandler: Started [email protected]{/, 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 [email protected]{/, null, AVAILABLE}
INFO  [2017-04-23 22:51:15, 176] org.eclipse.jetty.server.AbstractConnector: Started [email protected]{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 [email protected]{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

现在, 你拥有自己的Dropwizard应用程序, 它在端口8080上监听应用程序请求, 在8081上监听管理请求。

请注意, 服务器configuration.yml用于启动HTTP服务器并将YAML配置文件位置传递到服务器。

优秀的!最后, 我们使用Dropwizard框架实现了微服务。现在让我们休息一下, 喝杯茶。你做得很好。

访问资源

你可以使用任何HTTP客户端, 例如POSTMAN或其他任何客户端。你应该可以通过单击http:// localhost:8080 / parts访问服务器。你应该会收到一条消息, 要求提供凭据才能访问该服务。要进行身份验证, 请添加带有承载test_token值的Authorization标头。如果成功完成, 你应该看到类似以下内容的信息:

{
  "code": 200, "data": []
}

表示你的数据库为空。通过将HTTP方法从GET切换到POST来创建第一部分, 并提供以下负载:

{
  "name":"My first part", "code":"code_of_my_first_part"
}

所有其他端点均以相同的方式工作, 因此请继续玩乐。

如何更改上下文路径

默认情况下, Dropwizard应用程序将在/中启动并运行。例如, 如果你未提及应用程序的上下文路径, 则默认情况下, 可以从URL http:// localhost:8080 /访问该应用程序。如果你想为应用程序配置自己的上下文路径, 那么请将以下条目添加到你的YAML文件中。

server:
    applicationContextPath: /application

整理我们的Dropwizard教程

现在, 当你启动并运行Dropwizard REST服务时, 让我们总结一下将Dropwizard用作REST框架的一些主要优点或缺点。从这篇文章中可以很明显地看出Dropwizard为你的项目提供了非常快速的引导。这可能是使用Dropwizard的最大优势。

此外, 它将包括开发服务时所需的所有最先进的库/工具。因此, 你绝对不必为此担心。它还为你提供了很好的配置管理。当然, Dropwizard也有一些缺点。使用Dropwizard会限制你使用Dropwizard提供或支持的功能。你失去了一些在开发时可能习惯的自由。但是, 我什至都不会称其为劣势, 因为这正是Dropwizard的本质所在-易于设置, 易于开发, 但是却非常健壮和高性能REST框架。

我认为, 通过支持越来越多的第三方库来增加框架的复杂性也会在开发中引入不必要的复杂性。

赞(1)
未经允许不得转载:srcmini » 微服务入门:Dropwizard教程

评论 抢沙发

评论前必须登录!