- Home »

Шаблон внедрения зависимостей в Java — пример и урок
Dependency Injection (DI) — это не просто очередной паттерн из учебника, а реальный инструмент для написания поддерживаемого кода. Если ты когда-нибудь пытался тестировать сложную Java-систему, где классы крепко связаны друг с другом, то знаешь, что это настоящий кошмар. DI решает эту проблему, позволяя легко заменять зависимости, писать unit-тесты и масштабировать приложения. Особенно актуально это для серверных приложений, где нужна гибкость в настройке компонентов под разные окружения.
Как работает паттерн Dependency Injection
Суть DI проста: вместо того чтобы класс сам создавал свои зависимости, он их получает извне. Это как разница между “я сам куплю еду” и “мне принесут готовую еду”. Класс перестает быть ответственным за создание объектов, которые ему нужны для работы.
Три основных способа внедрения зависимостей:
- Constructor Injection — через конструктор (самый популярный)
- Setter Injection — через setter-методы
- Interface Injection — через интерфейс (редко используется)
Простой пример без DI vs с DI
Начнем с плохого примера — тесно связанный код:
// Плохо: жесткая зависимость
public class OrderService {
private EmailService emailService;
private DatabaseService databaseService;
public OrderService() {
this.emailService = new EmailService(); // Жестко привязано!
this.databaseService = new DatabaseService(); // Тоже жестко!
}
public void processOrder(Order order) {
databaseService.save(order);
emailService.sendConfirmation(order.getEmail());
}
}
Теперь с DI — гибкий и тестируемый код:
// Хорошо: зависимости внедряются извне
public class OrderService {
private final EmailService emailService;
private final DatabaseService databaseService;
// Constructor Injection
public OrderService(EmailService emailService, DatabaseService databaseService) {
this.emailService = emailService;
this.databaseService = databaseService;
}
public void processOrder(Order order) {
databaseService.save(order);
emailService.sendConfirmation(order.getEmail());
}
}
Пошаговое создание простого DI-контейнера
Давайте создадим минимальный DI-контейнер с нуля, чтобы понять, как это работает под капотом:
// 1. Создаем интерфейсы для сервисов
public interface EmailService {
void sendConfirmation(String email);
}
public interface DatabaseService {
void save(Order order);
}
// 2. Реализации
public class SmtpEmailService implements EmailService {
@Override
public void sendConfirmation(String email) {
System.out.println("Sending email to: " + email);
}
}
public class PostgresDatabaseService implements DatabaseService {
@Override
public void save(Order order) {
System.out.println("Saving order to PostgreSQL: " + order.getId());
}
}
// 3. Простой DI-контейнер
public class DIContainer {
private Map<Class<?>, Object> services = new HashMap<>();
public void register(Class serviceClass, T implementation) {
services.put(serviceClass, implementation);
}
@SuppressWarnings("unchecked")
public T resolve(Class serviceClass) {
return (T) services.get(serviceClass);
}
}
Использование нашего контейнера:
public class Application {
public static void main(String[] args) {
// Настройка контейнера
DIContainer container = new DIContainer();
container.register(EmailService.class, new SmtpEmailService());
container.register(DatabaseService.class, new PostgresDatabaseService());
// Создание сервиса с внедренными зависимостями
OrderService orderService = new OrderService(
container.resolve(EmailService.class),
container.resolve(DatabaseService.class)
);
// Использование
Order order = new Order("123", "user@example.com");
orderService.processOrder(order);
}
}
Практический пример с аннотациями
Создадим более продвинутый вариант с аннотациями, как в реальных фреймворках:
// 1. Создаем аннотацию для внедрения
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.CONSTRUCTOR)
public @interface Inject {
}
// 2. Аннотация для маркировки сервисов
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Service {
}
// 3. Обновляем наш сервис
@Service
public class OrderService {
private final EmailService emailService;
private final DatabaseService databaseService;
@Inject
public OrderService(EmailService emailService, DatabaseService databaseService) {
this.emailService = emailService;
this.databaseService = databaseService;
}
public void processOrder(Order order) {
databaseService.save(order);
emailService.sendConfirmation(order.getEmail());
}
}
Продвинутый DI-контейнер с рефлексией:
public class AdvancedDIContainer {
private Map<Class<?>, Class<?>> services = new HashMap<>();
private Map<Class<?>, Object> instances = new HashMap<>();
public void register(Class serviceClass, Class<? extends T> implementation) {
services.put(serviceClass, implementation);
}
@SuppressWarnings("unchecked")
public T resolve(Class serviceClass) {
if (instances.containsKey(serviceClass)) {
return (T) instances.get(serviceClass);
}
Class<?> implementation = services.get(serviceClass);
if (implementation == null) {
throw new RuntimeException("Service not registered: " + serviceClass.getName());
}
try {
Constructor<?> constructor = findInjectConstructor(implementation);
Object[] dependencies = resolveDependencies(constructor);
T instance = (T) constructor.newInstance(dependencies);
instances.put(serviceClass, instance);
return instance;
} catch (Exception e) {
throw new RuntimeException("Failed to create instance", e);
}
}
private Constructor<?> findInjectConstructor(Class<?> clazz) {
for (Constructor<?> constructor : clazz.getConstructors()) {
if (constructor.isAnnotationPresent(Inject.class)) {
return constructor;
}
}
return clazz.getConstructors()[0]; // Fallback
}
private Object[] resolveDependencies(Constructor<?> constructor) {
Class<?>[] paramTypes = constructor.getParameterTypes();
Object[] dependencies = new Object[paramTypes.length];
for (int i = 0; i < paramTypes.length; i++) {
dependencies[i] = resolve(paramTypes[i]);
}
return dependencies;
}
}
Сравнение популярных DI-фреймворков
Фреймворк | Размер | Скорость | Сложность | Особенности |
---|---|---|---|---|
Spring Framework | Большой (~20MB) | Средняя | Высокая | Полная экосистема, XML/аннотации |
Google Guice | Небольшой (~1.5MB) | Высокая | Средняя | Compile-time проверки, быстрый |
Dagger 2 | Минимальный | Очень высокая | Средняя | Compile-time генерация кода |
CDI (Weld) | Средний | Средняя | Высокая | Java EE стандарт |
Настройка проекта с Google Guice
Guice — отличный выбор для изучения DI. Он простой, быстрый и не требует XML-конфигурации. Добавляем зависимость в pom.xml:
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>5.1.0</version>
</dependency>
Создаем модуль конфигурации:
public class ApplicationModule extends AbstractModule {
@Override
protected void configure() {
bind(EmailService.class).to(SmtpEmailService.class);
bind(DatabaseService.class).to(PostgresDatabaseService.class);
}
@Provides
@Singleton
public OrderService provideOrderService(EmailService emailService,
DatabaseService databaseService) {
return new OrderService(emailService, databaseService);
}
}
Обновляем OrderService для работы с Guice:
public class OrderService {
private final EmailService emailService;
private final DatabaseService databaseService;
@Inject
public OrderService(EmailService emailService, DatabaseService databaseService) {
this.emailService = emailService;
this.databaseService = databaseService;
}
public void processOrder(Order order) {
databaseService.save(order);
emailService.sendConfirmation(order.getEmail());
}
}
Главный класс приложения:
public class Application {
public static void main(String[] args) {
Injector injector = Guice.createInjector(new ApplicationModule());
OrderService orderService = injector.getInstance(OrderService.class);
Order order = new Order("123", "user@example.com");
orderService.processOrder(order);
}
}
Тестирование с Mock-объектами
Главное преимущество DI — простота тестирования. Можем легко заменить реальные зависимости на mock-объекты:
public class OrderServiceTest {
@Test
public void testProcessOrder() {
// Arrange
EmailService mockEmailService = mock(EmailService.class);
DatabaseService mockDatabaseService = mock(DatabaseService.class);
OrderService orderService = new OrderService(mockEmailService, mockDatabaseService);
Order order = new Order("123", "test@example.com");
// Act
orderService.processOrder(order);
// Assert
verify(mockDatabaseService).save(order);
verify(mockEmailService).sendConfirmation("test@example.com");
}
}
Конфигурация для разных окружений
В серверных приложениях часто нужны разные конфигурации для dev, test и prod. Вот как это решается:
public class ProductionModule extends AbstractModule {
@Override
protected void configure() {
bind(EmailService.class).to(SmtpEmailService.class);
bind(DatabaseService.class).to(PostgresDatabaseService.class);
}
}
public class TestModule extends AbstractModule {
@Override
protected void configure() {
bind(EmailService.class).to(MockEmailService.class);
bind(DatabaseService.class).to(InMemoryDatabaseService.class);
}
}
public class Application {
public static void main(String[] args) {
Module module = "production".equals(System.getProperty("env"))
? new ProductionModule()
: new TestModule();
Injector injector = Guice.createInjector(module);
OrderService orderService = injector.getInstance(OrderService.class);
// Запуск приложения
}
}
Продвинутые возможности: Scope и Lifecycle
В реальных приложениях важно управлять жизненным циклом объектов. Вот пример с разными scope:
public class WebModule extends AbstractModule {
@Override
protected void configure() {
bind(EmailService.class).to(SmtpEmailService.class).in(Singleton.class);
bind(DatabaseService.class).to(PostgresDatabaseService.class).in(Singleton.class);
bind(OrderService.class).in(RequestScoped.class); // Новый на каждый запрос
}
}
// Кастомный scope для connection pool
public class ConnectionPoolModule extends AbstractModule {
@Override
protected void configure() {
bind(ConnectionPool.class).toProvider(ConnectionPoolProvider.class).in(Singleton.class);
}
}
public class ConnectionPoolProvider implements Provider {
@Override
public ConnectionPool get() {
return new ConnectionPool(
System.getProperty("db.url", "jdbc:postgresql://localhost:5432/mydb"),
System.getProperty("db.user", "user"),
System.getProperty("db.password", "password")
);
}
}
Интеграция с веб-серверами
Для серверных приложений особенно важна интеграция с HTTP-серверами. Пример с Jetty:
public class WebServerModule extends AbstractModule {
@Override
protected void configure() {
bind(EmailService.class).to(SmtpEmailService.class);
bind(DatabaseService.class).to(PostgresDatabaseService.class);
bind(OrderController.class);
}
}
public class OrderController {
private final OrderService orderService;
@Inject
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
public void handleRequest(HttpServletRequest request, HttpServletResponse response) {
String orderId = request.getParameter("orderId");
String email = request.getParameter("email");
Order order = new Order(orderId, email);
orderService.processOrder(order);
response.setStatus(200);
}
}
public class WebApplication {
public static void main(String[] args) throws Exception {
Injector injector = Guice.createInjector(new WebServerModule());
Server server = new Server(8080);
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/");
// Интеграция с DI
OrderController controller = injector.getInstance(OrderController.class);
context.addServlet(new ServletHolder(new HttpServlet() {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
controller.handleRequest(req, resp);
}
}), "/orders");
server.setHandler(context);
server.start();
server.join();
}
}
Мониторинг и отладка DI-контейнера
В продакшене важно понимать, что происходит с DI-контейнером. Добавляем логирование:
public class MonitoringModule extends AbstractModule {
@Override
protected void configure() {
bindListener(Matchers.any(), new TypeListener() {
@Override
public void hear(TypeLiteral type, TypeEncounter encounter) {
encounter.register(new InjectionListener() {
@Override
public void afterInjection(I injectee) {
System.out.println("Created: " + injectee.getClass().getSimpleName());
}
});
}
});
}
}
Автоматизация и скрипты развертывания
Для VPS и выделенных серверов полезно автоматизировать развертывание приложений с DI:
#!/bin/bash
# deploy.sh
# Настройка окружения
export ENV=production
export DB_URL=jdbc:postgresql://localhost:5432/production_db
export DB_USER=prod_user
export DB_PASSWORD=secure_password
# Сборка приложения
mvn clean package -DskipTests
# Запуск с правильными параметрами
java -Denv=$ENV \
-Ddb.url=$DB_URL \
-Ddb.user=$DB_USER \
-Ddb.password=$DB_PASSWORD \
-jar target/myapp-1.0.jar
Dockerfile для контейнеризации:
FROM openjdk:11-jre-slim
WORKDIR /app
COPY target/myapp-1.0.jar app.jar
# Настройка переменных окружения
ENV ENV=production
ENV DB_URL=jdbc:postgresql://db:5432/mydb
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
Нестандартные способы использования
Несколько интересных трюков с DI:
- Plugin Architecture — загрузка модулей во время выполнения
- Feature Toggles — включение/отключение функций через DI
- A/B Testing — разные реализации для разных пользователей
- Circuit Breaker — автоматическое переключение на fallback-реализации
Пример feature toggle:
public class FeatureToggleModule extends AbstractModule {
@Override
protected void configure() {
boolean newFeatureEnabled = Boolean.parseBoolean(
System.getProperty("feature.new.enabled", "false")
);
if (newFeatureEnabled) {
bind(PaymentService.class).to(NewPaymentService.class);
} else {
bind(PaymentService.class).to(LegacyPaymentService.class);
}
}
}
Производительность и оптимизация
DI может влиять на производительность. Несколько советов:
- Используйте Singleton для тяжелых объектов
- Избегайте циклических зависимостей
- Lazy initialization для редко используемых сервисов
- Compile-time DI (Dagger) для критически важных по скорости приложений
public class OptimizedModule extends AbstractModule {
@Override
protected void configure() {
// Expensive objects as singletons
bind(ConnectionPool.class).in(Singleton.class);
bind(CacheManager.class).in(Singleton.class);
// Lazy initialization
bind(HeavyService.class).toProvider(new Provider() {
private HeavyService instance;
@Override
public HeavyService get() {
if (instance == null) {
instance = new HeavyService();
}
return instance;
}
});
}
}
Заключение и рекомендации
Dependency Injection — это не просто паттерн, а философия написания гибкого кода. Особенно важно для серверных приложений, где нужна возможность легко менять конфигурацию под разные окружения.
Когда использовать DI:
- Серверные приложения с множеством компонентов
- Проекты, где важно unit-тестирование
- Системы с частыми изменениями бизнес-логики
- Микросервисная архитектура
Мои рекомендации:
- Начинайте с простых решений — не нужен Spring для маленького проекта
- Guice — отличный выбор для изучения и средних проектов
- Spring — когда нужна полная экосистема
- Dagger 2 — для Android или высокопроизводительных систем
Важно помнить: DI — это инструмент, а не цель. Не усложняйте код ради самого DI. Используйте его там, где он действительно решает проблемы. В правильных руках DI превращает спагетти-код в элегантную, тестируемую систему.
Полезные ссылки:
В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.
Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.