Как работает @Lazy в Spring — и когда он полезен

от автора

Привет, Хабр!

Если вы хоть раз писали хоть что‑то сложнее 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *