Apache Camel для Java: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Apache Camel для Java: полное руководство с примерами

1. Что это и зачем нужна

Apache Camel — это мощный фреймворк для интеграции приложений с открытым исходным кодом, реализующий шаблоны корпоративной интеграции (Enterprise Integration Patterns, EIP). Если вам когда-либо приходилось соединять несколько систем, обменивающихся данными через разные протоколы (HTTP, JMS, FTP, файлы, базы данных), вы знаете, как быстро это превращается в "спагетти-код". Camel решает эту проблему, предоставляя единый, декларативный DSL (Domain Specific Language) для описания маршрутов данных.

Основная идея Camel заключается в том, что вы определяете маршруты (routes), по которым движутся сообщения (messages). Каждый маршрут состоит из "точек входа" (from), "процессоров" (processors) и "точек выхода" (to). Camel берет на себя всю "скучную" работу: подключение к источникам, преобразование форматов (JSON, XML, CSV), маршаллинг/демаршаллинг, обработку ошибок и маршрутизацию на основе контента. Это позволяет разработчику сосредоточиться на бизнес-логике, а не на написании тонн boilerplate-кода для сетевого взаимодействия.

На практике Apache Camel используется для построения ESB (Enterprise Service Bus), микросервисных архитектур, ETL-процессов, интеграции с облачными сервисами (AWS, Google Cloud) и даже для создания простых файловых "наблюдателей". Фреймворк поддерживает более 300 компонентов для работы с различными технологиями, что делает его "швейцарским ножом" для интеграции в мире Java.

2. Установка

Apache Camel распространяется как набор JAR-файлов. Самый простой способ начать работу — использовать систему сборки Maven или Gradle. Ниже приведена команда для добавления зависимостей в Maven (pom.xml):

<!-- Основной движок Camel -->
<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-core</artifactId>
    <version>4.6.0</version> <!-- Проверьте актуальную версию на сайте -->
</dependency>

<!-- Компонент для работы с HTTP (например, для REST API) -->
<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-http</artifactId>
    <version>4.6.0</version>
</dependency>

<!-- Компонент для работы с файлами -->
<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-file</artifactId>
    <version>4.6.0</version>
</dependency>

<!-- Компонент для работы с JSON (Jackson) -->
<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-jackson</artifactId>
    <version>4.6.0</version>
</dependency>

Для Gradle (build.gradle):

implementation 'org.apache.camel:camel-core:4.6.0'
implementation 'org.apache.camel:camel-http:4.6.0'
implementation 'org.apache.camel:camel-file:4.6.0'
implementation 'org.apache.camel:camel-jackson:4.6.0'

Важно: Версии компонентов должны совпадать. Рекомендуется всегда использовать последнюю стабильную версию с официального сайта.

3. Быстрый старт — минимальный рабочий пример

Создайте простой маршрут, который читает файлы из одной папки и копирует их в другую, добавляя к имени файла префикс "COPY_".

import org.apache.camel.CamelContext;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.impl.DefaultCamelContext;

public class QuickStart {
    public static void main(String[] args) throws Exception {
        // 1. Создаем контекст Camel
        try (CamelContext context = new DefaultCamelContext()) {
            // 2. Добавляем маршрут
            context.addRoutes(new RouteBuilder() {
                @Override
                public void configure() throws Exception {
                    from("file:data/input?noop=true") // Читаем из папки input, не удаляя файлы
                        .process(exchange -> {
                            // 3. Логика преобразования: меняем имя файла
                            String originalName = exchange.getIn().getHeader("CamelFileName", String.class);
                            exchange.getIn().setHeader("CamelFileName", "COPY_" + originalName);
                        })
                        .to("file:data/output"); // Записываем в папку output
                }
            });

            // 4. Запускаем контекст
            context.start();

            // 5. Даем время на выполнение (в реальном приложении здесь был бы бесконечный цикл)
            Thread.sleep(5000);

            // 6. Контекст автоматически закроется благодаря try-with-resources
        }
    }
}

Как это работает:

  1. file:data/input?noop=true — Camel будет "слушать" папку data/input. Параметр noop=true означает, что файлы не будут удаляться после прочтения.
  2. .process() — блок для выполнения произвольного Java-кода. Здесь мы меняем заголовок CamelFileName.
  3. .to("file:data/output") — записываем результат в папку data/output.

Положите любой текстовый файл в data/input и через 5 секунд проверьте data/output.

4. Основные методы/функции/классы

Рассмотрим ключевые строительные блоки Camel. Все примеры предполагают, что вы находитесь внутри метода configure() класса, расширяющего RouteBuilder.

4.1. from(String uri)

Сигнатура: RouteDefinition from(String uri)

Что делает: Определяет точку входа маршрута. URI указывает, откуда брать сообщения (файл, HTTP-эндпоинт, JMS-очередь и т.д.).

// Читаем сообщения из JMS-очереди "orders"
from("jms:queue:orders")
    .log("Получен заказ: ${body}");

4.2. to(String uri)

Сигнатура: RouteDefinition to(String uri)

Что делает: Определяет точку выхода маршрута. Отправляет обработанное сообщение по указанному URI.

// Отправляем результат в HTTP-эндпоинт
from("direct:processOrder")
    .to("http://example.com/api/orders");

4.3. process(Processor processor)

Сигнатура: RouteDefinition process(Processor processor)

Что делает: Позволяет выполнить произвольный Java-код над сообщением. Processor — это функциональный интерфейс с методом process(Exchange exchange).

from("direct:greet")
    .process(exchange -> {
        String name = exchange.getIn().getBody(String.class);
        exchange.getIn().setBody("Привет, " + name + "!");
    })
    .to("log:greeting");

4.4. choice() и when() / otherwise()

Сигнатура: ChoiceDefinition choice()

Что делает: Реализует условную маршрутизацию (Content-Based Router). Позволяет направлять сообщения в разные каналы в зависимости от содержимого.

from("direct:router")
    .choice()
        .when(header("type").isEqualTo("A"))
            .to("direct:handleA")
        .when(header("type").isEqualTo("B"))
            .to("direct:handleB")
        .otherwise()
            .to("direct:handleDefault");

4.5. marshal() и unmarshal()

Сигнатура: RouteDefinition marshal(DataFormat format) / RouteDefinition unmarshal(DataFormat format)

Что делает: Преобразует тело сообщения в/из бинарного формата. Чаще всего используется для работы с JSON и XML.

// Предположим, у нас есть класс Order с полями id, name
// Преобразуем Java-объект в JSON
from("direct:orderToJson")
    .marshal().json(JsonLibrary.Jackson, Order.class)
    .to("log:jsonOutput");

// Обратно: из JSON в Java-объект
from("direct:jsonToOrder")
    .unmarshal().json(JsonLibrary.Jackson, Order.class)
    .to("log:orderObject");

4.6. split()

Сигнатура: SplitDefinition split(Expression expression)

Что делает: Разделяет одно сообщение на несколько. Полезно для обработки списков или коллекций.

// Допустим, тело сообщения — это список строк
from("direct:splitter")
    .split(body())
        .log("Обрабатываю элемент: ${body}")
    .end();

4.7. aggregate()

Сигнатура: AggregateDefinition aggregate(Expression correlationExpression, AggregationStrategy aggregationStrategy)

Что делает: Объединяет несколько сообщений в одно на основе ключа корреляции. Используется для "сборки" данных из разных источников.

// Собираем все сообщения с одинаковым заголовком "orderId"
from("direct:aggregator")
    .aggregate(header("orderId"), new GroupedBodyAggregationStrategy())
        .completionSize(3) // Завершаем агрегацию после получения 3 сообщений
        .to("log:aggregatedResult");

4.8. setBody() и setHeader()

Сигнатура: RouteDefinition setBody(Expression expression) / RouteDefinition setHeader(String name, Expression expression)

Что делает: Позволяет изменить тело сообщения или его заголовки без написания полноценного process().

from("direct:transform")
    .setBody(simple("Новое тело: ${body}"))
    .setHeader("processedBy", constant("Camel"))
    .to("log:transformed");

4.9. log()

Сигнатура: RouteDefinition log(String message)

Что делает: Удобный способ логирования сообщений внутри маршрута. Поддерживает Simple-выражения.

from("direct:logging")
    .log("Получено сообщение: ${body}, заголовок: ${header.myHeader}");

4.10. errorHandler()

Сигнатура: RouteDefinition errorHandler(ErrorHandlerBuilder errorHandlerBuilder)

Что делает: Определяет глобальную или локальную стратегию обработки ошибок. Позволяет настраивать повторные попытки, dead letter channel и т.д.

// Dead Letter Channel: отправляем ошибочные сообщения в очередь "errors"
errorHandler(deadLetterChannel("jms:queue:errors")
    .maximumRedeliveries(3)
    .redeliveryDelay(1000));

from("direct:riskyOperation")
    .to("http://unreliable-service.com/api");

5. Реальный практический пример

Создадим микросервис, который принимает HTTP-запрос с JSON-заказом, валидирует его, обогащает данными из БД (симулируем), преобразует в XML и сохраняет в файл, а также отправляет уведомление в JMS-очередь.

import org.apache.camel.CamelContext;
import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.model.rest.RestBindingMode;
import org.apache.camel.model.rest.RestParamType;

public class OrderProcessingService {

    public static void main(String[] args) throws Exception {
        try (CamelContext context = new DefaultCamelContext()) {

            // Настройка REST-конфигурации
            context.addRoutes(new RouteBuilder() {
                @Override
                public void configure() throws Exception {

                    // Глобальный обработчик ошибок
                    errorHandler(deadLetterChannel("log:dead?level=ERROR")
                        .maximumRedeliveries(2)
                        .redeliveryDelay(1000));

                    // REST-конфигурация
                    restConfiguration()
                        .component("servlet")
                        .port(8080)
                        .bindingMode(RestBindingMode.json)
                        .dataFormatProperty("prettyPrint", "true")
                        .apiContextPath("/api-doc")
                        .apiProperty("api.title", "Order Service API")
                        .apiProperty("api.version", "1.0");

                    // Определяем REST-эндпоинт
                    rest("/orders")
                        .post()
                        .param().name("body").type(RestParamType.body).endParam()
                        .to("direct:processOrder");

                    // Основной маршрут обработки заказа
                    from("direct:processOrder")
                        .routeId("orderProcessingRoute")
                        .log("Получен новый заказ: ${body}")

                        // Шаг 1: Валидация (проверяем, что есть поле "customerId")
                        .process(exchange -> {
                            String body = exchange.getIn().getBody(String.class);
                            if (body == null || !body.contains("customerId")) {
                                throw new IllegalArgumentException("Отсутствует customerId в заказе");
                            }
                        })
                        .log("Валидация пройдена                    

Рекомендаций