Home » Шаблон внедрения зависимостей в Java — пример и урок
Шаблон внедрения зависимостей в Java — пример и урок

Шаблон внедрения зависимостей в 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 превращает спагетти-код в элегантную, тестируемую систему.

Полезные ссылки:


В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.

Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.

Leave a reply

Your email address will not be published. Required fields are marked