Создание микросервисов на Java с Dropwizard

9dc523aeafdefba654286d4465242174.png

Привет, Хабр!

Dropwizard — это комплексный фреймворк, созданный с целью упростить разработку RESTful веб‑сервисов, объединяя в себе множество проверенных временем библиотек и инструментов. В его основе лежат компоненты:

  • Jetty — легковесный HTTP сервер и контейнер сервлетов, обеспечивающий высокую производительность и надежность при обработке запросов.

  • Jersey — фреймворк для создания RESTful веб‑сервисов, который предоставляет удобный и мощный API для разработки REST API.

  • Jackson — библиотека для сериализации и десериализации объектов Java в JSON и обратно, широко известная своей скоростью и гибкостью.

  • Metrics — библиотека для сбора и анализа метрик производительности, позволяющая отслеживать состояние приложения в режиме реального времени.

Кроме того, Dropwizard включает в себя поддержку конфигурации, валидации, логгирования, мониторинга и тестирования.

Установим

Начнем с самого начала. Прежде всего, потребуется JDK,  желательно версии 11 или выше. Далее потребуется выбрать между двумя инструментами сборки: Maven и Gradle. Оба инструмента поддерживаются Dropwizard, и выбор зависит скорее от предпочтений.

Для тех, кто предпочитает Maven, настройка достаточно проста. Создаем новый Maven‑проект, добавив в pom.xml необходимые зависимости. Пример базовой конфигурации:


    io.dropwizard
    dropwizard-core
    2.1.0

После этого выполняем команду mvn clean install.

Если выбор пал Gradle, то процесс начинается с создания файла build.gradle с включением необходимых зависимостей:

plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'io.dropwizard:dropwizard-core:2.1.0'
}

После этого выполняемкоманду ./gradlew build, чтобы собрать проект и загрузить все необходимые библиотеки.

Приступим к созданию самого проекта. Основной класс приложения должен наследовать io.dropwizard.Application и переопределять метод run. Вот минимальный пример:

public class HelloWorldApplication extends Application {
    public static void main(String[] args) throws Exception {
        new HelloWorldApplication().run(args);
    }

    @Override
    public void run(HelloWorldConfiguration configuration, Environment environment) {
        final HelloWorldResource resource = new HelloWorldResource(
            configuration.getTemplate(),
            configuration.getDefaultName()
        );
        environment.jersey().register(resource);
    }
}

Обычно, проект разделен на несколько основных частей:

  • src/main/java — содержит исходный код.

  • src/main/resources — конфигурационные файлы, включая config.yml.

  • target — директория, куда помещаются скомпилированные артефакты.

Конфигурационный файл config.yml является основным элементом в настройке приложения. Пример минимального конфигурационного файла:

server:
  applicationConnectors:
    - type: http
      port: 8080
  adminConnectors:
    - type: http
      port: 8081

logging:
  level: INFO
  loggers:
    com.example.helloworld: DEBUG

Здесь можно настраивать серверные соединения, уровень логирования и другие параметры без необходимости изменять код.

Создаем базовый микросервис

Первое, что нужно сделать, — это создать REST API. Dropwizard делает этот процесс понятным благодаря интеграции с Jersey. Начнем с определения ресурса, который будет отвечать за определенный URL‑эндпоинт:

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/hello-world")
@Produces(MediaType.APPLICATION_JSON)
public class HelloWorldResource {
    @GET
    public String sayHello() {
        return "{\"message\":\"Hello, World!\"}";
    }
}

Определяем класс HelloWorldResource, который будет обрабатывать GET‑запросы по пути /hello-world и возвращать простой JSON‑ответ. Аннотации @Path и @GET определяют маршрут и HTTP‑метод соответственно.

Чтобы этот ресурс начал работать, его нужно зарегистрировать в приложении:

import io.dropwizard.Application;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;

public class HelloWorldApplication extends Application {
    public static void main(String[] args) throws Exception {
        new HelloWorldApplication().run(args);
    }

    @Override
    public void run(HelloWorldConfiguration configuration, Environment environment) {
        final HelloWorldResource resource = new HelloWorldResource();
        environment.jersey().register(resource);
    }
}

