Какая проблема возникнет при Self Injection?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Self Injection в Spring: Проблемы и решения
Self Injection — это анти-паттерн в Spring, когда компонент инжектит сам себя. Это выглядит как простое решение на первый взгляд, но создаёт серьёзные проблемы для тестируемости, мониторинга и логики приложения. После 10 лет разработки я часто вижу этот паттерн в legacy коде и всегда пытаюсь его избежать.
Что такое Self Injection?
// ANTI-PATTERN: Класс инжектит самого себя
@Service
public class UserService {
@Autowired
private UserService self; // Self injection!
public void createUser(User user) {
// Основная логика
repository.save(user);
// Вызываем метод через self
self.notifyUser(user); // Зачем?
}
@Transactional
public void notifyUser(User user) {
// Отправляем email
emailService.send(user.getEmail());
}
}
Основная проблема: Обход прокси-объектов
Это главная причина использования self injection, и она выявляет глубокое непонимание того, как работает Spring.
// ДО: НЕ работает благодаря прокси-объектам
@Service
public class UserService {
@Transactional
public void createUser(User user) {
repository.save(user);
notifyUser(user); // Вызов через this!
}
@Transactional
public void notifyUser(User user) {
// Транзакция НЕ применится!
// Причина: вызов через this игнорирует прокси
emailService.send(user.getEmail());
}
}
// ПРАВИЛО Spring:
// - Вызов через @Autowired инстанс → прокси работает ✅
// - Вызов через this → прокси НЕ работает ❌
Проблема 1: Нарушение логики Spring Proxy
// ПРОБЛЕМА: Self injection создаёт мнимое ощущение, что будто работает
@Service
public class OrderService {
@Autowired
private OrderService self; // Сам себя
public void processOrder(Order order) {
// Транзакция 1: открыта
saveOrder(order);
// Надеемся, что здесь откроется новая транзакция
self.notifyPayment(order);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void notifyPayment(Order order) {
// На самом деле НЕ откроется новая транзакция!
// Почему? Потому что self — это реальный объект, не прокси
paymentService.charge(order);
}
}
// РЕЗУЛЬТАТ: Ошибка в платёже откатит весь заказ!
Проблема 2: Сложность тестирования
// Self injection усложняет unit тесты
@Service
public class UserService {
@Autowired
private UserService self; // Self injection
public void registerUser(User user) {
saveUser(user);
self.sendWelcomeEmail(user); // Зависит от самого себя!
}
public void sendWelcomeEmail(User user) {
emailService.send(user.getEmail());
}
}
// ТЕСТ: Очень сложно мокировать
@Test
public void testRegisterUser() {
UserService userService = new UserService();
// Как мокировать sendWelcomeEmail, если она вызывается через self?
// Нельзя просто заменить userService.sendWelcomeEmail = mock
// Приходится создавать сложные шпионские объекты
UserService spyService = Mockito.spy(userService);
spyService.registerUser(new User());
// Проблема: spy работает плохо с self injection
}
Проблема 3: Неопределённое поведение и race conditions
// Self injection может привести к непредсказуемому поведению
@Service
public class PaymentService {
@Autowired
private PaymentService self; // Self injection
public void processPayment(Payment payment) {
self.validatePayment(payment);
// Вызов через self может быть обойдён в конкурентной среде
}
@Transactional
public void validatePayment(Payment payment) {
// Если два потока одновременно вызывают processPayment
// Поведение может отличаться
}
}
Проблема 4: Цикличные зависимости
// Self injection может привести к проблемам инициализации
@Service
public class ServiceA {
@Autowired
private ServiceA self; // Self injection
@Autowired
private ServiceB serviceB; // Зависит от B
}
@Service
public class ServiceB {
@Autowired
private ServiceA serviceA; // B зависит от A
}
// РЕЗУЛЬТАТ: Циклическая зависимость, ошибка инициализации
// BeanCurrentlyInCreationException
Правильные решения
Решение 1: Рефакторинг в отдельный сервис
// ВМЕСТО self injection — разделяй ответственность
@Service
public class UserService {
@Autowired
private UserRepository repository;
@Autowired
private NotificationService notificationService; // Внешний сервис
public void createUser(User user) {
repository.save(user);
notificationService.notifyUser(user);
}
}
@Service
public class NotificationService {
@Autowired
private EmailService emailService;
@Transactional
public void notifyUser(User user) {
emailService.send(user.getEmail());
}
}
// ПРЕИМУЩЕСТВА:
// - Чёткое разделение ответственности
// - Легко тестировать: мокируем NotificationService
// - Spring прокси работает правильно
// - Транзакции работают корректно
Решение 2: ApplicationContext для явного вызова
// ЕСЛИ ОЧЕНЬ НУЖЕН self, используй ApplicationContext
@Service
public class UserService implements ApplicationContextAware {
@Autowired
private UserRepository repository;
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext context) {
this.applicationContext = context;
}
public void createUser(User user) {
repository.save(user);
// Получи прокси через контекст
UserService proxy = applicationContext.getBean(UserService.class);
proxy.notifyUser(user); // Теперь прокси РАБОТАЕТ
}
@Transactional
public void notifyUser(User user) {
emailService.send(user.getEmail());
}
}
// Это лучше, чем self injection, но всё ещё не идеально
Решение 3: ObjectProvider для ленивой загрузки
// Используй ObjectProvider для более элегантного решения
@Service
public class UserService {
@Autowired
private UserRepository repository;
@Autowired
private ObjectProvider<UserService> userServiceProvider;
public void createUser(User user) {
repository.save(user);
// Получи прокси через ObjectProvider
userServiceProvider.getIfAvailable()
.ifPresent(service -> service.notifyUser(user));
}
@Transactional
public void notifyUser(User user) {
emailService.send(user.getEmail());
}
}
Решение 4: Правильная архитектура
// ЛУЧШИЙ ПОДХОД: Правильная архитектура
// 1. Разделяй на слои
@Service
public class UserApplicationService { // Application layer
@Autowired
private UserDomainService userDomainService;
@Autowired
private NotificationService notificationService;
@Transactional
public void registerUser(UserDTO dto) {
User user = userDomainService.create(dto);
notificationService.sendWelcome(user);
}
}
@Service
public class UserDomainService { // Domain layer
@Autowired
private UserRepository repository;
public User create(UserDTO dto) {
User user = new User(dto);
return repository.save(user);
}
}
@Service
public class NotificationService { // Application layer
@Autowired
private EmailService emailService;
public void sendWelcome(User user) {
emailService.send(user.getEmail(), "Welcome!");
}
}
// ПРЕИМУЩЕСТВА:
// - Чёткая архитектура
// - Легко расширять
// - Простое тестирование
// - Никакие self injection не нужны
Когда МОЖЕТ быть self injection?
Очень редко, но есть легитимные case'ы:
// 1. Кеширование через аспект
@Service
public class CachedService {
@Autowired
private CachedService self;
public void process() {
self.cachedMethod(); // Кеширование через прокси
}
@Cacheable("data")
public String cachedMethod() {
// Это работает, если используется прокси
return expensiveOperation();
}
}
// 2. Обход циклических зависимостей (очень редко)
// Но лучше пересмотри архитектуру
Как выявить self injection в коде?
// Паттерн для поиска в IDE:
// Ищи: @Autowired.*Self
// или: @Autowired.*this.class
// Проверь:
git grep -n "@Autowired" | grep -i "self\|@Service.*\n.*@Autowired.*this"
Правила для избежания
1. Никогда не инжектируй сам свой класс
// ❌ ПЛОХО
@Autowired
private UserService self;
// ✅ ХОРОШО
@Autowired
private OtherService otherService;
2. Используй отдельные сервисы для разных ответственностей
// ❌ ПЛОХО: Одна большая служба
@Service
public class UserService {
public void createUser() { }
public void notifyUser() { }
public void auditUser() { }
public void reportUser() { }
}
// ✅ ХОРОШО: Разделённые сервисы
@Service
public class UserService { }
@Service
public class UserNotificationService { }
@Service
public class UserAuditService { }
@Service
public class UserReportService { }
3. Если нужно вызвать метод с AOP — переделай
// ❌ ПЛОХО: Self injection для транзакции
@Service
public class UserService {
@Autowired
private UserService self;
public void register(User user) {
self.saveInTransaction(user);
}
@Transactional
public void saveInTransaction(User user) { }
}
// ✅ ХОРОШО: Извлеки логику
@Service
public class UserService {
@Transactional
public void register(User user) {
// Всё уже в транзакции
}
}
Вывод
Self injection — это anti-pattern в Spring, создающий:
- Проблемы с прокси-объектами
- Сложности при тестировании
- Непредсказуемое поведение транзакций
- Нарушение принципов SOLID
Вместо self injection:
- Разделяй ответственность на разные сервисы
- Используй правильную архитектуру
- Инжектируй внешние сервисы через конструктор
- Применяй DDD принципы
Чистый код без self injection намного проще поддерживать и расширять.