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

от автора

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

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

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

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

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

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

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

Установим

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

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

<dependency>     <groupId>io.dropwizard</groupId>     <artifactId>dropwizard-core</artifactId>     <version>2.1.0</version> </dependency>

После этого выполняем команду 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<HelloWorldConfiguration> {     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<HelloWorldConfiguration> {     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<Long, Person> 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<MyConfiguration> 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».


ссылка на оригинал статьи https://habr.com/ru/articles/836042/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *