Внедрение зависимостей в Quarkus с помощью CDI: ArC, области видимости, квалификаторы и внедрение во время сборки
Внедрение зависимостей (DI) — это основа современной разработки корпоративных приложений Java. В течение многих лет разработчики Java полагались на контейнеры Spring IoC (инверсия управления) или стандартные контейнеры Jakarta EE CDI (внедрение контекста и зависимостей), такие как Hibernate Weld или OpenWebBeans, для разделения жизненных циклов компонентов, управления зависимостями сервисов и внедрения свойств конфигурации.
Однако по мере перехода корпоративных приложений к облачным микросервисам, контейнеризации с помощью Docker и Kubernetes и бессерверным моделям выполнения (AWS Lambda, Knative) традиционные контейнеры внедрения зависимостей выявили критический недостаток: тяжелое отражение во время выполнения и большие затраты памяти.
Познакомьтесь с Quarkus и его облегченным механизмом CDI ArC. Созданный с нуля для компиляции Kubernetes и GraalVM, Quarkus переопределяет внедрение зависимостей, перемещая отражение, сканирование путей к классам и связывание графов компонентов со времени выполнения приложения на время сборки.
В этом подробном руководстве мы рассмотрим Внедрение зависимостей в Quarkus с помощью CDI, охватывая внутреннюю механику ArC-контейнера, области CDI, пользовательские квалификаторы, производители, перехватчики, управляемые событиями наблюдатели, программный поиск и лучшие практики написания высокопроизводительных облачных Java-приложений.
1. Понимание ArC: Quarkus Build-Time CDI Engine
Стандартные контейнеры Jakarta EE CDI работают полностью при запуске приложения. Когда загружается устаревшее приложение, контейнер CDI сканирует каждый JAR в пути к классам, анализирует метаданные, проверяет аннотации с помощью Java Reflection (java.lang.reflect), генерирует динамические прокси в памяти и строит граф зависимостей. Это динамическое отражение потребляет значительный объем оперативной памяти (метапространство) и увеличивает время загрузки при холодном запуске.
Quarkus решает это узкое место с помощью ArC (реализация CDI во время сборки, основанная на спецификациях Jakarta CDI 4.0).
Чем ArC отличается от стандартного CDI:
- Дополнение во время сборки: во время
mvn packageили./gradlew buildArC сканирует все прикладные компоненты, проверяет точки внедрения и предварительно строит граф зависимостей. - Упреждающая генерация байт-кода (AOT): ArC заранее генерирует прямой байт-код Java для создания экземпляров, прокси-компонентов и перехватчиков, устраняя отражение во время выполнения.
- Очистка неиспользуемых компонентов (устранение мертвого кода). Любой компонент CDI, который никогда не внедряется и не используется динамически, идентифицируется во время компиляции и автоматически удаляется из конечного двоичного файла, что уменьшает объем памяти.
- Встроенная оптимизация образа GraalVM. Поскольку связывание компонентов разрешается во время сборки, приложения Quarkus легко компилируются в собственные исполняемые файлы с подсказками по настройке без отражения.
2. Области применения CDI-компонентов и управление жизненным циклом
В CDI Область определяет жизненный цикл и видимость экземпляра компонента. Quarkus ArC поддерживает стандартные области Jakarta CDI с оптимизацией во время сборки.
Основные области CDI в Quarkus:
| Аннотация к области действия | Жизненный цикл и поведение | Создание прокси | Лучший вариант использования |
|---|---|---|---|
@ApplicationScoped |
Один экземпляр для каждого приложения. Ленивая инициализация при первом вызове. Состояние является общим для всех потоков. | Создан клиентский прокси | Сервисы без сохранения состояния, репозитории, конфигурации. |
@RequestScoped |
Экземпляр, привязанный к одному HTTP-запросу или вызову события. Уничтожается после завершения запроса. | Создан клиентский прокси | Пользовательские сеансы, контекстные данные запроса, токены безопасности. |
@Зависимый |
Область по умолчанию, если она не указана. Экземпляр напрямую связан со временем жизни внедряющего компонента. | Нет прокси (прямой экземпляр) | Вспомогательные служебные классы, держатели эфемерных состояний. |
@Синглтон |
Один экземпляр для каждого приложения. Создается с нетерпением или лениво. Нет оболочки клиентского прокси. | Нет прокси (прямой экземпляр) | Высокопроизводительные синглтоны, в которых необходимо избегать накладных расходов клиентского прокси. |
@Неудаляемый |
Маркерная аннотация, указывающая ArC не удалять компонент во время оптимизации во время сборки. | Зависит от объема | Цели динамического поиска или bean-компоненты, ссылающиеся на отражение. |
Пример кода: объявление bean-компонентов с ограниченной областью действия
package com.ghaznix.services;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.RequestScoped;
import java.util.UUID;
@ApplicationScoped
public class OrderProcessingService {
public String processOrder(String orderId) {
return "Order " + orderId + " processed successfully.";
}
}
@RequestScoped
public class RequestContextHolder {
private final String requestId = UUID.randomUUID().toString();
public String getRequestId() {
return requestId;
}
}
3. Шаблоны внедрения зависимостей в Quarkus
Quarkus поддерживает несколько стилей внедрения, используя стандартную аннотацию jakarta.inject.Inject.
A. Внедрение в конструктор (рекомендуемая передовая практика)
Внедрение в конструктор гарантирует неизменность (поля final), упрощает модульное тестирование без необходимости использования контейнеров CDI и делает зависимости явными.
package com.ghaznix.resources;
import com.ghaznix.services.OrderProcessingService;
import jakarta.inject.Inject;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
@Path("/orders")
public class OrderResource {
private final OrderProcessingService orderService;
// In Quarkus, if a class has a single constructor, @Inject is OPTIONAL!
@Inject
public OrderResource(OrderProcessingService orderService) {
this.orderService = orderService;
}
@POST
@Path("/{id}")
public String checkout(@PathParam("id") String id) {
return orderService.processOrder(id);
}
}
Совет: если в Quarkus ArC компонент имеет только один конструктор, вы можете полностью опустить аннотацию
@Inject. ArC автоматически распознает его как конструктор внедрения.
B. Полевая инъекция
Внедрение полей является кратким, но делает поля изменяемыми и усложняет автономное модульное тестирование.
@ApplicationScoped
public class NotificationService {
@Inject
EmailClient emailClient; // Field injection
}
4. Квалификаторы и устранение неоднозначности в нескольких реализациях
Когда несколько bean-компонентов реализуют один и тот же интерфейс, CDI генерирует неоднозначное исключение зависимости во время сборки. Quarkus использует Квалификаторы для безопасного разрешения реализаций целевых компонентов.
Определение пользовательских квалификаторов
package com.ghaznix.payment;
import jakarta.inject.Qualifier;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface PaymentType {
Mode value();
enum Mode {
CREDIT_CARD,
PAYPAL,
CRYPTO
}
}
Реализация квалифицированных компонентов
public interface PaymentProcessor {
void process(double amount);
}
@ApplicationScoped
@PaymentType(PaymentType.Mode.CREDIT_CARD)
public class CreditCardProcessor implements PaymentProcessor {
@Override
public void process(double amount) {
System.out.println("Processing Credit Card payment: $" + amount);
}
}
@ApplicationScoped
@PaymentType(PaymentType.Mode.PAYPAL)
public class PayPalProcessor implements PaymentProcessor {
@Override
public void process(double amount) {
System.out.println("Processing PayPal payment: $" + amount);
}
}
Внедрение квалифицированных компонентов
@ApplicationScoped
public class CheckoutService {
private final PaymentProcessor creditCardProcessor;
private final PaymentProcessor payPalProcessor;
@Inject
public CheckoutService(
@PaymentType(PaymentType.Mode.CREDIT_CARD) PaymentProcessor creditCardProcessor,
@PaymentType(PaymentType.Mode.PAYPAL) PaymentProcessor payPalProcessor) {
this.creditCardProcessor = creditCardProcessor;
this.payPalProcessor = payPalProcessor;
}
}
5. Производители и диспоузеры: внедрение сторонних объектов
Иногда вам необходимо внедрить классы из сторонних библиотек (например, клиентов AWS SDK, соединений Redis, пользовательских компонентов конфигурации), которые не помечены @ApplicationScoped. Quarkus решает эту проблему с помощью Producer Methods и Disposers.
package com.ghaznix.config;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Disposes;
import jakarta.enterprise.inject.Produces;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
@ApplicationScoped
public class DatabaseConnectionProducer {
@Produces
@ApplicationScoped
public Connection createDatabaseConnection() throws SQLException {
return DriverManager.getConnection("jdbc:postgresql://localhost:5432/mydb", "user", "pass");
}
// Disposer method called automatically when bean is destroyed
public void closeConnection(@Disposes Connection connection) throws SQLException {
if (connection != null && !connection.isClosed()) {
connection.close();
}
}
}
6. Перехватчики CDI: аспектно-ориентированное программирование (АОП)
Перехватчики позволяют привязывать к методам bean-компонентов сквозные задачи (ведение журналов, контрольные журналы, метрики, ограничение скорости), не затрагивая бизнес-логику.
Шаг 1. Определите привязку перехватчика
package com.ghaznix.interceptors;
import jakarta.interceptor.InterceptorBinding;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@InterceptorBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface LogExecutionTime {
}
Шаг 2. Реализация логики перехватчика
package com.ghaznix.interceptors;
import jakarta.annotation.Priority;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;
@LogExecutionTime
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
public class LogExecutionTimeInterceptor {
@AroundInvoke
public Object logInvocation(InvocationContext context) throws Exception {
long startTime = System.currentTimeMillis();
try {
return context.proceed();
} finally {
long duration = System.currentTimeMillis() - startTime;
System.out.printf("[AUDIT] Method %s executed in %d ms%n",
context.getMethod().getName(), duration);
}
}
}
Шаг 3. Аннотирование целевых компонентов
@ApplicationScoped
public class AnalyticsService {
@LogExecutionTime
public void computeAggregates() {
// Business computation
}
}
Поскольку Quarkus генерирует статические прокси-перехватчики во время сборки, накладные расходы на вызов методов практически равны нулю по сравнению с динамическими прокси-серверами времени выполнения CGLIB в Spring Boot.
7. Архитектура, управляемая событиями, с событиями CDI
CDI обеспечивает встроенную синхронную и асинхронную публикацию событий для разделения внутрипроцессного взаимодействия.
Публикация и наблюдение за событиями
// 1. Define Event Payload
public record OrderCreatedEvent(String orderId, double totalAmount) {}
// 2. Publish Event
@ApplicationScoped
public class OrderService {
@Inject
Event<OrderCreatedEvent> orderEventPublisher;
public void createOrder(String orderId, double amount) {
// Create order logic...
orderEventPublisher.fire(new OrderCreatedEvent(orderId, amount));
}
}
// 3. Observe Event Synchronously or Asynchronously
@ApplicationScoped
public class InventoryObserver {
public void onOrderCreated(@Observes OrderCreatedEvent event) {
System.out.println("Updating inventory for order: " + event.orderId());
}
public void onOrderCreatedAsync(@ObservesAsync OrderCreatedEvent event) {
System.out.println("Async audit log for order: " + event.orderId());
}
}
События жизненного цикла приложения
Quarkus предоставляет события запуска и завершения работы для выполнения кода инициализации при запуске или завершении приложения:
@ApplicationScoped
public class ApplicationLifecycleBean {
void onStart(@Observes StartupEvent ev) {
System.out.println("Quarkus Application starting up...");
}
void onStop(@Observes ShutdownEvent ev) {
System.out.println("Quarkus Application shutting down...");
}
}
8. Программный поиск компонентов с помощью Instance<T> и Arc.container()
Если целевые реализации должны выбираться динамически во время выполнения (или внутри устаревших экземпляров, не поддерживающих CDI), используйте статический поиск Instance<T> или Quarkus Arc.container().
@ApplicationScoped
public class DynamicPluginRegistry {
@Inject
Instance<PaymentProcessor> processors;
public void executeAll(double amount) {
for (PaymentProcessor processor : processors) {
processor.process(amount);
}
}
}
9. Архитектурное сравнение: Quarkus ArC, Spring IoC и Weld
| Особенность | Кваркус ArC | Spring IoC-контейнер | Стандартная Джакарта Сварка |
|---|---|---|---|
| Разрешение зависимостей | Время сборки (AOT) | Отражение во время выполнения | Отражение во время выполнения |
| Затраты оперативной памяти Metaspace | Крайне низкий (~0 МБ для метаданных) | Высокий (кэш классов динамического прокси) | От умеренного до высокого |
| Время запуска | Миллисекунды (< 0,05 с) | Секунды (2,5–8 с) | Секунды (1,5–4 с) |
| Встроенная поддержка GraalVM | Нативный, готовый к использованию | Требуются встроенные подсказки Spring | Сложная конфигурация |
| Обрезка неиспользованных бобов | Автоматическое сокращение во время сборки | Хранится в пути к классам | Хранится в пути к классам |
| Одиночное внедрение в конструктор | Неявный (аннотация не требуется) | Поддерживается | Требуется @Inject |
10. Модульное тестирование CDI-компонентов с помощью @QuarkusTest и @InjectMock
Quarkus упрощает тестирование компонентов CDI с помощью JUnit 5 и RestAssured. Используйте @InjectMock для автоматической замены компонентов зависимостей макетами Mockito во время тестирования.
package com.ghaznix;
import com.ghaznix.services.OrderProcessingService;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@QuarkusTest
public class OrderServiceTest {
@Inject
OrderService orderService;
@InjectMock
OrderProcessingService orderProcessingService;
@Test
public void testOrderCheckout() {
Mockito.when(orderProcessingService.processOrder("ORD-101"))
.thenReturn("Mocked Success");
String result = orderService.checkout("ORD-101");
Assertions.assertEquals("Mocked Success", result);
}
}
11. Заключение
Внедрение зависимостей в Quarkus с помощью CDI (ArC) знаменует собой монументальный сдвиг в корпоративном дизайне Java. Перенося сканирование метаданных компонентов, генерацию прокси и разрешение графов внедрения во время компиляции, Quarkus обеспечивает сверхзвуковую скорость запуска, субатомный объем памяти и бесшовную встроенную поддержку двоичных файлов GraalVM.
Независимо от того, переходите ли вы со Spring Boot или создаете новые облачные микросервисы на Kubernetes, освоение Quarkus CDI обеспечит вам производительность и продуктивность разработки, необходимые для современной архитектуры программного обеспечения.
Tags
Empower Your Digital Presence & Workflows
Explore top-tier tools built by Ghaznix to streamline your links, surveys, and brand growth.