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

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

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

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


  1. GospodinKolhoznik
    06.06.2024 06:25

    А где в первом примере лень? Обычный синглтон, ни разу не ленивый. getInstance() вызывает конструктор объекта.

    Лень была бы если бы getInstance не запускала конструктор, а давала бы некоторый дополнительный объект, который содержал бы в себе все необходимые методы исходного объекта и в каждом методе сделана проверка того, а создан ли исходный объект, если нет то запускается его конструктор, а потом вызывается метод исходного объекта.

    Да, это муторно, зато это будет настоящая лень.


    1. AstarothAst
      06.06.2024 06:25

      Вроде в том и смысл, что пока не вызван getInstance() - объект не существует. Если не будет вызван ни разу - объект лениво никогда не создастся. Ваш вариант отличается только тем, что вызов конструктора перемещается в метод по сути проксика. Смысл это имеет только в одном случае - если мы допустим, что getInstance() вызывается в холостую и целевой объект нам не нужен, но зачем такое допускать? Если такое произойдет значит это облажался программист, пусть идет и исправляет.


      1. GospodinKolhoznik
        06.06.2024 06:25

        В строчке `LazyInitializedSingleton instance = LazyInitializedSingleton.getInstance();` конструктор зачем-то вызывается, объект инициализируется, но при этом потребности в нём ещё нет. Потребность появляется только на следующей строчке. А могла бы и не возникнуть.

        И что значит программист облажался? Почти всегда доподленно неизвестно, будет ли instance вообще использоваться хоть раз или нет. В приведенном коде очевидно, что instance нужен, но если бы instance использовался где то в другой функции, куда бы он передавался как аргумент, то зачастую очень сложно понять - а дейсвительно ли он нужен будет той функции, или в процессе исполнения функция обойдется без него?

        Поэтому я и написал, что LazyInitializedSingleton вовсе не ленивый - его невозможно передать какой либо функции в качестве аргумента, не запустив конструктор. Невозможно положить его в хэш-мапу вместе с другими ленивыми объектами, чтобы дергать их оттуда по мере необходимости. Неизбежно потребуется создавать класс-обёртку, или что-то ещё городить, насколько фантазия позволит.

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


  1. DenSigma
    06.06.2024 06:25

    Тэкс, а гле ломбоковский lazy?

    @Getter(lazy = true)


  1. m_chrom
    06.06.2024 06:25

    Делать весь метод getInstance() в ThreadSafeLazyInitializedSingleton synchronized все-таки несколько расточительно. Lock монитора не бесплатный, а нужен он ровно для одного вызова, который инициализирует переменную.
    Еще, кажется, будут проблемы с тем, что поле instance не volatile, а значит может быть закэшировано тредом. Но тут не уверен, надо перечитать доку что там гарантирует JMM в этих случаях.

    PS: не покритиковать ради, а из заботы о новичках. Люди могут потащить код из примеров в проекты и подложить себе мин замедленного действия. А так материал крутой и полезный.