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

Если вы хоть раз писали хоть что‑то сложнее 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», чтобы оценить свой уровень знаний и получить скидку на курс.

Комментарии (0)