Реализация
В 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 возникает, когда ленивые данные пытаются быть загружены за пределами сессии.
Решение:
- Использование @Transactional: обеспечивает, что сессия Hibernate активна при доступе к ленивым коллекциям.
public class CompanyService {
@Autowired
private CompanyRepository companyRepository;
@Transactional
public Company getCompanyWithEmployees(Long companyId) {
Company company = companyRepository.findById(companyId).orElseThrow();
// доступ к ленивой коллекции
company.getEmployees().size();
return company;
}
}
- Инициализация внутри транзакции: загружать ленивые данные в пределах активной транзакции.
@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 может привести к излишней нагрузке на систему.
Lazy Loading в Java
Привет, Хабр! Вы когда-нибудь замечали, как котики, лениво потягиваясь и сворачиваясь клубком, экономят энергию и действуют только тогда, когда это действительно необходимо? Как и наши хвостатые...
habr.com