Класс является точкой входа в приложение. Регистрируем ресурс в методе run, чтобы Dropwizard знал, как обрабатывать соответствующие HTTP‑запросы.

Теперь перейдем к другой части — реализации CRUD операций. Представим, что нужно поработать с сущностью Person, которая имеет ID, имя и возраст:

import com.fasterxml.jackson.annotation.JsonProperty;

public class Person {
    private long id;
    private String name;
    private int age;

    public Person() {}

    public Person(long id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    @JsonProperty
    public long getId() {
        return id;
    }

    @JsonProperty
    public String getName() {
        return name;
    }

    @JsonProperty
    public int getAge() {
        return age;
    }
}

Теперь определим ресурс, который будет управлять операциями для Person:

import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.*;

@Path("/people")
@Produces(MediaType.APPLICATION_JSON)
public class PersonResource {
    private final Map personDB = new HashMap<>();

    @POST
    public Response createPerson(Person person) {
        personDB.put(person.getId(), person);
        return Response.status(Response.Status.CREATED).entity(person).build();
    }

    @GET
    @Path("/{id}")
    public Person getPerson(@PathParam("id") long id) {
        Person person = personDB.get(id);
        if (person == null) {
            throw new WebApplicationException("Person not found", Response.Status.NOT_FOUND);
        }
        return person;
    }

    @PUT
    @Path("/{id}")
    public Person updatePerson(@PathParam("id") long id, Person person) {
        if (!personDB.containsKey(id)) {
            throw new WebApplicationException("Person not found", Response.Status.NOT_FOUND);
        }
        personDB.put(id, person);
        return person;
    }

    @DELETE
    @Path("/{id}")
    public Response deletePerson(@PathParam("id") long id) {
        Person person = personDB.remove(id);
        if (person == null) {
            throw new WebApplicationException("Person not found", Response.Status.NOT_FOUND);
        }
        return Response.status(Response.Status.NO_CONTENT).build();
    }
}

Этот ресурс позволяет выполнять все основные CRUD операции. Здесь используется HashMap как временное хранилище данных, но в продакшене, конечно, используем базы данных.

Не забываем о том, что нужно правильно обрабатывать HTTP‑запросы и формировать ответы. В приведенном выше примере использовали объект Response для управления статусами HTTP и телом ответа. Например, при создании нового объекта Person возвращается статус 201 Created, что указывает на успешное выполнение операции.

Также мы используем аннотацию @PathParam для извлечения параметров из URL, а @JsonProperty помогает сериализовать и десериализовать объекты в JSON и обратно.

Также в визарде есть инструменты для валидации данных. Например, можно использовать аннотации @NotNull, @Size, @Min, чтобы убедиться, что данные соответствуют определенным критериям перед их обработкой.

Пример валидации:

import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

public class Person {
    private long id;

    @NotEmpty
    private String name;

    @Min(0)
    private int age;

    // Геттеры и сеттеры
}

Логгирование в Dropwizard также упрощено благодаря интеграции с SLF4J и Logback. Например, можно добавить простое логгирование:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PersonResource {
    private static final Logger LOGGER = LoggerFactory.getLogger(PersonResource.class);

    @POST
    public Response createPerson(Person person) {
        LOGGER.info("Creating person: {}", person);
        personDB.put(person.getId(), person);
        return Response.status(Response.Status.CREATED).entity(person).build();
    }

    // Другие методы
}

Для мониторинга состояния приложения Dropwizard предлагает встроенные метрики, которые можно легко добавить:

import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.annotation.Timed;

public class PersonResource {
    private final MetricRegistry metrics;

    public PersonResource(MetricRegistry metrics) {
        this.metrics = metrics;
    }

    @POST
    @Timed
    public Response createPerson(Person person) {
        metrics.counter("create-person-requests").inc();
        personDB.put(person.getId(), person);
        return Response.status(Response.Status.CREATED).entity(person).build();
    }
}

Эта аннотация @Timed автоматически создает метрики времени выполнения метода, которые затем можно использовать для анализа производительности.

Тестирование и развертывание микросервиса

Тестирование микросервисов включает в себя несколько уровней проверки:

  • Юнит-тесты: Тестируют отдельные компоненты в изоляции. Юнит-тесты быстрые и просты в реализации.

