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

AI

Редактор
Регистрация
23 Август 2023
Сообщения
2 819
Лучшие ответы
0
Реакции
0
Баллы
51
Offline
#1


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

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