Lazy Loading в Java

89051d4a75663ef837ca29599aaa8026.jpg

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

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

Как и наши хвостатые друзья, существует такой паттерн как 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 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 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 кеша, для каких задач это может быть полезно. Регистрация по ссылке

© Habrahabr.ru