  • Интеграционные тесты: Проверяют взаимодействие между компонентами, включая доступ к базе данных или внешним API.

  • Тесты на производительность: Определяют, насколько хорошо микросервис обрабатывает нагрузку, и выявляют узкие места в производительности.

Начнем с простого юнит-теста. В Dropwizard можно использовать JUnit и Mockito для создания юнит-тестов. Допустим, есть сервис для управления сущностями Person:

public class PersonService {
    private final PersonDAO personDAO;

    public PersonService(PersonDAO personDAO) {
        this.personDAO = personDAO;
    }

    public Person findPersonById(long id) {
        return personDAO.findById(id).orElseThrow(() -> new NotFoundException("Person not found"));
    }
}

Юнит-тест для этого сервиса будет выглядеть так:

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

public class PersonServiceTest {
    private final PersonDAO personDAO = Mockito.mock(PersonDAO.class);
    private final PersonService personService = new PersonService(personDAO);

    @Test
    void findPersonById_ShouldReturnPerson_WhenPersonExists() {
        Person person = new Person(1, "John Doe", 30);
        when(personDAO.findById(1L)).thenReturn(Optional.of(person));

        Person foundPerson = personService.findPersonById(1L);
        assertEquals("John Doe", foundPerson.getName());
    }

    @Test
    void findPersonById_ShouldThrowNotFoundException_WhenPersonDoesNotExist() {
        when(personDAO.findById(1L)).thenReturn(Optional.empty());

        assertThrows(NotFoundException.class, () -> personService.findPersonById(1L));
    }
}

Этот тест проверяет как позитивные, так и негативные сценарии работы метода findPersonById.

Dropwizard также имеет удобные инструменты для интеграционного тестирования, включая поддержку встроенного Jetty-сервера для запуска приложения во время тестов.

Допустим, нужно протестировать REST API, который возвращает Person по ID. Можно юзать DropwizardAppRule для запуска Dropwizard-приложения в тестовом окружении:

import io.dropwizard.testing.junit5.DropwizardAppExtension;
import io.dropwizard.testing.ResourceHelpers;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;

public class PersonResourceTest {
    private static final DropwizardAppExtension RULE = 
        new DropwizardAppExtension<>(MyApplication.class, ResourceHelpers.resourceFilePath("config-test.yml"));

    @Test
    void getPerson_ShouldReturnPerson_WhenPersonExists() {
        given()
            .when()
            .get("/people/1")
            .then()
            .statusCode(200)
            .body("name", equalTo("John Doe"));
    }
}

Используем DropwizardAppExtension, чтобы запустить приложение, и RestAssured для упрощения отправки HTTP-запросов и проверки ответов.

Когда тесты успешно пройдены, пора переходить к развертыванию. Один из лучших способов сделать это — использовать Docker и Kubernetes в комбинации с CI/CD пайплайном.

Создадим Dockerfile для микросервиса:

FROM openjdk:11-jre-slim
COPY target/my-application.jar /app/my-application.jar
WORKDIR /app
CMD ["java", "-jar", "my-application.jar", "server", "config.yml"]

Соберем Docker-образ:

docker build -t my-application:latest .

Для развертывания в Kubernetes, создадим файл манифеста:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-application
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-application
  template:
    metadata:
      labels:
        app: my-application
    spec:
      containers:
      - name: my-application
        image: my-application:latest
        ports:
        - containerPort: 8080
        envFrom:
        - configMapRef:
            name: my-application-config

Этот манифест развертывает 3 экземпляра микросервиса в Kubernetes.

Подробнее с библиотекой можно ознакомиться здесь.

В заключение напоминаем про открытый урок, на котором разберем топ ошибок при переходе с монолита на микросервисную архитектуру. В результате вебинара:

  • Вы поймете основные ошибки при переходе на микросервисы и как их избежать.

  • Узнаете о преимуществах и недостатках микросервисной архитектуры.

  • Освоите ключевые паттерны и лучшие практики работы с микросервисами.

  • Получите примеры успешных и неудачных переходов на микросервисы, которые помогут вам в вашем проекте.

Записаться на урок можно на странице курса «Microservice Architecture».

© Habrahabr.ru