Lazy Loading в Java

от автора

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

Вы когда-нибудь замечали, как котики, лениво потягиваясь и сворачиваясь клубком, экономят энергию и действуют только тогда, когда это действительно необходимо?

Как и наши хвостатые друзья, существует такой паттерн как Lazy Loading, который позволяет экономить ресурсы, инициализируя объекты только тогда, когда они действительно нужны.

Рассмотрим, как мы можем применить этот котиковый подход в Java. Будем как котики — умными, экономными и эффективными!

Реализация

В Java существует несколько основных подходов к реализации Lazy Loading: Lazy Initialization, Proxy и Holder.

Lazy Initialization

Lazy Initialization предполагает отложенную инициализацию объекта до первого вызова, при котором он необходим. Это один из самых базовых способов реализации Lazy Loading:

public class LazyInitializedSingleton {     private static LazyInitializedSingleton instance;      private LazyInitializedSingleton() {         // private constructor     }      public static LazyInitializedSingleton getInstance() {         if (instance == null) {             instance = new LazyInitializedSingleton();         }         return instance;     }      public void displayMessage() {         System.out.println("Lazy Initialization Singleton instance.");     } }  public class Main {     public static void main(String[] args) {         LazyInitializedSingleton instance = LazyInitializedSingleton.getInstance();         instance.displayMessage();     } }

Объект LazyInitializedSingleton создается только при первом вызове метода getInstance(). Хоть выглядит и просто, но по сути это не является потокобезопасным.

Для потокобезопасности можно использовать синхронизацию:

public class ThreadSafeLazyInitializedSingleton {     private static ThreadSafeLazyInitializedSingleton instance;      private ThreadSafeLazyInitializedSingleton() {         // private constructor     }      public static synchronized ThreadSafeLazyInitializedSingleton getInstance() {         if (instance == null) {             instance = new ThreadSafeLazyInitializedSingleton();         }         return instance;     }      public void displayMessage() {         System.out.println("Thread-Safe Lazy Initialization Singleton instance.");     } }

Proxy

Паттерн Proxy позволяет контролировать доступ к объекту, отложив его создание до момента первого обращения. В Java можно использовать динамические прокси или вручную реализовать прокси-классы. Например, с динамическим прокси:

import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy;  interface Image {     void display(); }  class RealImage implements Image {     private String filename;      public RealImage(String filename) {         this.filename = filename;         loadImageFromDisk();     }      private void loadImageFromDisk() {         System.out.println("Loading " + filename);     }      public void display() {         System.out.println("Displaying " + filename);     } }  class ImageProxyHandler implements InvocationHandler {     private String filename;     private Image realImage;      public ImageProxyHandler(String filename) {         this.filename = filename;     }      @Override     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {         if (realImage == null) {             realImage = new RealImage(filename);         }         return method.invoke(realImage, args);     } }  public class Main {     public static void main(String[] args) {         Image imageProxy = (Image) Proxy.newProxyInstance(                 Image.class.getClassLoader(),                 new Class[]{Image.class},                 new ImageProxyHandler("test.jpg"));          imageProxy.display();  // изображение загружается и отображается     } }

ImageProxyHandler откладывает создание объекта RealImage до первого вызова метода display().

Holder

Подход Holder реализует ленивую инициализацию с использованием вложенного статического класса. Веьсма потокобезопасно и обеспечивает ленивую инициализацию без необходимости синхронизации:

public class HolderSingleton {     private HolderSingleton() {         // private constructor     }      private static class Holder {         private static final HolderSingleton INSTANCE = new HolderSingleton();     }      public static HolderSingleton getInstance() {         return Holder.INSTANCE;     }      public void displayMessage() {         System.out.println("Holder Singleton instance.");     } }  public class Main {     public static void main(String[] args) {         HolderSingleton instance = HolderSingleton.getInstance();         instance.displayMessage();     } }

Класс Holder содержит статическое поле INSTANCE, которое инициализируется только при первом вызове метода getInstance().

Lazy Loading в библиотеках и фреймворках

Hibernate

В Hibernate, Lazy Loading можно настроить с помощью аннотации @ManyToOne, @OneToMany, @OneToOne, @ManyToMany и указания атрибута fetch = FetchType.LAZY:

@Entity public class Company {     @Id     @GeneratedValue(strategy = GenerationType.IDENTITY)     private Long id;     private String name;      @OneToMany(mappedBy = "company", fetch = FetchType.LAZY)     private List<Employee> employees;      // getters and setters }  @Entity public class Employee {     @Id     @GeneratedValue(strategy = GenerationType.IDENTITY)     private Long id;     private String name;      @ManyToOne(fetch = FetchType.LAZY)     @JoinColumn(name = "company_id")     private Company company;      // getters and setters }

При загрузке компании, связанные с ней сотрудники не будут загружены сразу, а будут загружены только при первом доступе к полю employees.

Могут возникнуть некоторые ошибки при работе с Lazy в Hibernate:

LazyInitializationException возникает, когда ленивые данные пытаются быть загружены за пределами сессии.

Решение:

  1. Использование @Transactional: обеспечивает, что сессия Hibernate активна при доступе к ленивым коллекциям.

@Service public class CompanyService {     @Autowired     private CompanyRepository companyRepository;      @Transactional     public Company getCompanyWithEmployees(Long companyId) {         Company company = companyRepository.findById(companyId).orElseThrow();         // доступ к ленивой коллекции         company.getEmployees().size();         return company;     } }
  1. Инициализация внутри транзакции: загружать ленивые данные в пределах активной транзакции.

@EntityGraph(attributePaths = {"employees"}) @Query("SELECT c FROM Company c WHERE c.id = :id") Optional<Company> findByIdWithEmployees(@Param("id") Long id);

@Lazy в Spring

Spring предоставляет аннотацию @Lazy для ленивой инициализации бинов. В основном юзают для уменьшения времени старта приложения и оптимизации использования ресурсов.

Пример:

@Configuration public class AppConfig {      @Bean     @Lazy     public ServiceBean serviceBean() {         return new ServiceBean();     } }  @Component public class ClientBean {      private final ServiceBean serviceBean;      @Autowired     public ClientBean(@Lazy ServiceBean serviceBean) {         this.serviceBean = serviceBean;     }      public void doSomething() {         serviceBean.performAction();     } }

Бин ServiceBean будет инициализирован только при первом доступе к нему через ClientBean.

Примеры конфигураций:

Конфигурация контекста:

@Lazy @Configuration @ComponentScan(basePackages = "com.example.lazy") public class LazyConfig {      @Bean     public MainService mainService() {         return new MainService();     }      @Bean     @Lazy     public SecondaryService secondaryService() {         return new SecondaryService();     } }

Тестирование ленивой инициализации:

@RunWith(SpringRunner.class) @ContextConfiguration(classes = LazyConfig.class) public class LazyInitializationTest {      @Autowired     private ApplicationContext context;      @Test     public void testLazyInitialization() {         assertFalse(context.containsBean("secondaryService"));         MainService mainService = context.getBean(MainService.class);         mainService.callSecondaryService();         assertTrue(context.containsBean("secondaryService"));     } }

В тесте проверяется, что бин secondaryService не создается при старте контекста, но создается при первом доступе через метод callSecondaryService.


Lazy loading следует применять в тех случаях, когда требуется отложенная загрузка ресурсов или данных для улучшения скорости загрузки.

Однако, не стоит злоупотреблять lazy loading, так как это может привести к нежелательным задержкам и проблемам с производительностью. Например, если объекты часто запрашиваются и необходимы сразу после инициализации, lazy loading может привести к излишней нагрузке на систему.


В завершение приглашаю Java-разработчиков на открытые уроки от Otus:

  • 11 июня: Применение batch-операций в Jdbc. Научимся максимально быстро и эффективно сохранить в базу данных сотни строк сразу. Регистрация по ссылке

  • 25 июня: Redis и Java приложения. Посмотрим, как в java приложениях можно
    использовать Redis в качестве in-memory кеша, для каких задач это может быть полезно. Регистрация по ссылке


ссылка на оригинал статьи https://habr.com/ru/articles/819357/