Lazy Loading в Java

Kate

Administrator
Команда форума

Реализация​

В 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 может привести к излишней нагрузке на систему.

 
Сверху