Привет, Хабр!
Если вы хоть раз писали хоть что‑то сложнее REST‑контроллера в Spring, вы наверняка ловили больную ситуацию: два бина зависят друг от друга, инициализация идёт по кругу, и вот он — BeanCurrentlyInCreationException. И если в этот момент вы вспомнили про @Lazy — вы молодцы.
Сегодня я расскажу, как @Lazy может быть полезен, где он только делает вид, что спасает, и какие альтернативы работают лучше.
Что такое @Lazy и зачем он вообще нужен
Spring создает бины eagerly — то есть при старте контекста. Это удобно до тех пор, пока не начинается танец с зависимостями. А вот если у вас бин A зависит от B, а B от A — Spring начинает страдать и в какой‑то момент падает. Тут и полезен@Lazy.
Когда вы помечаете зависимость аннотацией @Lazy, Spring вместо настоящего бина внедряет прокси. И только когда вы реально обращаетесь к зависимости — бин создаётся. Лениво и по требованию.
@Component class A { private final B b; public A(@Lazy B b) { this.b = b; } public void process() { b.doWork(); } } @Component class B { private final A a; public B(@Lazy A a) { this.a = a; } public void doWork() { System.out.println("Work done"); } }
Где @Lazy действительно спасает
Есть опыт использования @Lazy на одном старом сервисе, где нужно было связать огромный ORM‑слой с хелперами, которые тоже использовали часть бизнес‑логики. Бины сцеплялись так, что получался красивый круг. Переписывать — долго. Втыкнуть @Lazy — быстро. И это работало.
Другой пример — бин, который обращается к heavy‑weight SOAP‑клиенту. Запуск бина занимал десятки секунд. Но этот функционал использовался только в отчётах по расписанию. С @Lazy клиент не грузился при старте, и приложение летало.
@Service class ReportGenerator { private final SoapClient client; public ReportGenerator(@Lazy SoapClient client) { this.client = client; } public void generate() { client.fetchData(); } }
Типичные случаи, где это оправдано:
-
Циклические зависимости (A → B → A).
-
Медленные на старте сервисы (SOAP, JDBC, сторонние API).
-
Редко используемые штуки — например, импорт через CSV, который вызывается раз в месяц вручную.
Где @Lazy не работает — и даже вредит
В @Configuration
@Configuration public class AppConfig { @Bean public A a(@Lazy B b) { return new A(b); } @Bean public B b(A a) { return new B(a); } }
Здесь @Lazy может проигнорироваться, если Spring решит, что можно сделать оптимизацию. В проде это вылезло тем, что бин создавался раньше, чем ожидалось — и падал с NPE внутри метода init().
В @Async
@Service public class TaskService { @Autowired @Lazy private HeavyProcessor processor; @Async public void runTask() { processor.process(); // NullPointerException } }
Асинхронные прокси живут своей жизнью. Оборачиваются они через Spring AOP, а @Lazy‑прокси — через JDK/CGLIB. Миксовать одно с другим — не очень.
В @Transactional
@Service public class PaymentService { @Autowired @Lazy private InvoiceService invoiceService; @Transactional public void charge() { invoiceService.issue(); } }
Здесь бин invoiceService подменяется прокси, но @Transactional уже использует другой слой проксирования. В итоге все ломается тихо: транзакции не откатываются, а Lazy не инициализируется.
Почему @Lazy — не решение от всех бед
Типичная ошибка — «починили» циклическую зависимость, воткнули @Lazy, и забыли. Через два месяца вы меняете код, вызываете метод до инициализации контекста — и всё, NullPointerException, потому что бин не проинициализирован.
@PostConstruct public void init() { b.doWork(); // б - ещё null }
Или ещё хуже — бин создается в отдельном потоке, без ApplicationContext. Прокси не может найти бина, и падает с BeanCurrentlyInCreationException, но уже совсем в другом месте.
Альтернатива: ObjectFactory и Provider
Если действительно нужно контролировать момент создания — используйте ObjectFactory или Provider. Примеры:
ObjectFactory
@Component class A { private final ObjectFactory<B> bFactory; public A(ObjectFactory<B> bFactory) { this.bFactory = bFactory; } public void process() { B b = bFactory.getObject(); b.doWork(); } }
Provider (из javax.inject)
@Component class A { private final Provider<B> bProvider; public A(Provider<B> bProvider) { this.bProvider = bProvider; } public void process() { bProvider.get().doWork(); } }
В обоих случаях бин создаётся явно, и это видно в коде.
Когда @Lazy использовать стоит — а когда лучше не надо
Использовать, если:
-
Надо быстро разрулить циклические зависимости без большого рефакторинга.
-
Есть тяжёлые бины, которые не всегда нужны.
-
Временное решение до архитектурного рефакторинга.
Не использовать, если:
-
Бин важен для старта.
-
Бин используется в
@Async,@Transactionalили через EventPublisher. -
Вы не понимаете, как Spring проксирует зависимости в вашем проекте.
@Lazy — не панацея, но полезная вещь
Нельзя сказать «никогда не используйте @Lazy». Я скажу иначе: используйте, если понимаете, зачем.
Если это временная мера, если вы точно знаете, где прокси может выстрелить — окей, живите с этим. Если не уверены — лучше копните архитектуру.
А если у вас есть собственный опыт — кейсы, где @Lazy помог или, наоборот, подвёл — пишите в комментарии.
Если вы уже работаете с Spring, то наверняка сталкивались с необходимостью оптимизировать мониторинг и управление приложениями. А что если у вас есть инструменты, которые сделают этот процесс простым и быстрым? Но как быть с вопросами денормализации в MongoDB, когда нужно принимать взвешенные решения и правильно строить связи?
Не упустите шанс узнать о ключевых аспектах Spring и MongoDB, которые помогут вам повысить эффективность и избежать распространённых проблем. Записывайтесь на открытые уроки:
25 июня — Spring Boot Actuator: основы мониторинга и управления приложением
15 июля — «Нормальная денормализация»
Также вы можете пройти вступительное тестирование курса «Разработчик на Spring Framework», чтобы оценить свой уровень знаний и получить скидку на курс.
ссылка на оригинал статьи https://habr.com/ru/articles/921030/
Добавить комментарий