Микросервисы на Java: не бойтесь, это проще, чем кажется
Вы когда-нибудь смотрели на монолитное приложение и думали: «Вот бы разделить это на части, чтобы не бояться, что одно изменение сломает всё»? Поздравляю — вы созрели для микросервисов. В мире Java эта тема обросла мифами о невероятной сложности и куче технологий. На самом деле, стартовать можно с двух классов и одного файла application.yml.
В этой статье мы не будем строить космический корабль. Мы напишем два простых микросервиса, которые поговорят друг с другом по HTTP, и разберёмся, с чего реально начать. Никакой магии — только Java, Spring Boot и здравый смысл.
Что такое микросервис (на пальцах)
Представьте себе ресторан. Монолит — это когда один повар делает всё: и суп варит, и стейк жарит, и посуду моет. Микросервисы — это команда: один повар отвечает только за супы, второй — за мясо, третий — за десерты. Каждый работает независимо, но вместе создают общее меню.
В Java микросервис — это самостоятельное приложение, которое отвечает за одну бизнес-функцию (например, «Управление пользователями» или «Обработка заказов»). У него своя база данных, свой код, и он общается с другими сервисами через сеть (обычно по HTTP/REST или через брокеры сообщений).
С чего начать: инструментарий джентльмена
Для первого проекта вам понадобится всего три вещи:
- Java 17+ — современная версия с полезными фичами (records, sealed classes).
- Spring Boot 3.x — самый популярный фреймворк для микросервисов на Java. Он берёт на себя 80% рутины.
- Maven или Gradle — для сборки. Maven проще для старта, Gradle быстрее.
Дополнительно, когда захотите усложнить:
- Docker — чтобы упаковать сервис в контейнер.
- Spring Cloud — для обнаружения сервисов (Service Discovery) и балансировки.
- PostgreSQL/H2 — база данных на выбор.
Совет: не пытайтесь сразу выучить Kubernetes и Kafka. Начните с простого REST-общения.
Пишем первый микросервис: Сервис пользователей
Создадим простой микросервис, который умеет отдавать информацию о пользователях. Используем Spring Initializr (start.spring.io) или создадим проект вручную.
Добавьте зависимости в pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
Теперь напишем модель пользователя:
import jakarta.persistence.*;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// конструкторы, геттеры, сеттеры (можно использовать Lombok)
public User() {}
public User(String name, String email) {
this.name = name;
this.email = email;
}
// геттеры и сеттеры
}
Репозиторий (упрощённый доступ к данным):
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
}
И контроллер — REST-ручка:
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserRepository userRepository;
public UserController(UserRepository userRepository) {
this.userRepository = userRepository;
}
@GetMapping
public List<User> getAllUsers() {
return userRepository.findAll();
}
@PostMapping
public User createUser(@RequestBody User user) {
return userRepository.save(user);
}
}
Готово! Это уже микросервис. Запустите main метод, и он будет слушать порт 8080. Проверьте: GET localhost:8080/api/users — получите пустой список.
Пишем второй микросервис: Сервис заказов
Создайте новый проект (или модуль) с теми же зависимостями. Теперь он будет обращаться к первому сервису, чтобы получить данные о пользователе.
Создадим простой REST-клиент с помощью RestClient (новый в Spring 6):
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
@Service
public class UserServiceClient {
private final RestClient restClient;
public UserServiceClient() {
this.restClient = RestClient.create("http://localhost:8080");
}
public String getUserNameById(Long userId) {
return restClient.get()
.uri("/api/users/{id}", userId)
.retrieve()
.body(User.class)
.getName();
}
}
Обратите внимание: мы жёстко закодировали адрес первого сервиса. В реальности так не делают — используют Service Discovery (Eureka или Consul). Но для старта сойдёт.
Теперь контроллер заказов:
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final UserServiceClient userServiceClient;
public OrderController(UserServiceClient userServiceClient) {
this.userServiceClient = userServiceClient;
}
@GetMapping("/{userId}/name")
public String getOrderUserName(@PathVariable Long userId) {
return "Заказ для пользователя: " + userServiceClient.getUserNameById(userId);
}
}
Запустите оба сервиса. Первый на порту 8080, второй — на 8081 (добавьте в application.properties: server.port=8081). Теперь при запросе GET localhost:8081/api/orders/1/name второй сервис сходит в первый и вернёт имя пользователя.
Как сделать общение надёжнее
В примере выше, если первый сервис упадёт, второй просто выбросит ошибку. В реальном мире это недопустимо. Вот что добавляют в боевых проектах:
- Circuit Breaker (Resilience4j) — если сервис не отвечает, не ждать вечно, а быстро вернуть запасной ответ.
- Retry — попробовать ещё раз через секунду.
- Service Discovery (Eureka) — чтобы не хардкодить адреса, а получать их из реестра.
Пример добавления Circuit Breaker:
import io.github.resilience4j.circuitbreaker.annotation.Circuit