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