Создание микросервисов на 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
необходимые зависимости. Пример базовой конфигурации:
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».