
Привет, Хабр!
Вы когда-нибудь замечали, как котики, лениво потягиваясь и сворачиваясь клубком, экономят энергию и действуют только тогда, когда это действительно необходимо?
Как и наши хвостатые друзья, существует такой паттерн как 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 возникает, когда ленивые данные пытаются быть загружены за пределами сессии.
Решение:
-
Использование @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; } }
-
Инициализация внутри транзакции: загружать ленивые данные в пределах активной транзакции.
@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/
Добавить комментарий