<?xml version="1.0" encoding="utf-8" ?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:tt="http://teletype.in/" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"><title>Java</title><author><name>Java</name></author><id>https://teletype.in/atom/javalib</id><link rel="self" type="application/atom+xml" href="https://teletype.in/atom/javalib?offset=0"></link><link rel="alternate" type="text/html" href="https://teletype.in/@javalib?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=javalib"></link><link rel="next" type="application/rss+xml" href="https://teletype.in/atom/javalib?offset=10"></link><link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></link><updated>2026-04-29T05:12:17.557Z</updated><entry><id>javalib:ooNKBUsUctO</id><link rel="alternate" type="text/html" href="https://teletype.in/@javalib/ooNKBUsUctO?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=javalib"></link><title>Как мы использовали Codex, чтобы запустить приложение Sora под Android за 28 дней четырьмя инженерами</title><published>2025-12-20T08:05:13.357Z</published><updated>2025-12-20T08:05:13.357Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img4.teletype.in/files/fc/2f/fc2f8524-2a34-4432-b5a2-b3c4e0065d9a.png"></media:thumbnail><category term="android" label="Android"></category><summary type="html">&lt;img src=&quot;https://img1.teletype.in/files/cf/b7/cfb7c7cc-0498-42bb-a95c-54b733a50521.png&quot;&gt;Это статья из официального блога OpenAI, но подход меня так зацепил, что решил перевести для всех. Я тоже часто переношу веб-приложения на мобилки примерно таким же способом и было очень здорово увидеть такой же подход (архитектура-как-пример) у по сути создателей сильного AI. Пишу про разные похожие интересные вещи тут</summary><content type="html">
  &lt;figure id=&quot;6fIP&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/cf/b7/cfb7c7cc-0498-42bb-a95c-54b733a50521.png&quot; width=&quot;1492&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;Z0OM&quot;&gt;&lt;em&gt;Это статья из официального блога OpenAI, но подход меня так зацепил, что решил перевести для всех. Я тоже часто переношу веб-приложения на мобилки примерно таким же способом и было очень здорово увидеть такой же подход (архитектура-как-пример) у по сути создателей сильного AI. Пишу про разные похожие интересные вещи &lt;a href=&quot;https://t.me/ai_product&quot; target=&quot;_blank&quot;&gt;тут&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
  &lt;p id=&quot;OqJY&quot;&gt;В ноябре мы представили миру приложение Sora для Android, предоставив любому пользователю с Android-устройством возможность превращать короткие текстовые промпты в живые видео. В день запуска приложение заняло &lt;strong&gt;1-е место в Play Store&lt;/strong&gt;. За первые 24 часа пользователи Android сгенерировали более миллиона видеороликов.&lt;/p&gt;
  &lt;p id=&quot;ShdJ&quot;&gt;За этим запуском стоит история: первая версия продакшн-приложения Sora для Android была создана всего за 28 дней благодаря тому же агенту, который доступен любой команде или разработчику – Codex.&lt;/p&gt;
  &lt;p id=&quot;nLRg&quot;&gt;С 8 октября по 5 ноября 2025 года небольшая команда инженеров, работая бок о бок с Codex и израсходовав примерно &lt;strong&gt;5 миллиардов токенов (вау)&lt;/strong&gt;, провела Sora для Android от прототипа до глобального запуска. Несмотря на скорость разработки и масштаб, приложение демонстрирует показатель стабильности (crash-free) &lt;strong&gt;99,9%&lt;/strong&gt; и архитектуру, которой мы гордимся. Если вам интересно, использовали ли мы какую-то секретную модель – нет, мы использовали раннюю версию модели &lt;strong&gt;GPT-5.1-Codex&lt;/strong&gt;, ту самую, которую любой разработчик или компания могут использовать уже сегодня через CLI, расширение для IDE или веб-приложение.&lt;/p&gt;
  &lt;h2 id=&quot;pMTY&quot;&gt;Принимая закон Брукса: оставаться гибкими, чтобы двигаться быстро&lt;/h2&gt;
  &lt;p id=&quot;WkA7&quot;&gt;Когда Sora вышла на iOS, использование взлетело моментально. Люди тут же начали генерировать потоки видео. На Android же у нас был только небольшой внутренний прототип и растущее число предрегистраций в Google Play.&lt;/p&gt;
  &lt;p id=&quot;DAzH&quot;&gt;Обычная реакция на запуск с высокими ставками и сжатыми сроками – навалиться ресурсами и усилить процессы. Продакшн-приложение такого масштаба и качества обычно требует работы множества инженеров в течение нескольких месяцев, замедляемых необходимостью координации.&lt;/p&gt;
  &lt;p id=&quot;grZE&quot;&gt;Американский архитектор ПО Фред Брукс знаменит своим предостережением: &lt;em&gt;«Добавление людей в запаздывающий проект лишь делает его еще более запаздывающим»&lt;/em&gt;. Другими словами, когда вы пытаетесь быстро выпустить сложный проект, добавление новых инженеров часто снижает эффективность из-за накладных расходов на коммуникацию, фрагментацию задач и интеграцию. Мы решили не игнорировать этот инсайт, а опереться на него. Мы собрали сильную команду всего из &lt;strong&gt;четырех инженеров&lt;/strong&gt;, каждый из которых был вооружен Codex для радикального увеличения личного импакта.&lt;/p&gt;
  &lt;p id=&quot;QQvs&quot;&gt;Работая в таком режиме, мы выпустили внутреннюю сборку Sora для Android для сотрудников всего за &lt;strong&gt;18 дней&lt;/strong&gt;, а публичный релиз состоялся спустя еще &lt;strong&gt;10 дней&lt;/strong&gt;. Мы сохранили высокую планку инженерных практик Android, вложились в поддерживаемость кода и придерживались того же уровня надежности, которого ожидали бы от более традиционного проекта. (Мы также продолжаем активно использовать Codex и сегодня для развития приложения и внедрения новых функций).&lt;/p&gt;
  &lt;h2 id=&quot;ugiV&quot;&gt;Онбординг нового сеньор-инженера&lt;/h2&gt;
  &lt;p id=&quot;UKyY&quot;&gt;Чтобы понять, как мы работали с Codex, полезно знать, где он силен, а где ему нужно направление. Относиться к нему как к только что нанятому сеньор-инженеру — отличный подход. Способности Codex позволили нам тратить больше времени на руководство и ревью кода, чем на его написание.&lt;/p&gt;
  &lt;h3 id=&quot;3sEC&quot;&gt;Где Codex нужно руководство&lt;/h3&gt;
  &lt;p id=&quot;eN5l&quot;&gt;Codex пока не умеет идеально догадываться о том, чего ему не сказали (например, о ваших предпочтительных архитектурных паттернах, продуктовой стратегии, реальном поведении пользователей и внутренних нормах или шорткатах). Точно так же Codex не мог видеть, как приложение работает в реальности: он не мог открыть Sora на устройстве, заметить, что скролл «подтормаживает», или почувствовать, что флоу (поток действий пользователя) запутан. Только наша команда могла закрыть эти экспертные задачи.&lt;/p&gt;
  &lt;p id=&quot;iCWw&quot;&gt;Каждый инстанс (экземпляр) требует онбординга. Передача контекста с четкими целями, ограничениями и гайдлайнами «как мы это делаем» была критически важна для того, чтобы Codex работал хорошо. В том же духе, Codex иногда испытывал трудности с глубокими архитектурными решениями: предоставленный сам себе, он мог ввести лишнюю вью-модель там, где мы хотели расширить существующую, или запихнуть в UI-слой логику, которой явно место в репозитории. Его инстинкт — заставить что-то работать, а не приоритезировать чистоту кода в долгосрочной перспективе.&lt;/p&gt;
  &lt;p id=&quot;J2HN&quot;&gt;Мы обнаружили, что крайне полезно поручать Codex создание и поддержку большого количества файлов &lt;a href=&quot;http://agent.md/&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;AGENT.md&lt;/code&gt;&lt;/a&gt; по всей кодовой базе. Это позволило легко применять единые руководства и лучшие практики (best practices) в рамках разных сессий. Например, чтобы гарантировать, что Codex пишет код в соответствии с нашими стайл-гайдами, мы добавили следующие инструкции в наш корневой файл &lt;a href=&quot;http://agents.md/&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
  &lt;pre id=&quot;GyKg&quot;&gt;## Formatting and static checks
- **Always run** &amp;#x60;./gradlew detektFix&amp;#x60; (or for the affected modules) **before committing**. CI will fail if formatting or detekt issues are present.&lt;/pre&gt;
  &lt;h3 id=&quot;Z8lv&quot;&gt;Где Codex превосходит ожидания&lt;/h3&gt;
  &lt;ul id=&quot;o3bJ&quot;&gt;
    &lt;li id=&quot;ukqe&quot;&gt;&lt;strong&gt;Быстрое чтение и понимание больших кодовых баз:&lt;/strong&gt; Codex знает практически все основные языки программирования, что упрощает использование одних и тех же концепций на разных платформах без сложных абстракций.&lt;/li&gt;
    &lt;li id=&quot;0REw&quot;&gt;&lt;strong&gt;Покрытие тестами:&lt;/strong&gt; Codex (уникально) с энтузиазмом пишет юнит-тесты для покрытия широкого спектра кейсов. Не каждый тест был глубоким, но широта покрытия помогла предотвратить регрессии.&lt;/li&gt;
    &lt;li id=&quot;LBCE&quot;&gt;&lt;strong&gt;Реакция на фидбэк:&lt;/strong&gt; В том же ключе, Codex хорош в реагировании на обратную связь. Когда падал CI, мы могли просто вставить вывод лога в промпт и попросить Codex предложить исправления.&lt;/li&gt;
    &lt;li id=&quot;0brA&quot;&gt;&lt;strong&gt;Массовое параллельное одноразовое выполнение:&lt;/strong&gt; Большинство не упрется в лимит количества сессий, которые можно запустить одновременно. Вполне реально тестировать несколько идей параллельно и относиться к коду как к расходному материалу.&lt;/li&gt;
    &lt;li id=&quot;gJXh&quot;&gt;&lt;strong&gt;Предложение новой перспективы:&lt;/strong&gt; В дизайн-обсуждениях мы использовали Codex как генеративный инструмент для исследования потенциальных точек отказа и поиска новых способов решения проблем. Например, при проектировании оптимизации памяти видеоплеера Codex просеял множество SDK, чтобы предложить подходы, на разбор которых у нас не было бы времени. Инсайты из «исследования» Codex оказались бесценны для минимизации потребления памяти в финальном приложении.&lt;/li&gt;
    &lt;li id=&quot;UlJ4&quot;&gt;&lt;strong&gt;Освобождение для работы высокого уровня:&lt;/strong&gt; На практике мы тратили больше времени на ревью и управление кодом, чем на его написание. При этом Codex очень хорош и в код-ревью, часто отлавливая баги до того, как они попадут в мердж, что повышает надежность.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;gJqK&quot;&gt;Как только мы приняли эти характеристики, наша рабочая модель стала понятнее. Мы полагались на Codex в выполнении огромного объема тяжелой работы внутри хорошо понятных паттернов и четко очерченных границ, в то время как наша команда фокусировалась на архитектуре, пользовательском опыте (UX), системных изменениях и финальном качестве.&lt;/p&gt;
  &lt;h2 id=&quot;kG9V&quot;&gt;Закладка фундамента вручную&lt;/h2&gt;
  &lt;p id=&quot;NJ2W&quot;&gt;Даже лучший новый сеньор не обладает правильной точкой обзора для принятия долгосрочных компромиссных решений сразу же. Чтобы использовать Codex и гарантировать, что его работа будет надежной и поддерживаемой, было ключевым моментом, что мы сами контролировали системный дизайн приложения и ключевые трейд-оффы. Сюда входили формирование архитектуры приложения, модуляризация, внедрение зависимостей (DI) и навигация; мы также сами реализовали аутентификацию и базовые сетевые флоу.&lt;/p&gt;
  &lt;p id=&quot;5SFi&quot;&gt;На этом фундаменте мы реализовали несколько репрезентативных функций от начала до конца (&lt;strong&gt;end-to-end&lt;/strong&gt;). Мы использовали правила, которым должна была следовать вся кодовая база, и документировали общепроектные паттерны по ходу работы. Указывая Codex на эти эталонные функции, мы добились того, что он смог работать более автономно, оставаясь в рамках наших стандартов. Для проекта, который, по нашим оценкам, &lt;strong&gt;на 85% написан Codex&lt;/strong&gt;, тщательно спланированный фундамент позволил избежать дорогостоящего переписывания и рефакторинга. Это было одно из самых важных решений, которые мы приняли.&lt;/p&gt;
  &lt;p id=&quot;dgmi&quot;&gt;Идея была не в том, чтобы как можно быстрее сделать «что-то рабочее», а в том, чтобы создать «что-то, что понимает, как именно мы хотим, чтобы оно работало». Существует много «правильных» способов написать код. Нам не нужно было говорить Codex, что именно делать; нам нужно было показать Codex, что считается «правильным» в нашей команде. Как только мы установили отправную точку и то, как мы любим строить, Codex был готов к старту.&lt;/p&gt;
  &lt;p id=&quot;Ad8a&quot;&gt;Чтобы посмотреть, что получится, мы попробовали промпт: &lt;em&gt;«Создай Android-приложение Sora на основе кода iOS. Вперед»&lt;/em&gt;, но быстро свернули с этого пути. Хотя то, что создал Codex, технически работало, продуктовый опыт был посредственным. А без четкого понимания эндпоинтов, данных и пользовательских флоу, код, написанный «с одного промпта» (single-shot), был ненадежным (даже без использования агента рискованно мерджить тысячи строк кода).&lt;/p&gt;
  &lt;p id=&quot;zDtg&quot;&gt;Мы выдвинули гипотезу, что Codex расцветет в «песочнице» из хорошо написанных примеров; и мы оказались правы. Просить Codex «сделать экран настроек» почти без контекста – ненадежно. Просить Codex «сделать этот экран настроек, используя ту же архитектуру и паттерны, что и вот этот другой экран, который ты только что видел» – работало куда лучше. Люди принимали структурные решения и задавали инварианты; Codex затем заполнял большие объемы кода внутри этой структуры.&lt;/p&gt;
  &lt;h4 id=&quot;hdgN&quot;&gt;Планирование с Codex перед написанием кода&lt;/h4&gt;
  &lt;p id=&quot;tiRN&quot;&gt;Нашим следующим шагом в максимизации потенциала Codex стало понимание того, как позволить Codex работать в течение длительных периодов времени (в последнее время — более 24 часов) без присмотра.&lt;/p&gt;
  &lt;p id=&quot;4Vlx&quot;&gt;В начале использования Codex мы сразу переходили к промптам типа: &lt;em&gt;«Вот фича. Вот файлы. Пожалуйста, собери это»&lt;/em&gt;. Иногда это срабатывало, но чаще выдавало код, который технически компилировался, но отклонялся от нашей архитектуры и целей.&lt;/p&gt;
  &lt;p id=&quot;Zbqg&quot;&gt;Поэтому мы изменили рабочий процесс. Для любого нетривиального изменения мы сначала просили Codex помочь нам понять, как работают система и код. Например, мы просили его прочитать набор связанных файлов и саммаризировать, как работает эта фича; скажем, как данные текут от API через слой репозитория, вью-модель и в UI. Затем мы корректировали или уточняли его понимание. (Например, мы указывали, что конкретная абстракция на самом деле принадлежит другому слою или что данный класс существует только для офлайн-режима и его не следует расширять).&lt;/p&gt;
  &lt;p id=&quot;XESi&quot;&gt;Подобно тому, как вы взаимодействуете с новым, высококвалифицированным коллегой, мы работали с Codex над созданием надежного плана реализации. Этот план часто выглядел как миниатюрный дизайн-документ, указывающий, какие файлы должны измениться, какие новые состояния нужно ввести и как должна проходить логика. Только после этого мы просили Codex начать выполнять план, шаг за шагом. Один полезный совет: для очень длинных задач, где мы упирались в лимит контекстного окна, мы просили Codex сохранить свой план в файл, что позволяло нам применять одно и то же направление в разных инстансах.&lt;/p&gt;
  &lt;p id=&quot;RNus&quot;&gt;Этот дополнительный цикл планирования стоил потраченного времени. Он позволил нам оставлять Codex работать «без присмотра» на долгие отрезки, потому что мы знали его планы. Это упростило код-ревью, так как мы могли сверять реализацию с нашим планом, а не читать дифф (diff) без контекста. И когда что-то шло не так, мы могли сначала отлаживать план, а потом код.&lt;/p&gt;
  &lt;p id=&quot;6tjj&quot;&gt;Динамика ощущалась схожей с тем, как хороший дизайн-документ дает техлиду уверенность в проекте. Мы не просто генерировали код: мы производили код, который поддерживал общий roadmap.&lt;/p&gt;
  &lt;h2 id=&quot;bAan&quot;&gt;Распределенная инженерия&lt;/h2&gt;
  &lt;p id=&quot;GdXq&quot;&gt;На пике проекта мы часто запускали несколько сессий Codex параллельно. Одна работала над воспроизведением видео, другая – над поиском, третья – над обработкой ошибок, а иногда еще одна – над тестами или рефакторингом. Это ощущалось не столько как использование инструмента, сколько как управление командой.&lt;/p&gt;
  &lt;p id=&quot;0Hei&quot;&gt;Каждая сессия периодически отчитывалась нам о прогрессе. Одна могла сказать: &lt;em&gt;«Я закончил планирование этого модуля; вот что я предлагаю»&lt;/em&gt;, в то время как другая предлагала большой дифф для новой фичи. Каждая требовала внимания, фидбэка и ревью. Это было до жути похоже на роль техлида с несколькими новыми инженерами: все двигаются вперед, всем нужно руководство.&lt;/p&gt;
  &lt;p id=&quot;1Wl6&quot;&gt;Результатом стал коллаборативный поток (flow). Чистая способность Codex к кодингу освободила нас от большого количества ручного набора текста. У нас появилось больше времени на обдумывание архитектуры, внимательное чтение пулл-реквестов и тестирование приложения.&lt;/p&gt;
  &lt;p id=&quot;Fygj&quot;&gt;В то же время эта дополнительная скорость означала, что у нас всегда что-то висело в очереди на ревью. Codex не блокировался переключением контекста, а мы – да. Наше «бутылочное горлышко» в разработке сместилось с написания кода на принятие решений, дачу фидбэка и интеграцию изменений.&lt;/p&gt;
  &lt;p id=&quot;ZJe9&quot;&gt;Именно здесь инсайты Брукса раскрываются по-новому. Вы не можете просто добавлять сессии Codex и ожидать линейного ускорения, точно так же, как не можете бесконечно добавлять инженеров в проект, ожидая линейного сокращения сроков. Каждая дополнительная «пара рук», даже виртуальных, добавляет накладные расходы на координацию. Мы стали дирижерами оркестра, а не просто более быстрыми солистами.&lt;/p&gt;
  &lt;h2 id=&quot;Yinq&quot;&gt;Codex как кроссплатформенная суперсила&lt;/h2&gt;
  &lt;p id=&quot;HtKk&quot;&gt;Мы начали наш проект с огромного подспорья: Sora уже вышла на iOS. Мы часто указывали Codex на кодовые базы iOS и бэкенда, чтобы помочь ему понять ключевые требования и ограничения. На протяжении проекта мы шутили, что изобрели идею кроссплатформенного фреймворка заново. Забудьте про React Native или Flutter; будущее кроссплатформы – это просто Codex.&lt;/p&gt;
  &lt;p id=&quot;8MTQ&quot;&gt;Под этой шуткой лежат два принципа:&lt;/p&gt;
  &lt;ol id=&quot;qx6V&quot;&gt;
    &lt;li id=&quot;vpNw&quot;&gt;&lt;strong&gt;Логика портативна.&lt;/strong&gt; Написан ли код на Swift или на Kotlin, лежащая в основе логика приложения – модели данных, сетевые вызовы, правила валидации, бизнес-логика – одинакова. Codex очень хорош в чтении реализации на Swift и создании эквивалента на Kotlin с сохранением семантики.&lt;/li&gt;
    &lt;li id=&quot;2frx&quot;&gt;&lt;strong&gt;Конкретные примеры дают мощный контекст.&lt;/strong&gt; Свежая сессия Codex, которая может видеть «вот как именно это работает на iOS» и «вот архитектура Android», куда эффективнее, чем та, которая работает только по описаниям на естественном языке.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;ic5Z&quot;&gt;Заставив эти принципы работать, мы сделали репозитории iOS, бэкенда и Android доступными в одной среде. Мы давали Codex промпты вроде:&lt;/p&gt;
  &lt;blockquote id=&quot;xM67&quot;&gt;&lt;em&gt;«Прочитай эти модели и эндпоинты в коде iOS, а затем предложи план реализации эквивалентного поведения на Android, используя наш существующий API-клиент и классы моделей».&lt;/em&gt;&lt;/blockquote&gt;
  &lt;p id=&quot;bno0&quot;&gt;Одним маленьким, но полезным трюком было детальное описание в &lt;code&gt;~/.codex/&lt;a href=&quot;http://agents.md/&quot; target=&quot;_blank&quot;&gt;AGENTS.md&lt;/a&gt;&lt;/code&gt;, где находятся локальные репозитории и что они содержат. Это упростило Codex обнаружение и навигацию по релевантному коду.&lt;/p&gt;
  &lt;p id=&quot;E9JU&quot;&gt;Мы, по сути, занимались кроссплатформенной разработкой через перевод, а не через общую абстракцию. Поскольку Codex брал на себя большую часть перевода, мы избежали удвоения затрат на реализацию.&lt;/p&gt;
  &lt;p id=&quot;hBIv&quot;&gt;Более широкий урок заключается в том, что для Codex контекст – это всё. Codex выдавал лучшие результаты, когда понимал, как фича уже работает в iOS, в сочетании с пониманием того, как структурировано наше Android-приложение. Когда Codex не хватало этого контекста, он не «отказывался сотрудничать»; он гадал. Чем больше мы относились к нему как к новому коллеге и вкладывались в предоставление правильных входных данных, тем лучше он работал.&lt;/p&gt;
  &lt;h4 id=&quot;He32&quot;&gt;Программная инженерия завтрашнего дня, уже сегодня&lt;/h4&gt;
  &lt;p id=&quot;LDbd&quot;&gt;К концу нашего четырехнедельного спринта использование Codex перестало ощущаться как эксперимент и стало нашим дефолтным циклом разработки. Мы использовали его для понимания существующего кода, планирования изменений и реализации фич. Мы ревьюили его выдачу так же, как ревьюили бы работу коллеги. Это стало просто способом, которым мы шипим софт.&lt;/p&gt;
  &lt;p id=&quot;h2Rg&quot;&gt;Стало ясно, что AI-ассистированная разработка не снижает потребность в строгости; она ее повышает. Каким бы способным ни был Codex, его цель – добраться из точки А в точку Б, сейчас. Вот почему кодинг с ИИ не работает без людей. Инженеры ПО могут понимать и применять реальные системные ограничения, лучшие способы архитектуры софта и то, как строить с учетом будущих планов развития и продукта. Суперсилами инженера завтрашнего дня станут глубокое системное понимание и способность работать совместно с ИИ на длинных временных горизонтах.&lt;/p&gt;
  &lt;p id=&quot;64Dr&quot;&gt;Самые интересные части программной инженерии – это создание привлекательных продуктов, проектирование масштабируемых систем, написание сложных алгоритмов и эксперименты с данными, паттернами и кодом. Однако реалии инженерии прошлого и настоящего часто склоняются к рутине: центрирование кнопок, связывание эндпоинтов и написание бойлерплейта. Теперь Codex делает возможным сфокусироваться на самых значимых частях программной инженерии и причинах, по которым мы любим наше ремесло.&lt;/p&gt;
  &lt;p id=&quot;xaWE&quot;&gt;Как только Codex настроен в богатой контекстом среде, где он понимает ваши цели и то, как вы любите строить, любая команда может мультиплицировать свои возможности. Наше ретро по запуску – не универсальный рецепт, и мы не утверждаем, что решили проблему AI-разработки. Но мы надеемся, что наш опыт поможет вам найти лучшие способы дать Codex возможность усилить вас.&lt;/p&gt;
  &lt;p id=&quot;qHXX&quot;&gt;Когда Codex запустился в превью для исследователей семь месяцев назад, программная инженерия выглядела совсем иначе. Благодаря Sora нам удалось исследовать следующую главу инженерии. По мере того как наши модели и обвязка продолжают улучшаться, ИИ будет становиться все более незаменимой частью создания продуктов.&lt;/p&gt;
  &lt;p id=&quot;JIY9&quot;&gt;Что вы создадите со своей собственной командой Codex?&lt;/p&gt;
  &lt;p id=&quot;yBQp&quot;&gt;&lt;a href=&quot;https://habr.com/ru/articles/976330/&quot; target=&quot;_blank&quot;&gt;Источник&lt;/a&gt;&lt;/p&gt;

</content></entry><entry><id>javalib:lUqACRjSjVb</id><link rel="alternate" type="text/html" href="https://teletype.in/@javalib/lUqACRjSjVb?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=javalib"></link><title>Опыт отладки хитрой утечки прямой памяти</title><published>2024-09-19T07:47:11.681Z</published><updated>2024-09-19T07:47:11.681Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img2.teletype.in/files/d1/18/d11802bc-7a68-4935-a060-2a75dda57e9e.png"></media:thumbnail><category term="java" label="Java"></category><summary type="html">&lt;img src=&quot;https://img2.teletype.in/files/51/85/5185f5b4-588a-412c-9fda-255ade4855bd.png&quot;&gt;Pinterest поддерживает формирование отчётов по метрикам рекламных объявлений внешних рекламодателей и расчёт рекламных бюджетов в реальном времени. Всё это основано на потоковых конвейерах обработки данных, созданных с помощью на Apache Flink. Доступность заданий (job) Flink для пользователей находится на уровне 99-го перцентиля. Но время от времени некоторые задачи (task) «валятся» под ударами неприятных ошибок, вызванных утечками прямой памяти (Out-Of-Memory, OOM), возникающими сразу в нескольких операторах. Выглядит это примерно так:</summary><content type="html">
  &lt;figure id=&quot;Fu7A&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/51/85/5185f5b4-588a-412c-9fda-255ade4855bd.png&quot; width=&quot;780&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;eXIt&quot;&gt;Pinterest поддерживает формирование отчётов по метрикам рекламных объявлений внешних рекламодателей и расчёт рекламных бюджетов в реальном времени. Всё это основано на потоковых конвейерах обработки данных, созданных с помощью на Apache Flink. Доступность заданий (job) Flink для пользователей находится на уровне 99-го перцентиля. Но время от времени некоторые &lt;a href=&quot;https://nightlies.apache.org/flink/flink-docs-release-1.17/docs/internals/task_lifecycle/#:~:text=A%20task%20in%20Flink%20is,executed%20by%20a%20separate%20task.&quot; target=&quot;_blank&quot;&gt;задачи&lt;/a&gt; (task) «валятся» под ударами неприятных ошибок, вызванных утечками прямой памяти (Out-Of-Memory, OOM), возникающими сразу в нескольких &lt;a href=&quot;https://nightlies.apache.org/flink/flink-docs-master/docs/dev/datastream/operators/overview/#:~:text=Operators%20transform%20one%20or%20more%20DataStreams%20into%20a%20new%20DataStream.%20Programs%20can%20combine%20multiple%20transformations%20into%20sophisticated%20dataflow%20topologies.&quot; target=&quot;_blank&quot;&gt;операторах&lt;/a&gt;. Выглядит это примерно так:&lt;/p&gt;
  &lt;figure id=&quot;4OI9&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/9e/e9/9ee9205f-d0ff-4821-bb33-b65e89d8ecd0.png&quot; /&gt;
    &lt;figcaption&gt;Стек-трейс, сформированный при возникновении OOM-ошибки&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;Kk3m&quot;&gt;Как и в случае с большинством проблем, происходящих в распределённой системе, подобное часто ведёт к целому каскаду сбоев в самых разных местах. Одна такая ошибка оставляет после себя множество ложных следов. Flink‑платформа, используемая в Pinterest, поддерживает автоматический перезапуск задания в том случае, когда объём сбоев превысит настраиваемое пороговое значение. Но, из‑за того, что такое происходит нечасто, мы, для обеспечения отказоустойчивости системы, обычно позволяем произвести автоматический перезапуск с самой свежей контрольной точки. К концу прошлого года мы начали консолидацию кластеров и подкорректировали выделение памяти во всех заданиях ради повышения эффективности использования ресурсов. Неожиданным следствием этого шага стало то, что мы, в начале текущего года, стали получать целые страницы сообщений об утечках прямой памяти. Это повлекло за собой сбои и повлияло на службы, зависящие от нашей системы. Всё более очевидным становилось то, что с этой проблемой надо что‑то делать. В этом материале мы расскажем об используемом нами процессе поиска ошибок и поделимся идеями, которые могут пригодиться при отладке любой крупномасштабной распределённой системы. Такой системы, где одних лишь разумно размещённых инструкций &lt;code&gt;print&lt;/code&gt; для борьбы с ошибками недостаточно.&lt;/p&gt;
  &lt;p id=&quot;p9C4&quot;&gt;Первая часть головоломки заключалась в том, чтобы отделить симптомы проблемы от её первопричины. В ходе инцидента мы обратили внимание на высокий уровень обратного давления (&lt;a href=&quot;https://nightlies.apache.org/flink/flink-docs-master/docs/ops/monitoring/back_pressure/#back-pressure:~:text=this%20means%20that%20it%20is%20producing%20data%20faster%20than%20the%20downstream%20operators%20can%20consume.%20Records%20in%20your%20job%20flow%20downstream%20(e.g.%20from%20sources%20to%20sinks)%20and%20back%20pressure%20is%20pro&quot; target=&quot;_blank&quot;&gt;back pressure&lt;/a&gt;) у нескольких операторов, а так же — на сбои задачи, отражённые на вышеприведённом стрек‑трейсе. Сначала нам показалось, что возможной причиной проблемы могут быть и неполадки уровня контейнера. А именно, у контейнера могла закончиться память в ходе выделения прямой памяти для сетевых буферов, используемых для организации работы каналов ввода/вывода. Это привело к первому набору действий — к искусственному вызову отказов задач и к созданию высокого обратного давления на экземпляре задания, используемого в ходе разработки. При этом мы наблюдали за тем, как всё это воздействует на потребление прямой памяти. Делалось это для того, чтобы установить причинно‑следственную связь между этими двумя событиями.&lt;/p&gt;
  &lt;p id=&quot;PTkm&quot;&gt;Но сначала нам надо было найти временное решение, позволяющее предотвратить частое обращение к дежурным инженерам в то время, пока мы устраняем глубинную причину проблемы. Для того чтобы это сделать, полезно было вспомнить о том, как устроена &lt;a href=&quot;https://nightlies.apache.org/flink/flink-docs-release-1.17/docs/deployment/memory/mem_setup_tm/#detailed-memory-model&quot; target=&quot;_blank&quot;&gt;модель памяти&lt;/a&gt; Flink.&lt;/p&gt;
  &lt;figure id=&quot;hJ60&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/f6/c6/f6c6817b-0766-4c09-bd46-e479e2ce8531.png&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Модель памяти Flink&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;Vfnj&quot;&gt;Как видно на предыдущем рисунке — конфигурацию прямой памяти Flink можно разделить на три части. Это — память фреймворка вне кучи (framework off‑heap memory), память задания вне кучи (task off‑heap memory) и сетевая память (network memory). Память фреймворка вне кучи зарезервирована для внутренних операций Flink и для структур данных. Не зная точно о том, вызвана ли OOM утечкой памяти уровня приложения, мы увеличили и память задания вне кучи и сетевую память с 2 до 5 Гб. Мы сознательно сделали столь щедрый жест, надеясь на то, что «купим» себе таким образом достаточно времени для решения проблемы.&lt;/p&gt;
  &lt;h3 id=&quot;vqtw&quot;&gt;Искусственное создание обратного давления&lt;/h3&gt;
  &lt;p id=&quot;j8TB&quot;&gt;Так как в нашем задании Flink имеется лишь один выходной Sink‑оператор — создание обратного давления сложностей не вызвала. А именно — для этого достаточно было добавить в главный поток длинную паузу, воспользовавшись конструкцией &lt;code&gt;Thread.sleep()&lt;/code&gt;. Так как такой оператор на обрабатывает какие‑либо входные записи, входные буферы всех операторов, находящихся перед ним, быстро переполнятся, что создаст значительное обратное давление.&lt;/p&gt;
  &lt;figure id=&quot;6gsR&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/55/a3/55a32526-f6ed-4577-a131-71ef02eccaa6.png&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Эффект обратного давления на различных операторах приложения. Красные маркеры указывают на наличие обратного давления в заданный момент времени.&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;0xMw&quot;&gt;На рисунке показана ситуация с обратным давлением в различных операторах, возникшая после некоторого времени работы приложения. Это неизменно ведёт к нехватке прямой памяти на узлах, подверженных обратному давлению. А это, в свою очередь, вызывает отказы заданий.&lt;/p&gt;
  &lt;h3 id=&quot;R0oL&quot;&gt;Искусственный перезапуск заданий&lt;/h3&gt;
  &lt;p id=&quot;Onxh&quot;&gt;В Pinterest Flink-приложения отправляют менеджеру ресурсов, входящему в состав &lt;a href=&quot;https://hadoop.apache.org/docs/current/hadoop-yarn/hadoop-yarn-site/YARN.html&quot; target=&quot;_blank&quot;&gt;YARN&lt;/a&gt;. Он распределяет задачи заданий по контейнерам, расположенным на машинах, которыми управляют сущности YARN NodeManager. Для того чтобы сымитировать перезапуск задач, мы останавливали случайным образом выбранные экземпляры контейнеров, используя команду &lt;code&gt;yarn container&lt;/code&gt; &lt;code&gt;-signal [container-id] GRACEFUL_SHUTDOWN&lt;/code&gt;, наблюдая при этом за тем, как приложение потребляет прямую память.&lt;/p&gt;
  &lt;figure id=&quot;Hupj&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/a7/8e/a78e6319-ef6a-443b-aead-fe985dec1a8c.png&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Паттерн потребления прямой памяти при сбоях задачи&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;iIEK&quot;&gt;График на предыдущем рисунке иллюстрирует воздействие искусственно вызванных сбоев на потребление прямой памяти. Он показывает заметное увеличение потребления памяти, возникающее в точности тогда, когда мы останавливаем контейнер. Это, в итоге, вело к OOM‑ошибкам, а когда останавливался кворум контейнеров одного и того же оператора, вызывало возникновение обратного давления на узлах, предшествовавших этому оператору. «Лестничный» паттерн на графике выглядит особенно интригующим, так как это — красноречивое свидетельство утечки памяти. Значит — где‑то в коде была выделена прямая память, которую не освободили должным образом.&lt;/p&gt;
  &lt;p id=&quot;8FRZ&quot;&gt;Для того чтобы сузить сферу поиска, мы решили выяснить — является ли причиной происходящего ошибка платформы, или проблема, связанная с логикой приложения. Чтобы это сделать — мы повторили ручной перезапуск задачи на отдельном приложении, в котором не выполнялась логика нашего задания. Мы хотели понаблюдать за тем, появится ли при этом уже знакомый нам паттерн потребления прямой памяти. Это указало бы на то, что, возможно, во всём виновата ошибка уровня платформы.&lt;/p&gt;
  &lt;figure id=&quot;rCwq&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/1b/5c/1b5ced2b-d9e6-4bb5-aa0c-a86aee5a6870.png&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Паттерн потребления прямой памяти при отказах задачи на отдельном задании&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;dL5j&quot;&gt;Как видно на предыдущем рисунке — в другом Flink‑приложении никаких заметных пиков в потреблении прямой памяти нам заметить не удалось. Это послужило убедительным доказательством того, что источник утечки памяти связан с ошибкой в коде нашего приложения.&lt;/p&gt;
  &lt;h3 id=&quot;5CcA&quot;&gt;Отладка кода приложения&lt;/h3&gt;
  &lt;p id=&quot;pUvh&quot;&gt;Наше Flink‑приложение состоит из нескольких тысяч строк кода. При отладке столь масштабной кодовой базы полезно использовать подход, который можно сравнить с чисткой лука. А именно — речь идёт о том, что код разбивают на небольшие компоненты, исследуемые с целью воспроизведения проблемы. Очень упрощённая схема нашего приложения выглядит так.&lt;/p&gt;
  &lt;figure id=&quot;L5qt&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/dc/94/dc9418dd-3f4d-4e24-8b1e-7e24a5c68985.png&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Высокоуровневый обзор операторов нашего Flink-приложения&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;FAvV&quot;&gt;Первый слой (Layer 1 на рисунке) выполняет чтение из различных топиков Kafka, десериализует данные, формируя внутренние объекты, и передаёт их на второй слой (Layer 2), который объединяет выходные данные и производит некоторые трансформации. Этот слой, кроме того, выполняет кое‑какие RPC‑вызовы к внешнему KVStore, обращаясь к downstream‑службам, после чего передаёт данные третьему слою (Layer 3), который трансформирует данные и передаёт событие в Druid. Эти три слоя заключают в себе группу операторов, использующих прямую память. Вооружённые знаниями об архитектуре приложения, мы можем, в индивидуальном порядке, убирать некоторые из операторов и пытаться воспроизвести проблему, вручную перезапуская задачи. При таком подходе мы можем изолировать оператор, являющийся источником проблемы, и исправить код.&lt;/p&gt;
  &lt;h3 id=&quot;tBKx&quot;&gt;Убираем операторы 2 и 3 слоёв&lt;/h3&gt;
  &lt;p id=&quot;Wtoi&quot;&gt;На прерыдущем рисунке некоторые операторы из второго слоя выполняют RPC‑вызовы к внешнему KVStore с очень большими объёмами передаваемых данных. Мы подозревали, что именно эти большие объекты вызывали OOM‑ошибки в том случае, если объект &lt;code&gt;DirectByteBuffer&lt;/code&gt; из &lt;a href=&quot;https://thrift.apache.org/&quot; target=&quot;_blank&quot;&gt;Thrift&lt;/a&gt; не мог зарезервировать достаточно прямой памяти для выполнения сетевых операций ввода/вывода.&lt;/p&gt;
  &lt;p id=&quot;78kQ&quot;&gt;В слое 3 тоже используется память вне кучи. В ней хранятся курсы обмена валют для различных стран. Эти сведения загружаются из внешнего хранилища данных. Раньше эти вычисления выполнялись с использованием памяти кучи, на которую они создавали очень серьёзную нагрузку. Файл, хранящий обменные курсы, периодически загружался из хранилища, подвергался парсингу для извлечения из него полезной информации, трансформировался в хеш‑карту, которая потом заменяла старую (иммутабельную) хеш‑карту. Старая хеш‑карта затем перемещалась в область памяти, предназначенную для хранения сущностей старого поколения (Old Generation). Это значит, что соответствующая память не освобождалась до следующего вызова полной процедуры сборки мусора. Из‑за большого размера данных онлайн‑приложения, и из‑за того, что полная сборка мусора выполняется нечасто, мы перешли на решение, использующее память вне кучи, применив &lt;a href=&quot;https://chronicle.software/map/&quot; target=&quot;_blank&quot;&gt;ChronicleMap&lt;/a&gt;. При этом проблема, связанная с освобождением этой памяти, вполне может, со временем, привести к OOM‑ошибке. В результате мы начали с того, что убрали эти блоки кода. Далее — мы перезапускали задачи, выбираемые произвольно и связанные с оставшимися операторами, наблюдая при этом за воздействием происходящего на потребление прямой памяти.&lt;/p&gt;
  &lt;figure id=&quot;6v5K&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/37/41/374125cc-da7e-4421-a5d0-661e7b90f5f6.png&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Паттерн потребления прямой памяти при отказах задач, связанных с оставшимися операторами&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;cFqB&quot;&gt;Как и ожидалось — мы не заметили каких‑либо аномалий в потреблении прямой памяти. Это позволило нам сузить область поиска причины утечки памяти до оставшихся операторов.&lt;/p&gt;
  &lt;h4 id=&quot;vaQn&quot;&gt;Убираем операторы слоя 3&lt;/h4&gt;
  &lt;p id=&quot;tWZw&quot;&gt;Теперь мы удалили операторы слоя 3, использующие &lt;code&gt;ChronicleMap&lt;/code&gt; для реализации логики приложения и повторили уже знакомый эксперимент по искусственному перезапуску задач.&lt;/p&gt;
  &lt;figure id=&quot;zR4L&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/c0/45/c0454f26-9f77-42a9-94b5-31b2eb904df2.png&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Паттерн потребления прямой памяти при отказах задач на оставшихся операторах&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;buL6&quot;&gt;Предыдущий рисунок иллюстрирует обнаруженное нами небольшое отклонение, но на нём не наблюдается «лестницы», которая позволила бы сделать вывод об утечке памяти в оставшихся операторах. Это показалось нам интересным, так как, в противовес исходному предположению, мы не смогли найти свидетельств утечки памяти в операторах, которые взаимодействуют с KVStore посредством RPC‑вызовов.&lt;/p&gt;
  &lt;h4 id=&quot;4sje&quot;&gt;Убираем операторы слоя 2&lt;/h4&gt;
  &lt;p id=&quot;Zfq2&quot;&gt;Далее — изолируем операторы третьего слоя, убрав операторы слоя 2, которые тоже используют прямую память.&lt;/p&gt;
  &lt;figure id=&quot;1aU1&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/179/e35/62e/179e3562ea8ed9023d3e3797018ba4aa.png&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Паттерн потребления прямой памяти при отказах задач на оставшихся операторов&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;GN1F&quot;&gt;Вот оно! Мы смогли воспроизвести проблему в урезанном варианте кода приложения. На схеме эта проблема устрашающе похожа на ту, которую мы наблюдали в самом начале, анализируя поведение полной версии кода. Мы нашли убедительные доказательства того, что утечка прямой памяти коренится в коде, относящемся к третьему слою приложения, в котором используется такая память.&lt;/p&gt;
  &lt;h3 id=&quot;YT7P&quot;&gt;Исправление ошибки&lt;/h3&gt;
  &lt;p id=&quot;tFMb&quot;&gt;После исследования проблемного оператора, мы обнаружили, что ссылка на ChronicleMap удалялась, но при этом связанная с ней память не освобождалась, что и приводило к утечке. Эта память не освобождалась до выполнения следующей полной процедуры сборки мусора, что особенно проблематично в онлайн‑службах наподобие нашей, при проектировании которых стремятся к тому, чтобы сборка мусора выполнялась бы не слишком часто.&lt;/p&gt;
  &lt;p id=&quot;4bje&quot;&gt;Для того чтобы лучше с этим разобраться, разумнее всего будет поговорить о &lt;a href=&quot;https://nightlies.apache.org/flink/flink-docs-release-1.14/docs/internals/task_lifecycle/&quot; target=&quot;_blank&quot;&gt;жизненном цикле задач&lt;/a&gt; Flink и о внутреннем механизме их перезапуска. Он используется при остановке задач из‑за сбоев. В такой ситуации JVM продолжает работу, а при выполнении Flink‑кода осуществляется переход на метод &lt;code&gt;close()&lt;/code&gt; оператора, затронутого сбоем. После перезапуска Flink вызовет метод &lt;code&gt;open()&lt;/code&gt;, определённый в коде оператора. Если логика ссылается на объект (вроде &lt;code&gt;ChronicleMap&lt;/code&gt;), находящийся за пределами жизненного цикла оператора, код может непроизвольно вызвать утечку памяти.&lt;/p&gt;
  &lt;figure id=&quot;Lfv7&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/aa1/9e8/f9f/aa19e8f9fe97bbdc949c73d67ee8a56d.png&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Фрагмент кода, иллюстрирующий ручную очистку памяти&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;rCrs&quot;&gt;После исправления утечки мы снова организовали перезапуск задачи и понаблюдали за воздействием этого на потребление прямой памяти.&lt;/p&gt;
  &lt;figure id=&quot;Do62&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/1e0/149/5a4/1e01495a4f9e1be0a3de1d3691bb91ad.png&quot; width=&quot;700&quot; /&gt;
    &lt;figcaption&gt;Паттерн потребления прямой памяти при сбоях задачи, наблюдаемый после исправления проблемы, связанной с утечкой памяти&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;TO8k&quot;&gt;Как видно на предыдущем рисунке, мы наблюдаем ровную линию, описывающую характер потребления памяти. Она сильно отличается от той «лестницы», которую мы видели в самом начале.&lt;/p&gt;
  &lt;h3 id=&quot;USV0&quot;&gt;Итоги&lt;/h3&gt;
  &lt;p id=&quot;iCBP&quot;&gt;Код, устраняющий утечку памяти, тесно связан с логикой нашего приложения. Но главным, универсальным результатом нашей работы стала сама процедура нахождения первопричины проблемы. На примере истории борьбы с нашим сбоем мы прошли через девять принципов отладки, которые описал Дэвид Дж. Аганс в &lt;a href=&quot;https://oscat.ru/?p=539&quot; target=&quot;_blank&quot;&gt;книге&lt;/a&gt; «Отладка: девять незаменимых правил для обнаружения самых неуловимых ошибок в ПО и „железе“»:&lt;/p&gt;
  &lt;ol id=&quot;qSB2&quot;&gt;
    &lt;li id=&quot;4ctM&quot;&gt;Изучите свою систему.&lt;/li&gt;
    &lt;li id=&quot;VPhe&quot;&gt;Воспроизведите ошибку (надёжно воспроизведите).&lt;/li&gt;
    &lt;li id=&quot;gVwG&quot;&gt;Не предполагайте, а смотрите.&lt;/li&gt;
    &lt;li id=&quot;i0Nk&quot;&gt;Разделяйте и властвуйте.&lt;/li&gt;
    &lt;li id=&quot;FDot&quot;&gt;Вносите по одному изменению за раз.&lt;/li&gt;
    &lt;li id=&quot;KHHU&quot;&gt;Записывайте всё, что происходит.&lt;/li&gt;
    &lt;li id=&quot;UQ89&quot;&gt;Проверьте кабель.&lt;/li&gt;
    &lt;li id=&quot;yEM8&quot;&gt;Воспользуйтесь чьим‑то свежим взглядом.&lt;/li&gt;
    &lt;li id=&quot;jtKj&quot;&gt;Если вы не исправили ошибку — она не исчезла.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;kWT5&quot;&gt;&lt;a href=&quot;https://habr.com/ru/companies/wunderfund/articles/843618/&quot; target=&quot;_blank&quot;&gt;Источник&lt;/a&gt;&lt;/p&gt;

</content></entry><entry><id>javalib:6jkLts4G4i5</id><link rel="alternate" type="text/html" href="https://teletype.in/@javalib/6jkLts4G4i5?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=javalib"></link><title>Memory Fences и volatile в Java: низкоуровневые гарантии порядка памяти</title><published>2024-09-17T06:34:10.978Z</published><updated>2024-09-17T06:34:10.978Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img1.teletype.in/files/cc/4c/cc4c6a9b-9115-48a5-b6c1-be3aba213ab3.png"></media:thumbnail><category term="java" label="Java"></category><summary type="html">&lt;img src=&quot;https://img1.teletype.in/files/42/99/4299e8f2-c6e4-4776-8c34-37f52c489882.png&quot;&gt;Привет!</summary><content type="html">
  &lt;figure id=&quot;ZVZl&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/42/99/4299e8f2-c6e4-4776-8c34-37f52c489882.png&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;xUW0&quot;&gt;&lt;em&gt;Привет!&lt;/em&gt;&lt;/p&gt;
  &lt;p id=&quot;e0Hk&quot;&gt;Сегодня мы рассмотрим интересную тему для тех, кто сталкивается с многопоточностью в Java – это &lt;strong&gt;управление порядком памяти&lt;/strong&gt;. Базовых инструментов синхронизации, например как &lt;code&gt;synchronized&lt;/code&gt; или блокировки, порой недостаточно. Именно здесь могут помочь низкоуровневые механизмы, такие как &lt;code&gt;Memory Fences&lt;/code&gt; и ключевое слово &lt;code&gt;volatile&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;jV9t&quot;&gt;Эти инструменты позволяют контролировать порядок выполнения операций с памятью. В этой статье мы рассмотрим, как &lt;code&gt;volatile&lt;/code&gt; влияет на поведение программы, что такое Memory Fences, и как они могут помочь в сложных ситуациях с потоками.&lt;/p&gt;
  &lt;h2 id=&quot;O4Mh&quot;&gt;volatile&lt;/h2&gt;
  &lt;p id=&quot;PwZO&quot;&gt;Ключевое слово &lt;code&gt;volatile&lt;/code&gt; используется при объявлении переменных и сообщает JVM, что значение этой переменной может быть изменено разными потоками. Синтаксис прост:&lt;/p&gt;
  &lt;pre id=&quot;E8rp&quot; data-lang=&quot;java&quot;&gt;public class VolatileExample {
    private volatile int counter;
    // остальные методы
}&lt;/pre&gt;
  &lt;p id=&quot;OBe6&quot;&gt;Размещение &lt;code&gt;volatile&lt;/code&gt; перед типом переменной гарантирует, что все потоки будут видеть актуальное значение &lt;code&gt;counter&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;gUgm&quot;&gt;&lt;strong&gt;Примитивные типы&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;FcSz&quot;&gt;Для примитивных типов использование &lt;code&gt;volatile&lt;/code&gt; дает:&lt;/p&gt;
  &lt;ul id=&quot;J2ih&quot;&gt;
    &lt;li id=&quot;2srm&quot;&gt;&lt;strong&gt;Видимость изменений&lt;/strong&gt;: Если один поток изменяет значение &lt;code&gt;volatile&lt;/code&gt; переменной, другие потоки сразу увидят это изменение.&lt;/li&gt;
    &lt;li id=&quot;g2gg&quot;&gt;&lt;strong&gt;Запрет переупорядочивания&lt;/strong&gt;: Компилятор и процессор не будут переупорядочивать операции чтения и записи с &lt;code&gt;volatile&lt;/code&gt; переменными.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;y4iy&quot;&gt;Пример:&lt;/p&gt;
  &lt;pre id=&quot;7lvf&quot; data-lang=&quot;java&quot;&gt;public class FlagExample {
    private volatile boolean isActive;

    public void activate() {
        isActive = true;
    }

    public void process() {
        while (!isActive) {
            // Ждем активации
        }
        // Продолжаем обработку
    }
}&lt;/pre&gt;
  &lt;p id=&quot;jKEq&quot;&gt;&lt;strong&gt;Объекты&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;Knm7&quot;&gt;При использовании с объектами &lt;code&gt;volatile&lt;/code&gt; обеспечивает видимость изменения ссылки на объект, но не его внутреннего состояния.&lt;/p&gt;
  &lt;p id=&quot;hdVD&quot;&gt;Пример:&lt;/p&gt;
  &lt;pre id=&quot;oqro&quot; data-lang=&quot;java&quot;&gt;public class ConfigUpdater {
    private volatile Config config;

    public void updateConfig() {
        config = new Config(); // Новая ссылка будет видна всем потокам
    }

    public void useConfig() {
        Config localConfig = config;
        // Используем localConfig
    }
}&lt;/pre&gt;
  &lt;p id=&quot;6osQ&quot;&gt;Если внутреннее состояние объекта может меняться, необходимо обеспечить его потокобезопасность другими способами, например, через неизменяемые объекты или синхронизацию.&lt;/p&gt;
  &lt;p id=&quot;9qp4&quot;&gt;&lt;strong&gt;Как volatile влияет на чтение и запись переменных&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;PdCX&quot;&gt;Ключевое слово &lt;code&gt;volatile&lt;/code&gt; влияет на взаимодействие потоков с переменной следующим образом:&lt;/p&gt;
  &lt;ul id=&quot;TaJ3&quot;&gt;
    &lt;li id=&quot;8sRP&quot;&gt;&lt;strong&gt;Чтение&lt;/strong&gt;: Каждый раз при обращении к &lt;code&gt;volatile&lt;/code&gt; переменной поток читает ее актуальное значение из основной памяти, а не из кеша процессора.&lt;/li&gt;
    &lt;li id=&quot;Cwsk&quot;&gt;&lt;strong&gt;Запись&lt;/strong&gt;: При изменении &lt;code&gt;volatile&lt;/code&gt; переменной ее новое значение сразу записывается в основную память, делая его доступным для других потоков.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;cG1C&quot;&gt;Пример без &lt;code&gt;volatile&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;u6zZ&quot; data-lang=&quot;java&quot;&gt;public class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}&lt;/pre&gt;
  &lt;p id=&quot;pkxZ&quot;&gt;В многопоточной среде разные потоки могут работать с устаревшим значением &lt;code&gt;count&lt;/code&gt;, так как оно может быть закешировано.&lt;/p&gt;
  &lt;p id=&quot;dpQx&quot;&gt;Пример с &lt;code&gt;volatile&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;WRR1&quot; data-lang=&quot;java&quot;&gt;public class VolatileCounter {
    private volatile int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}&lt;/pre&gt;
  &lt;p id=&quot;Iy28&quot;&gt;Теперь каждый поток будет работать с актуальным значением &lt;code&gt;count&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;tMvR&quot;&gt;&lt;strong&gt;Однако будьте осторожны&lt;/strong&gt;: операция &lt;code&gt;count++&lt;/code&gt; не является атомарной, даже с &lt;code&gt;volatile&lt;/code&gt;. Для атомарности используйте &lt;code&gt;AtomicInteger&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;dkW5&quot;&gt;&lt;strong&gt;Ограничения volatile&lt;/strong&gt;&lt;/p&gt;
  &lt;ol id=&quot;z08U&quot;&gt;
    &lt;li id=&quot;N0au&quot;&gt;&lt;strong&gt;Отсутствие атомарности сложных операций&lt;/strong&gt;&lt;code&gt;volatile&lt;/code&gt; не делает операции атомарными. Операции инкремента &lt;code&gt;count++&lt;/code&gt;, декремента и другие сложные операции могут приводить к состояниям гонки.Пример проблемы:&lt;/li&gt;
  &lt;/ol&gt;
  &lt;pre id=&quot;WKzN&quot; data-lang=&quot;java&quot;&gt;public class NonAtomicVolatile {
    private volatile int count = 0;

    public void increment() {
        count++;
    }
}&lt;/pre&gt;
  &lt;p id=&quot;w0K6&quot;&gt;Здесь &lt;code&gt;count++&lt;/code&gt; состоит из чтения, увеличения и записи, которые могут быть прерваны другими потоками.&lt;/p&gt;
  &lt;p id=&quot;Eyxu&quot;&gt;&lt;strong&gt;2. Не обеспечивает синхронизацию доступа&lt;/strong&gt;&lt;code&gt;volatile&lt;/code&gt; не заменяет блоки &lt;code&gt;synchronized&lt;/code&gt; или объекты &lt;code&gt;Lock&lt;/code&gt;. Он не предотвращает одновременный доступ нескольких потоков к блоку кода.&lt;/p&gt;
  &lt;h4 id=&quot;azg4&quot;&gt;Примеры использования&lt;/h4&gt;
  &lt;p id=&quot;fvsI&quot;&gt;&lt;strong&gt;Флаги остановки потоков&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;DJRl&quot;&gt;В серверных приложениях или службах часто возникает необходимость корректно остановить поток или задачу, например, при завершении работы приложения или при изменении настроек.&lt;/p&gt;
  &lt;pre id=&quot;n1ci&quot; data-lang=&quot;java&quot;&gt;public class Worker implements Runnable {
    private volatile boolean isRunning = true;

    @Override
    public void run() {
        while (isRunning) {
            // Выполняем задачи
            performTask();
        }
    }

    public void stop() {
        isRunning = false;
    }

    private void performTask() {
        // Реализация задачи
    }
}&lt;/pre&gt;
  &lt;p id=&quot;pJIv&quot;&gt;Почему &lt;code&gt;volatile&lt;/code&gt;: Переменная &lt;code&gt;isRunning&lt;/code&gt; используется для контроля цикла выполнения потока. Без &lt;code&gt;volatile&lt;/code&gt; поток может не увидеть изменения переменной, сделанные другим потоком, из-за кеширования переменных на уровне процессора.&lt;/p&gt;
  &lt;p id=&quot;n9T8&quot;&gt;Метод &lt;code&gt;stop()&lt;/code&gt; может быть вызван из другого потока, устанавливая &lt;code&gt;isRunning&lt;/code&gt; в &lt;code&gt;false&lt;/code&gt;. Благодаря &lt;code&gt;volatile&lt;/code&gt; текущий поток немедленно увидит это изменение и корректно завершит работу.&lt;/p&gt;
  &lt;p id=&quot;3PjY&quot;&gt;&lt;strong&gt;Двойная проверка инициализации Singleton&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;nqaq&quot;&gt;Допустим, нужно создать ленивую =инициализацию Singleton без потерь производительности из-за избыточной синхронизации:&lt;/p&gt;
  &lt;pre id=&quot;c37E&quot; data-lang=&quot;java&quot;&gt;public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {
        // Инициализация ресурсов
    }

    public static Singleton getInstance() {
        if (instance == null) { // Первая проверка (без синхронизации)
            synchronized (Singleton.class) {
                if (instance == null) { // Вторая проверка (с синхронизацией)
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}&lt;/pre&gt;
  &lt;p id=&quot;NfBj&quot;&gt;Без &lt;code&gt;volatile&lt;/code&gt; возможно переупорядочивание инструкций, при котором другой поток может увидеть частично сконструированный объект &lt;code&gt;instance&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;fcaS&quot;&gt;&lt;code&gt;volatile&lt;/code&gt; гарантирует, что запись в &lt;code&gt;instance&lt;/code&gt; происходит только после полной инициализации объекта, и все последующие чтения увидят актуальное состояние.&lt;/p&gt;
  &lt;p id=&quot;itZ3&quot;&gt;&lt;strong&gt;Кэширование конфигурации с мгновенным обновлением&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;Sbl1&quot;&gt;В приложениях, где конфигурационные параметры могут динамически обновляться, необходимо сделать так, чтобы все рабочие потоки сразу получали актуальные настройки без необходимости перезапуска:&lt;/p&gt;
  &lt;pre id=&quot;WD0o&quot; data-lang=&quot;java&quot;&gt;public class ConfigurationManager {
    private volatile Config currentConfig;

    public ConfigurationManager() {
        // Инициализируем конфигурацию по умолчанию
        currentConfig = loadDefaultConfig();
    }

    public Config getConfig() {
        return currentConfig;
    }

    public void updateConfig(Config newConfig) {
        currentConfig = newConfig;
    }

    private Config loadDefaultConfig() {
        // Загрузка конфигурации по умолчанию
        return new Config(...);
    }
}

public class Worker implements Runnable {
    private ConfigurationManager configManager;

    public Worker(ConfigurationManager configManager) {
        this.configManager = configManager;
    }

    @Override
    public void run() {
        while (true) {
            Config config = configManager.getConfig();
            // Используем актуальную конфигурацию
            process(config);
        }
    }

    private void process(Config config) {
        // Обработка с использованием текущей конфигурации
    }
}&lt;/pre&gt;
  &lt;p id=&quot;XgAA&quot;&gt;Переменная &lt;code&gt;currentConfig&lt;/code&gt; может быть обновлена одним потоком (например, при изменении настроек администратором) и должна быть немедленно видна другим потокам, выполняющим задачи.&lt;/p&gt;
  &lt;p id=&quot;Gmde&quot;&gt;При обновлении конфигурации методом &lt;code&gt;updateConfig&lt;/code&gt; новое значение &lt;code&gt;currentConfig&lt;/code&gt; становится сразу доступным для всех рабочих потоков благодаря &lt;code&gt;volatile&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;8SMu&quot;&gt;&lt;strong&gt;Использование для одноразовых событий&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;joO3&quot;&gt;Частенько &lt;code&gt;volatile&lt;/code&gt; используется для сигнализации о наступлении определенного события, после которого необходимо изменить поведение приложения:&lt;/p&gt;
  &lt;pre id=&quot;zn8Y&quot; data-lang=&quot;java&quot;&gt;public class EventNotifier {
    private volatile boolean eventOccurred = false;

    public void waitForEvent() {
        while (!eventOccurred) {
            // Ожидание события
        }
        // Реакция на событие
    }

    public void triggerEvent() {
        eventOccurred = true;
    }
}&lt;/pre&gt;
  &lt;p id=&quot;uaw9&quot;&gt;Используйте &lt;code&gt;volatile&lt;/code&gt; для простых флагов состояния и публикации неизменяемых объектов. Избегайте сложных операций с &lt;code&gt;volatile&lt;/code&gt; переменными без дополнительной синхронизации.&lt;/p&gt;
  &lt;p id=&quot;qAty&quot;&gt;Переходим к следующей теме статьи – &lt;strong&gt;Memory Fences&lt;/strong&gt;&lt;/p&gt;
  &lt;h2 id=&quot;QiaR&quot;&gt;Memory Fences&lt;/h2&gt;
  &lt;p id=&quot;5OXJ&quot;&gt;&lt;strong&gt;Memory Fence&lt;/strong&gt; — это механизм, который предотвращает переупорядочивание операций чтения и записи памяти компилятором или процессором. Это означает, что операции, связанные с памятью, будут выполняться в ожидаемом порядке.&lt;/p&gt;
  &lt;p id=&quot;16mt&quot;&gt;&lt;strong&gt;Типы Memory Fences:&lt;/strong&gt;&lt;/p&gt;
  &lt;ol id=&quot;oDgh&quot;&gt;
    &lt;li id=&quot;gDJ0&quot;&gt;&lt;strong&gt;LoadLoad Barrier:&lt;/strong&gt; Гарантирует, что все операции чтения до барьера будут завершены до начала любых операций чтения после барьера.&lt;/li&gt;
    &lt;li id=&quot;gROL&quot;&gt;&lt;strong&gt;StoreStore Barrier:&lt;/strong&gt; Гарантирует, что все операции записи до барьера будут завершены до начала любых операций записи после барьера.&lt;/li&gt;
    &lt;li id=&quot;Jh4Y&quot;&gt;&lt;strong&gt;LoadStore Barrier:&lt;/strong&gt; Гарантирует, что все операции чтения до барьера будут завершены до начала любых операций записи после барьера.&lt;/li&gt;
    &lt;li id=&quot;JHvH&quot;&gt;&lt;strong&gt;StoreLoad Barrier:&lt;/strong&gt; Гарантирует, что все операции записи до барьера будут завершены до начала любых операций чтения после барьера. Это самый &amp;quot;&lt;em&gt;сильный&lt;/em&gt;&amp;quot; барьер.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;K9IV&quot;&gt;Java имеет несколько средств для управления барьерами памяти:&lt;/p&gt;
  &lt;ol id=&quot;aIWE&quot;&gt;
    &lt;li id=&quot;Xtoa&quot;&gt;&lt;strong&gt;Ключевое слово &lt;/strong&gt;&lt;code&gt;volatile&lt;/code&gt;&lt;/li&gt;
    &lt;li id=&quot;doCB&quot;&gt;&lt;strong&gt;Классы из пакета &lt;/strong&gt;&lt;code&gt;java.util.concurrent.atomic&lt;/code&gt;&lt;/li&gt;
    &lt;li id=&quot;Hb3b&quot;&gt;&lt;strong&gt;Класс &lt;/strong&gt;&lt;code&gt;Unsafe&lt;/code&gt; (с осторожностью)&lt;/li&gt;
    &lt;li id=&quot;3Bwr&quot;&gt;&lt;code&gt;VarHandle&lt;/code&gt;&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;G9uN&quot;&gt;Рассмотрим их подробнее.&lt;/p&gt;
  &lt;h4 id=&quot;pTmU&quot;&gt;Классы из пакета java.util.concurrent.atomic&lt;/h4&gt;
  &lt;p id=&quot;PtZN&quot;&gt;Пакет &lt;code&gt;java.util.concurrent.atomic&lt;/code&gt; содержит классы, предоставляющие атомарные операции над переменными разных типов:&lt;/p&gt;
  &lt;ul id=&quot;ix4Q&quot;&gt;
    &lt;li id=&quot;aGs1&quot;&gt;&lt;code&gt;AtomicInteger&lt;/code&gt;&lt;/li&gt;
    &lt;li id=&quot;f8YC&quot;&gt;&lt;code&gt;AtomicLong&lt;/code&gt;&lt;/li&gt;
    &lt;li id=&quot;LHVZ&quot;&gt;&lt;code&gt;AtomicReference&lt;/code&gt;&lt;/li&gt;
    &lt;li id=&quot;feP7&quot;&gt;&lt;code&gt;AtomicBoolean&lt;/code&gt;&lt;/li&gt;
    &lt;li id=&quot;LSm0&quot;&gt;И другие&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;B5lz&quot;&gt;Эти классы используют низкоуровневые примитивы синхронизации.&lt;/p&gt;
  &lt;p id=&quot;EnLW&quot;&gt;&lt;strong&gt;Пример использования &lt;/strong&gt;&lt;code&gt;AtomicInteger&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;mDvX&quot; data-lang=&quot;java&quot;&gt;import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private AtomicInteger counter = new AtomicInteger(0);

    public void increment() {
        counter.incrementAndGet(); // Атомарное увеличение значения на 1
    }

    public int getValue() {
        return counter.get(); // Атомарное получение текущего значения
    }
}&lt;/pre&gt;
  &lt;h4 id=&quot;nPaQ&quot;&gt;Методы Unsafe&lt;/h4&gt;
  &lt;p id=&quot;i9OH&quot;&gt;Класс &lt;code&gt;sun.misc.Unsafe&lt;/code&gt; предоставляет низкоуровневые операции над памятью, включая методы для установки барьеров памяти.&lt;/p&gt;
  &lt;p id=&quot;rh7N&quot;&gt;Класс &lt;code&gt;Unsafe&lt;/code&gt; является внутренним API и не предназначен для общего использования. Его использование может привести к непереносимому коду и потенциальным ошибкам.&lt;/p&gt;
  &lt;p id=&quot;jmEK&quot;&gt;&lt;em&gt;В Java 9 и выше доступ к &lt;/em&gt;&lt;code&gt;Unsafe&lt;/code&gt;&lt;em&gt; ограничен модульной системой.&lt;/em&gt;&lt;/p&gt;
  &lt;p id=&quot;MdOI&quot;&gt;&lt;strong&gt;Однако для образовательных целей рассмотрим пример:&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;bK9V&quot; data-lang=&quot;java&quot;&gt;import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class UnsafeMemoryFenceExample {
    private static final Unsafe unsafe;

    static {
        try {
            Field field = Unsafe.class.getDeclaredField(&amp;quot;theUnsafe&amp;quot;);
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (Exception e) {
            throw new RuntimeException(&amp;quot;Не удалось получить доступ к Unsafe&amp;quot;, e);
        }
    }

    public void storeLoadFence() {
        unsafe.storeFence(); // Применение StoreStore Barriers
        // Ваш код
        unsafe.loadFence(); // Применение LoadLoad Barriers
    }
}&lt;/pre&gt;
  &lt;p id=&quot;5ydd&quot;&gt;Используйте &lt;code&gt;Unsafe&lt;/code&gt; только если полностью понимаете риски и альтернатив нет.&lt;/p&gt;
  &lt;p id=&quot;fih9&quot;&gt;Рекомендуется использовать &lt;code&gt;VarHandle&lt;/code&gt; или высокоуровневые классы из &lt;code&gt;java.util.concurrent&lt;/code&gt;.&lt;/p&gt;
  &lt;h4 id=&quot;1j0g&quot;&gt;Примеры использования Memory Fences для синхронизации потоков&lt;/h4&gt;
  &lt;p id=&quot;FUV6&quot;&gt;Реализация неблокирующего счетчика с AtomicLong:&lt;/p&gt;
  &lt;pre id=&quot;FJ58&quot; data-lang=&quot;java&quot;&gt;import java.util.concurrent.atomic.AtomicLong;

public class NonBlockingCounter {
    private AtomicLong counter = new AtomicLong(0);

    public void increment() {
        counter.getAndIncrement(); // Атомарное увеличение значения
    }

    public long getValue() {
        return counter.get();
    }
}&lt;/pre&gt;
  &lt;p id=&quot;qCIL&quot;&gt;AtomicReference для реализации неблокирующего стека:&lt;/p&gt;
  &lt;pre id=&quot;SsVi&quot; data-lang=&quot;java&quot;&gt;import java.util.concurrent.atomic.AtomicReference;

public class LockFreeStack&amp;lt;T&amp;gt; {
    private AtomicReference&amp;lt;Node&amp;lt;T&amp;gt;&amp;gt; head = new AtomicReference&amp;lt;&amp;gt;(null);

    private static class Node&amp;lt;T&amp;gt; {
        final T value;
        final Node&amp;lt;T&amp;gt; next;

        Node(T value, Node&amp;lt;T&amp;gt; next) {
            this.value = value;
            this.next = next;
        }
    }

    public void push(T value) {
        Node&amp;lt;T&amp;gt; newHead;
        Node&amp;lt;T&amp;gt; oldHead;

        do {
            oldHead = head.get();
            newHead = new Node&amp;lt;&amp;gt;(value, oldHead);
        } while (!head.compareAndSet(oldHead, newHead));
    }

    public T pop() {
        Node&amp;lt;T&amp;gt; oldHead;
        Node&amp;lt;T&amp;gt; newHead;

        do {
            oldHead = head.get();
            if (oldHead == null) {
                return null; // Стек пуст
            }
            newHead = oldHead.next;
        } while (!head.compareAndSet(oldHead, newHead));

        return oldHead.value;
    }
}&lt;/pre&gt;
  &lt;h4 id=&quot;T4Sr&quot;&gt;VarHandle — современный подход к Memory Fences&lt;/h4&gt;
  &lt;p id=&quot;WyXf&quot;&gt;Начиная с Java 9, был введен &lt;code&gt;VarHandle&lt;/code&gt; как более мощная альтернатива &lt;code&gt;Atomic&lt;/code&gt; классам и &lt;code&gt;Unsafe&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;sEpk&quot;&gt;&lt;strong&gt;Фичи &lt;/strong&gt;&lt;code&gt;VarHandle&lt;/code&gt;:&lt;/p&gt;
  &lt;ul id=&quot;l5ys&quot;&gt;
    &lt;li id=&quot;pz5Y&quot;&gt;Позволяет выполнять операции с разными уровнями гарантий памяти.&lt;/li&gt;
    &lt;li id=&quot;AoXv&quot;&gt;Более гибкий и безопасный по сравнению с &lt;code&gt;Unsafe&lt;/code&gt;.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;HGYv&quot;&gt;&lt;strong&gt;Пример использования &lt;/strong&gt;&lt;code&gt;VarHandle&lt;/code&gt;&lt;strong&gt;:&lt;/strong&gt;&lt;/p&gt;
  &lt;pre id=&quot;Vl3k&quot; data-lang=&quot;java&quot;&gt;import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;

public class VarHandleExample {
    private int value = 0;
    private static final VarHandle VALUE_HANDLE;

    static {
        try {
            VALUE_HANDLE = MethodHandles.lookup().findVarHandle(VarHandleExample.class, &amp;quot;value&amp;quot;, int.class);
        } catch (Exception e) {
            throw new Error(e);
        }
    }

    public void setValue(int newValue) {
        VALUE_HANDLE.setRelease(this, newValue); // Обеспечивает StoreStore Barrier
    }

    public int getValue() {
        return (int) VALUE_HANDLE.getAcquire(this); // Обеспечивает LoadLoad Barrier
    }
}&lt;/pre&gt;
  &lt;p id=&quot;neoB&quot;&gt;Реализация простого счетчика с VarHandle:&lt;/p&gt;
  &lt;pre id=&quot;rqQp&quot; data-lang=&quot;java&quot;&gt;import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;

public class VarHandleCounter {
    private int count = 0;
    private static final VarHandle COUNT_HANDLE;

    static {
        try {
            COUNT_HANDLE = MethodHandles.lookup().findVarHandle(VarHandleCounter.class, &amp;quot;count&amp;quot;, int.class);
        } catch (Exception e) {
            throw new Error(e);
        }
    }

    public void increment() {
        int prevValue;
        do {
            prevValue = (int) COUNT_HANDLE.getVolatile(this);
        } while (!COUNT_HANDLE.compareAndSet(this, prevValue, prevValue + 1));
    }

    public int getCount() {
        return (int) COUNT_HANDLE.getVolatile(this);
    }
}&lt;/pre&gt;
  &lt;hr /&gt;
  &lt;p id=&quot;6hN5&quot;&gt;&lt;strong&gt;Заключение&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;fYP4&quot;&gt;Правильное применение &lt;code&gt;volatile&lt;/code&gt; и Memory Fences позволяет создавать эффективные и надежные многопоточные приложения.&lt;/p&gt;
  &lt;p id=&quot;WyhY&quot;&gt;&lt;a href=&quot;https://habr.com/ru/companies/otus/articles/843394/&quot; target=&quot;_blank&quot;&gt;Источник&lt;/a&gt;&lt;/p&gt;

</content></entry><entry><id>javalib:chY8FYNPpy9</id><link rel="alternate" type="text/html" href="https://teletype.in/@javalib/chY8FYNPpy9?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=javalib"></link><title>Новое событие в JFR для диагностики использования устаревшего (deprecated) кода</title><published>2024-08-20T06:57:06.802Z</published><updated>2024-08-20T06:57:06.802Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img1.teletype.in/files/80/77/807723a5-d302-4560-aabc-d2a8f6efa3e5.png"></media:thumbnail><category term="java" label="Java"></category><summary type="html">&lt;img src=&quot;https://img3.teletype.in/files/ef/58/ef5835c1-23b3-4e70-b4fd-872c013c8c4c.jpeg&quot;&gt;В Java есть специальная аннотация @Deprecated для маркировки уставшего кода. С определенной периодичностью такой код из JDK удаляется. Обычно о конкретных сроках удаления анонс делается заранее и в теории можно успеть подготовиться, но на практике не все так просто.</summary><content type="html">
  &lt;figure id=&quot;Fo2R&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/ef/58/ef5835c1-23b3-4e70-b4fd-872c013c8c4c.jpeg&quot; width=&quot;768&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;yhGt&quot;&gt;В Java есть специальная аннотация @Deprecated для маркировки уставшего кода. С определенной периодичностью такой код из JDK удаляется. Обычно о конкретных сроках удаления анонс делается заранее и в теории можно успеть подготовиться, но на практике не все так просто.&lt;/p&gt;
  &lt;p id=&quot;KODE&quot;&gt;В больших проектах найти куски устаревшего кода в куче зависимостей задача не тривиальная и требующая хорошей автоматизации. В этой ситуации к нам приходит на помощь новый тип события в JFR. Он был добавлен в JDK 22.&lt;/p&gt;
  &lt;p id=&quot;ScPp&quot;&gt;Давайте посмотрим на простом примере как это работает.&lt;/p&gt;
  &lt;hr /&gt;
  &lt;p id=&quot;86mN&quot;&gt;Для начала создадим простой класс и в его методе main вызовим любой @Deprecated метод. Для примера я выбрал конструктор класса java.net.URL(String). Со списком всех методов помеченных аннотацией @Deprecated вы можете &lt;a href=&quot;https://docs.oracle.com/en/java/javase/22/docs/api/deprecated-list.html&quot; target=&quot;_blank&quot;&gt;ознакомиться по этой ссылке&lt;/a&gt;&lt;/p&gt;
  &lt;pre id=&quot;Hit4&quot; data-lang=&quot;java&quot;&gt;public class App
{
    public static void main(String[] args) throws MalformedURLException {

        URL url = new URL(&amp;quot;https://habr.com/ru/&amp;quot;);
    }
}&lt;/pre&gt;
  &lt;p id=&quot;tTbn&quot;&gt;Если вы попробуете скомпилировать его при помощи &lt;a href=&quot;https://docs.oracle.com/en/java/javase/22/docs/specs/man/javac.html&quot; target=&quot;_blank&quot;&gt;javac&lt;/a&gt;, то получите собщение c предупреждением о том, что у вас в коде используется устаревший код.&lt;/p&gt;
  &lt;pre id=&quot;g7kG&quot; data-lang=&quot;java&quot;&gt;~/IdeaProjects/Tests/src/main/java (master*) » javac App.java                                                                                                         user@user-home
Note: App.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.&lt;/pre&gt;
  &lt;p id=&quot;9OEN&quot;&gt;Если добавить в параметры запуска еще и -Xlint:deprecation вывод станет чуть более информативен.&lt;/p&gt;
  &lt;pre id=&quot;DWEE&quot; data-lang=&quot;java&quot;&gt;~/IdeaProjects/Tests/src/main/java (master*) » javac -Xlint:deprecation App.java                                                                                      user@user-home
App.java:8: warning: [deprecation] URL(String) in URL has been deprecated
        URL url = new URL(&amp;quot;https://habr.com/ru/&amp;quot;);
                  ^
1 warning&lt;/pre&gt;
  &lt;p id=&quot;EqiV&quot;&gt;Но таким &amp;quot;дедовским&amp;quot; способом реальные проекты никто не собирает. Обычно используются инструменты автоматизирующие сборку. На данный момент довольно широко используются &lt;a href=&quot;https://maven.apache.org/&quot; target=&quot;_blank&quot;&gt;Maven&lt;/a&gt; и &lt;a href=&quot;https://gradle.org/&quot; target=&quot;_blank&quot;&gt;Gradle&lt;/a&gt;.&lt;/p&gt;
  &lt;p id=&quot;858T&quot;&gt;Если сделать простой Maven проект с таким pom.xml, то к удивлению мы не получим warning при компиляции того файла.&lt;/p&gt;
  &lt;pre id=&quot;Obep&quot; data-lang=&quot;java&quot;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt;
&amp;lt;project xmlns=&amp;quot;http://maven.apache.org/POM/4.0.0&amp;quot;
         xmlns:xsi=&amp;quot;http://www.w3.org/2001/XMLSchema-instance&amp;quot;
         xsi:schemaLocation=&amp;quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&amp;quot;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

    &amp;lt;groupId&amp;gt;com.github.rodindenis&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;Tests&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.0-SNAPSHOT&amp;lt;/version&amp;gt;

    &amp;lt;properties&amp;gt;
        &amp;lt;maven.compiler.source&amp;gt;22&amp;lt;/maven.compiler.source&amp;gt;
        &amp;lt;maven.compiler.target&amp;gt;22&amp;lt;/maven.compiler.target&amp;gt;
        &amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;
    &amp;lt;/properties&amp;gt;

&amp;lt;/project&amp;gt;&lt;/pre&gt;
  &lt;p id=&quot;yi7n&quot;&gt;Для получегия таких warning нужно будет еще добавить параметр конфигурации showDeprecation для maven-compiler-plugin.&lt;/p&gt;
  &lt;pre id=&quot;akj2&quot; data-lang=&quot;java&quot;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt;
&amp;lt;project xmlns=&amp;quot;http://maven.apache.org/POM/4.0.0&amp;quot;
         xmlns:xsi=&amp;quot;http://www.w3.org/2001/XMLSchema-instance&amp;quot;
         xsi:schemaLocation=&amp;quot;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd&amp;quot;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

    &amp;lt;groupId&amp;gt;com.github.rodindenis&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;Tests&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.0-SNAPSHOT&amp;lt;/version&amp;gt;

    &amp;lt;properties&amp;gt;
        &amp;lt;maven.compiler.source&amp;gt;22&amp;lt;/maven.compiler.source&amp;gt;
        &amp;lt;maven.compiler.target&amp;gt;22&amp;lt;/maven.compiler.target&amp;gt;
        &amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;
    &amp;lt;/properties&amp;gt;

    &amp;lt;build&amp;gt;
        &amp;lt;plugins&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;maven-compiler-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;3.13.0&amp;lt;/version&amp;gt;
                &amp;lt;configuration&amp;gt;
                    &amp;lt;showDeprecation&amp;gt;true&amp;lt;/showDeprecation&amp;gt;
                &amp;lt;/configuration&amp;gt;
            &amp;lt;/plugin&amp;gt;
        &amp;lt;/plugins&amp;gt;
    &amp;lt;/build&amp;gt;

&amp;lt;/project&amp;gt;&lt;/pre&gt;
  &lt;p id=&quot;rXxk&quot;&gt;Что будет если этот кусок кода уедет в библиотеку и мы с вами будем ее получать как уже скомпилированную зависимость? Ответ под спойлером.&lt;/p&gt;
  &lt;p id=&quot;2CBu&quot;&gt;Так как компилятор проверяет в момент компиляции, то соответственно warning в момент компиляции нашего кода мы не получим.&lt;/p&gt;
  &lt;p id=&quot;IuaW&quot;&gt;Давайте проверим? Я вынес вызов устаревшего кода в отдельный сабмодуль.&lt;/p&gt;
  &lt;pre id=&quot;6s2U&quot; data-lang=&quot;java&quot;&gt;package com.github.rodindenis.jfr.event.lib;

import java.net.MalformedURLException;
import java.net.URL;

public class Printer {

    public static void print(String url) throws MalformedURLException {
        System.out.println(new URL(url).toString());
    }
}&amp;gt;
    &amp;lt;/build&amp;gt;

&amp;lt;/project&amp;gt;&lt;/pre&gt;
  &lt;p id=&quot;TTPx&quot;&gt;Наш класс App тоже изменился&lt;/p&gt;
  &lt;pre id=&quot;u56S&quot; data-lang=&quot;java&quot;&gt;package com.github.rodindenis.jfr.event.main;

import com.github.rodindenis.jfr.event.lib.Printer;
import java.net.MalformedURLException;

public class App
{
    public static void main(String[] args) throws MalformedURLException {

        Printer.print(&amp;quot;https://habr.com/ru/&amp;quot;);
    }
}&lt;/pre&gt;
  &lt;p id=&quot;58iI&quot;&gt;Примеры pom.xml здесь не привожу. Их можно будет посмотреть в репозитории.&lt;/p&gt;
  &lt;p id=&quot;NJcy&quot;&gt;Как и ожидалось после компиляции класса Printer мы получаем warning, но вот когда мы уже подключаем уже скомпилированную библиотеку с этим классом как зависимость и пробуем скомпилировать App, то warning мы уже не ловим.&lt;/p&gt;
  &lt;p id=&quot;Menw&quot;&gt;Для воспроизведения ситуации в скаченном из репозитория кода проведите следующие манипуляции. Сначала скомпилируйте и установитие все артефакты. На этом этапе вы увидите в логе warning при компиляции библиотеки.&lt;/p&gt;
  &lt;pre id=&quot;skKJ&quot;&gt;mvn clean install&lt;/pre&gt;
  &lt;p id=&quot;Qebt&quot;&gt;Так как у нас теперь локально установлена скомпилированная библиотека с кодом класса Printer, то мы можем попробовать отдельно перекомпилировать только основной код App. Эту команду выполняем только для сабмодуля main с классом App.&lt;/p&gt;
  &lt;pre id=&quot;wddg&quot;&gt;mvn clean package&lt;/pre&gt;
  &lt;hr /&gt;
  &lt;p id=&quot;Nwz7&quot;&gt;И вот пришло время ощутить всю силу JFR.&lt;/p&gt;
  &lt;p id=&quot;tAuL&quot;&gt;Запускаем наше приложение с включенным JFR логированием в файл. Я перешел в каталог main/target своего проекта и из него выполнил команду&lt;/p&gt;
  &lt;pre id=&quot;RHvM&quot; data-lang=&quot;java&quot;&gt;~/.jdks/temurin-22.0.2/bin/java -XX:StartFlightRecording:jdk.DeprecatedInvocation#level=all,filename=recording.jfr -cp ../../lib/target/lib-1.0-SNAPSHOT.jar:./main-1.0-SNAPSHOT.jar com.github.rodindenis.jfr.event.main.App&lt;/pre&gt;
  &lt;p id=&quot;7mRC&quot;&gt;В текущем каталоге появился файл recording.jfr. Давайте посмотрим есть ли в нем нужное нам событие.&lt;/p&gt;
  &lt;pre id=&quot;n57C&quot; data-lang=&quot;java&quot;&gt;~/.jdks/temurin-22.0.2/bin/jfr print --events jdk.DeprecatedInvocation recording.jfr&lt;/pre&gt;
  &lt;p id=&quot;lIpO&quot;&gt;В моем кейсе было напечатано&lt;/p&gt;
  &lt;pre id=&quot;bQhI&quot; data-lang=&quot;java&quot;&gt;jdk.DeprecatedInvocation {
  startTime = 16:39:18.769 (2024-08-19)
  method = java.net.URL.&amp;lt;init&amp;gt;(String)
  invocationTime = 16:39:18.759 (2024-08-19)
  forRemoval = false
  stackTrace = [
    com.github.rodindenis.jfr.event.lib.Printer.print(String) line: 9
    ...
  ]
}&lt;/pre&gt;
  &lt;p id=&quot;vIqC&quot;&gt;Важная оговорка. Данный подход позволит вам в рантайме выявить вызовы устаревшего кода, но если код не вызывается, то и события такого вы не получите.&lt;/p&gt;
  &lt;p id=&quot;I0L9&quot;&gt;&lt;a href=&quot;https://habr.com/ru/articles/837052/&quot; target=&quot;_blank&quot;&gt;Источник&lt;/a&gt;&lt;/p&gt;

</content></entry><entry><id>javalib:lAcwncuqHmO</id><link rel="alternate" type="text/html" href="https://teletype.in/@javalib/lAcwncuqHmO?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=javalib"></link><title>JPA Entity. Загрузи меня не полностью</title><published>2024-08-16T10:24:42.090Z</published><updated>2024-08-16T10:27:57.757Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img3.teletype.in/files/6c/01/6c01e05c-d7e5-4334-9172-e6406d8e2b79.png"></media:thumbnail><category term="java" label="Java"></category><summary type="html">&lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/35e/8d6/bd5/35e8d6bd500bb63a86a1fbd3608387e8.png&quot;&gt;JPA часто подвергается критике за невозможность загружать сущности частично, что на самом деле является большим заблуждением. Spring Data JPA и Hibernate включают в себя множество инструментов по частичной загрузке сущностей.</summary><content type="html">
  &lt;p id=&quot;n7rW&quot;&gt;JPA часто подвергается критике за невозможность загружать сущности частично, что на самом деле является большим заблуждением. Spring Data JPA и Hibernate включают в себя множество инструментов по частичной загрузке сущностей.&lt;/p&gt;
  &lt;p id=&quot;HnfH&quot;&gt;Подготовили статью, в которой рассмотрели имеющиеся в Spring Data JPA инструменты для частичной загрузки сущностей, а также разобрали их особенности и corner-кейсы. Давайте попробуем рассмотреть все способы такой частичной загрузки сущностей на примере основных способов взаимодействия с Hibernate в Spring приложениях:&lt;/p&gt;
  &lt;ul id=&quot;hLmp&quot;&gt;
    &lt;li id=&quot;BJlf&quot;&gt;Spring Data JPA&lt;/li&gt;
    &lt;li id=&quot;Rf0Y&quot;&gt;EntityManager&lt;/li&gt;
    &lt;li id=&quot;Kp4f&quot;&gt;Criteria API&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;7Z0U&quot;&gt;Также существует проект &lt;a href=&quot;https://github.com/jakartaee/data&quot; target=&quot;_blank&quot;&gt;Jakarta Data&lt;/a&gt;, который активно развивается. Однако на момент написания этой статьи вышла только первая стабильная версия 1.0.0. Рассматривать его не станем, пока не накопим достаточное количество реальных использований.&lt;/p&gt;
  &lt;figure id=&quot;UbT3&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/35e/8d6/bd5/35e8d6bd500bb63a86a1fbd3608387e8.png&quot; width=&quot;780&quot; /&gt;
  &lt;/figure&gt;
  &lt;h3 id=&quot;dUcB&quot;&gt;Интересует ли эта проблема сообщество&lt;/h3&gt;
  &lt;p id=&quot;bRsa&quot;&gt;После очередного холивара в одном из Telegram-каналов про Java на тему того, как плох JPA и Hibernate в частности, как он неоптимизированно выполняет запросы и как много грузится лишних данных, я решил немного углубиться в эти вопросы и попытаться встать на защиту упомянутого выше стэка, отправившись в путешествие на Stackoverflow. Сделаем поиск по тегу &lt;code&gt;[spring-data-jpa]&lt;/code&gt; и отсортируем вопросы по популярности. Мы увидим, что вопрос &lt;a href=&quot;https://stackoverflow.com/questions/22007341/spring-jpa-selecting-specific-columns&quot; target=&quot;_blank&quot;&gt;Spring JPA selecting specific columns&lt;/a&gt; находится на шестом месте.&lt;br /&gt;В этой статье мы постараемся ответить на этот вопрос максимально широко: рассмотрим не только самые простые случаи с базовыми атрибутами, но также окунемся и в мир JPA-ассоциаций.&lt;/p&gt;
  &lt;h3 id=&quot;N7y2&quot;&gt;Задача&lt;/h3&gt;
  &lt;p id=&quot;nZ7F&quot;&gt;В &lt;a href=&quot;https://github.com/comru/jpa-partial-load&quot; target=&quot;_blank&quot;&gt;проекте&lt;/a&gt; будет рассматриваться следующая модель данных:&lt;/p&gt;
  &lt;figure id=&quot;8R8r&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/5a6/cc9/76c/5a6cc976c104ca2209aee23b0911f222.png&quot; width=&quot;1788&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;ZBFL&quot;&gt;Нашей задачей является загрузка нескольких базовый полей + ToOne (в нашем случае это атрибут author, ссылающийся на сущность Post) ассоциации для каждого способа частичной загрузки. Предположим, мы хотим получить все статьи, заголовок которых содержит некоторый текст, причем поиск не чувствителен к регистру, т.е. contains with ignore case. Итого выгружаем &lt;code&gt;Post: id, slug, title; User(author): id, username&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;WD84&quot;&gt;Проверять результат мы будем в соответствующих &lt;a href=&quot;https://github.com/comru/jpa-partial-load/tree/main/src/test/java/io/amplicode/jpa/repository&quot; target=&quot;_blank&quot;&gt;тестах&lt;/a&gt;, итоговые запросы будут видны в консоли: лог, в котором отобразится SQL-запрос, сгенерированный силами Hibernate.&lt;/p&gt;
  &lt;h3 id=&quot;xACp&quot;&gt;Тестовые данные&lt;/h3&gt;
  &lt;p id=&quot;jEYA&quot;&gt;Создадим две записи &lt;code&gt;Post&lt;/code&gt;, проинициализировав базовые поля и привязав авторов. Сервис в котором создаются и удаляются данные &lt;a href=&quot;https://github.com/comru/jpa-partial-load/blob/main/src/test/java/io/amplicode/jpa/InitTestDataService.java&quot; target=&quot;_blank&quot;&gt;InitTestDataService&lt;/a&gt;.&lt;/p&gt;
  &lt;h3 id=&quot;baMU&quot;&gt;toOne&lt;/h3&gt;
  &lt;p id=&quot;a5MB&quot;&gt;Всего был найден 21 способ частичной загрузки для поставленной нами задачи.&lt;/p&gt;
  &lt;p id=&quot;XQ8L&quot;&gt;Эти способы включают в себя разные подходы написания запроса:&lt;/p&gt;
  &lt;ul id=&quot;FNdT&quot;&gt;
    &lt;li id=&quot;n1nV&quot;&gt;Spring Data Repository derived-methods&lt;/li&gt;
    &lt;li id=&quot;E4p5&quot;&gt;Spring Data Repository query-methods&lt;/li&gt;
    &lt;li id=&quot;W0Qt&quot;&gt;Entity Manager&lt;/li&gt;
    &lt;li id=&quot;QPyI&quot;&gt;Criteria API&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;SR5n&quot;&gt;Тестовый класс в котором можно увидеть все тесты с комментариями - &lt;a href=&quot;https://github.com/comru/jpa-partial-load/blob/main/src/test/java/io/amplicode/jpa/repository/ToOneTest.java&quot; target=&quot;_blank&quot;&gt;ToOneTest&lt;/a&gt;.&lt;/p&gt;
  &lt;h3 id=&quot;NVSZ&quot;&gt;Предисловие&lt;/h3&gt;
  &lt;p id=&quot;7Bxd&quot;&gt;Стоит внести небольшую ясность перед нашим экспериментом. &lt;code&gt;Derived&lt;/code&gt; методы — это методы, автоматически реализуемые фреймворком на основе их имен, т.е. без явного указания аннотации &lt;a href=&quot;https://docs.spring.io/spring-data/commons/reference/repositories/query-methods.html&quot; target=&quot;_blank&quot;&gt;@Query&lt;/a&gt;. Поскольку мы не указываем запрос явно, а значит у нас отсутствует возможность указать, какие именно атрибуты нам требуется загрузить, у нас есть всего один способ указания конкретных атрибутов для загрузки - это &lt;strong&gt;projection&lt;/strong&gt;.&lt;/p&gt;
  &lt;p id=&quot;tPxS&quot;&gt;Сами же проекции бывают двух видов: &lt;strong&gt;основанные на интерфейсах &lt;/strong&gt;(Interface-based Projections) и &lt;strong&gt;на классах &lt;/strong&gt;(Class-based Projections). &lt;strong&gt;Interface-based Projections &lt;/strong&gt;в свою очередь можно поделить на открытые и закрытые.&lt;/p&gt;
  &lt;p id=&quot;pF3D&quot;&gt;В &lt;a href=&quot;https://docs.spring.io/spring-data/jpa/reference/repositories/projections.html#projections.interfaces.closed&quot; target=&quot;_blank&quot;&gt;закрытых&lt;/a&gt; проекциях геттеры объявляются явно:&lt;/p&gt;
  &lt;pre id=&quot;7QL8&quot; data-lang=&quot;java&quot;&gt;interface NamesOnly {
  String getFirstname();
  String getLastname();
}&lt;/pre&gt;
  &lt;p id=&quot;mBvu&quot;&gt;В случае &lt;a href=&quot;https://docs.spring.io/spring-data/jpa/reference/repositories/projections.html#projections.interfaces.open&quot; target=&quot;_blank&quot;&gt;открытых проекций&lt;/a&gt; значение геттеров интерфейсов могут высчитываться на основе SpEL выражения:&lt;/p&gt;
  &lt;pre id=&quot;87M0&quot; data-lang=&quot;java&quot;&gt;interface NamesOnly {  
    @Value(&amp;quot;#{target.firstname + &amp;#x27; &amp;#x27; + target.lastname}&amp;quot;)
    String getFullName();
}  &lt;/pre&gt;
  &lt;p id=&quot;w1ze&quot;&gt;Их мы рассматривать не будем, т.к. в &lt;a href=&quot;https://docs.spring.io/spring-data/cassandra/reference/repositories/projections.html#:~:text=Spring%20Data%20cannot%20apply%20query%20execution%20optimizations%20in%20this%20case%2C%20because%20the%20SpEL%20expression%20could%20use%20any%20attribute%20of%20the%20aggregate%20root.&quot; target=&quot;_blank&quot;&gt;документации явно сказано&lt;/a&gt;, что для них оптимизация запроса производиться не будет:&lt;/p&gt;
  &lt;blockquote id=&quot;lhsa&quot;&gt;Spring Data cannot apply query execution optimizations in this case, because the SpEL expression could use any attribute of the aggregate root.&lt;/blockquote&gt;
  &lt;p id=&quot;ezBX&quot;&gt;Для загрузки &lt;strong&gt;ToOne&lt;/strong&gt;-ассоциаций ставим задачу проверить два варианта с &lt;strong&gt;flatten&lt;/strong&gt; (плоскими) атрибутами, с &lt;strong&gt;nested&lt;/strong&gt; (вложенным) классом, а также с &lt;strong&gt;Tuple&lt;/strong&gt; и &lt;strong&gt;Map&lt;/strong&gt;.&lt;/p&gt;
  &lt;p id=&quot;wkXc&quot;&gt;Для &lt;strong&gt;ToMany&lt;/strong&gt; имеет смысл проверять работу с плоскими атрибутами, однако в этом случае загрузка будет со сложностью n*m. Это означает, что наш эксперимент подходит и для ToOne, и для ToMany, однако записей в случае с ToManyвыгрузится больше.&lt;/p&gt;
  &lt;p id=&quot;wGIF&quot;&gt;Почти все, что мы рассмотрим для ToOne, справедливо и для ToMany, однако Hibernate не способен мапить коллекционные атрибуты на DTO/Projection, в связи с чем Hibernate в силах выполнить только HQL, и для кейса с ToMany будет возвращено декартово произведение n*m. Иначе говоря, кроме выгрузки и мапинга результата нам придется еще схлопывать дубликаты записей. Если эта тема будет интересна, мы обязательно напишем дополнительный пост про частичную загрузку и с ToMany-ассоциациями. Однако с несколькими примерами можно ознакомиться в &lt;a href=&quot;https://github.com/comru/jpa-partial-load&quot; target=&quot;_blank&quot;&gt;проекте&lt;/a&gt; в тестовом классе &lt;a href=&quot;https://github.com/comru/jpa-partial-load/blob/main/src/test/java/io/amplicode/jpa/repository/ToManyTest.java&quot; target=&quot;_blank&quot;&gt;ToManyTest&lt;/a&gt;.&lt;/p&gt;
  &lt;p id=&quot;vvyb&quot;&gt;Все, что будет рассмотрено далее, отлично подойдет и для &lt;strong&gt;Embbeded&lt;/strong&gt;-кейса, поэтому отдельно его рассматривать не имеет смысла&lt;/p&gt;
  &lt;h3 id=&quot;XT19&quot;&gt;Derived-methods&lt;/h3&gt;
  &lt;h4 id=&quot;gy6q&quot;&gt;Interface-based flat projection&lt;/h4&gt;
  &lt;p id=&quot;Tc0o&quot;&gt;Для данного кейса будем в качестве проекции использовать отдельно взятый интерфейс. Пожалуй, данный подход можно отнести к базовой концепции в контексте использования проекций.&lt;/p&gt;
  &lt;p id=&quot;bO8u&quot;&gt;Объявим в нашем репозитории метод:&lt;/p&gt;
  &lt;pre id=&quot;SOLs&quot; data-lang=&quot;java&quot;&gt;public interface PostRepository extends JpaRepository&amp;lt;Post, Long&amp;gt; {  
	&amp;lt;T&amp;gt; List&amp;lt;T&amp;gt; findAllByTitleContainsIgnoreCase(String title, Class&amp;lt;T&amp;gt; projection);
}&lt;/pre&gt;
  &lt;p id=&quot;7SiL&quot;&gt;Исключительно в целях удобства метод будет заточен под &lt;a href=&quot;https://www.baeldung.com/spring-data-jpa-projections#dynamic-projections&quot; target=&quot;_blank&quot;&gt;динамическую проекцию&lt;/a&gt;, чтобы не писать под каждую проекцию отдельный метод.&lt;br /&gt;Создадим класс проекции:&lt;/p&gt;
  &lt;pre id=&quot;eleE&quot; data-lang=&quot;java&quot;&gt; public interface PostWithAuthorFlat {  
    Long getId();  
    String getSlug();  
    String getTitle();  
    Long getAuthorId();  
    String getAuthorUsername();  
}&lt;/pre&gt;
  &lt;p id=&quot;3Haf&quot;&gt;Протестируем решение, написав тест. Попробуем немного углубиться в работу проекции, воспользовавшись дебагом. Ставим breakpoint на нашем тесте:&lt;/p&gt;
  &lt;figure id=&quot;tRIg&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/7ec/4f3/22a/7ec4f322a5dc3d29bfc8063ca8781ab2.png&quot; width=&quot;1958&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;G6ri&quot;&gt;Завершающим звеном по получению данных является работа класса TupleBackedMap, сам объект Tuple же будет содержать необходимую нам информацию. Чтобы посмотреть цепочку по получению данных, обозначим границу установкой breakpoint в методе получения значения из TupleBackedMap &lt;code&gt;(org.springframework.data.jpa.repository.query.AbstractJpaQuery.TupleConverter.TupleBackedMap)&lt;/code&gt;:&lt;/p&gt;
  &lt;figure id=&quot;vzlN&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/e9f/6a4/6eb/e9f6a46eb3593c4f697fb9d969342921.png&quot; width=&quot;1868&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;ilGW&quot;&gt;И наблюдаем следующую цепочку по получению данных:&lt;/p&gt;
  &lt;figure id=&quot;TcwG&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/3b1/583/39a/3b158339a53f896ee5054a88059a359d.png&quot; width=&quot;1150&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;QbTO&quot;&gt;Полученный прокси сначала дойдет до метода &lt;code&gt;invoke&lt;/code&gt; класса &lt;code&gt;MapAccessingMethodInterceptor&lt;/code&gt;:&lt;/p&gt;
  &lt;figure id=&quot;EVYi&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/e22/093/332/e220933326bb1a1f4bafb5cdc2970990.png&quot; width=&quot;1980&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;CVGm&quot;&gt;В результате чего мы получаем объект класса &lt;code&gt;Accessor&lt;/code&gt;, который предоставляет доступ к самому &lt;code&gt;propertyName&lt;/code&gt;. Преобразованная строка будет передана в &lt;code&gt;TupleBackedMap&lt;/code&gt;. Далее полученное значение будет нам представлено из всеми нам знакомого объекта &lt;code&gt;Tuple&lt;/code&gt;:&lt;/p&gt;
  &lt;figure id=&quot;5tHN&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/114/4a5/a4b/1144a5a4b4209a20809c180ffcfba573.png&quot; width=&quot;1738&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;aswF&quot;&gt;Сам же объект &lt;code&gt;Tuple&lt;/code&gt; в свою очередь предоставляет нам полный доступ к ключу и значению.&lt;/p&gt;
  &lt;p id=&quot;DT2z&quot;&gt;Всю схему можно описать так:&lt;br /&gt;Под проекцией лежит прокси, в прокси - прокси, в прокси лежит некий target-объект, который является объектом &lt;code&gt;TupleBackedMap&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;mMDK&quot;&gt;Или так:&lt;br /&gt;&lt;em&gt;«На море на океане есть остров, на том острове дуб стоит, под дубом сундук зарыт, в сундуке — заяц, в зайце — утка, в утке — яйцо»&lt;/em&gt; в яйце игла — смерть Кощея!&lt;/p&gt;
  &lt;p id=&quot;asow&quot;&gt;Подводя итоги данного кейса можно сделать вывод о том, что Spring предоставляет конвертер, который сам маппит &lt;code&gt;Tuple&lt;/code&gt; на проекцию. Этот подход отлично подходит для решения поставленной нами задачи, ведь в запросе присутствуют только те колонки, которые указаны в проекции:&lt;/p&gt;
  &lt;pre id=&quot;bzav&quot; data-lang=&quot;java&quot;&gt;Hibernate: 
    select
        p1_0.id,
        p1_0.slug,
        p1_0.title,
        p1_0.author_id,
        a1_0.username 
    from
        posts p1_0 
    left join
        users a1_0 
            on a1_0.id=p1_0.author_id 
    where
        upper(p1_0.title) like upper(?) escape &amp;#x27;\&amp;#x27;&lt;/pre&gt;
  &lt;h4 id=&quot;KPTY&quot;&gt;Interface-based nested interface projections&lt;/h4&gt;
  &lt;p id=&quot;gJ7m&quot;&gt;Следующим способом для решения нашей задачи могло быть решение, основанное на использовании проекции, имеющей внутри себя еще одну проекцию, для получения данных об авторе поста. Это и есть тот самый &lt;strong&gt;nested&lt;/strong&gt; кейс.&lt;/p&gt;
  &lt;pre id=&quot;5IaP&quot; data-lang=&quot;java&quot;&gt;public interface PostWithAuthorNested {  
    Long getId();  
    String getSlug();  
    String getTitle();  
    UserPresentation getAuthor();  
}&lt;/pre&gt;
  &lt;p id=&quot;Q9iE&quot;&gt;Для базовых полей будет загружено только то, что указано в проекции. Мы получим необходимые нам &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;slug&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;. Однако для вложенного объекта будут загружены абсолютно все поля, что конечно, противоречит нашим требованиям. Проблема известна и даже имеет &lt;a href=&quot;https://github.com/spring-projects/spring-data-jpa/issues/3352&quot; target=&quot;_blank&quot;&gt;официальный ответ&lt;/a&gt;.&lt;/p&gt;
  &lt;p id=&quot;gsZE&quot;&gt;Стоит обратить внимание, что &lt;code&gt;PostWithAuthorNested&lt;/code&gt; это все тот же прокси вокруг &lt;code&gt;TupleBackedMap&lt;/code&gt;, а вот сам вложенный объект &lt;code&gt;UserPresentation&lt;/code&gt; является прокси непосредственно вокруг самой сущности &lt;code&gt;User&lt;/code&gt;:&lt;/p&gt;
  &lt;pre id=&quot;weR8&quot; data-lang=&quot;java&quot;&gt;Hibernate: 
    select
        p1_0.id,
        p1_0.slug,
        p1_0.title,
        a1_0.id,
        a1_0.bio,
        a1_0.email,
        a1_0.image,
        a1_0.password,
        a1_0.token,
        a1_0.username 
    from
        posts p1_0 
    left join
        users a1_0 
            on a1_0.id=p1_0.author_id 
    where
        upper(p1_0.title) like upper(?) escape &amp;#x27;\&amp;#x27;&lt;/pre&gt;
  &lt;p id=&quot;Ps5m&quot;&gt;Подводя итоги: подход работает неоптимально, поставленную нами задачу не решает.&lt;/p&gt;
  &lt;h4 id=&quot;u66T&quot;&gt;Class-based flat projections&lt;/h4&gt;
  &lt;p id=&quot;ZOmT&quot;&gt;Идем далее и следующим вариантом решения задачи по частичной выгрузке полей сущности можно отметить использования в качестве проекции отдельного &lt;code&gt;Record&lt;/code&gt;-класса (метод репозитория остается прежним)&lt;/p&gt;
  &lt;pre id=&quot;ZJ0L&quot; data-lang=&quot;java&quot;&gt;public record PostWithAuthorFlatDto(Long id,  
                                    String slug,  
                                    String title,  
                                    Long authorId,  
                                    String authorUsername) {  
}&lt;/pre&gt;
  &lt;p id=&quot;3pnK&quot;&gt;Сам тест:&lt;/p&gt;
  &lt;pre id=&quot;Z5CC&quot; data-lang=&quot;java&quot;&gt;@Test  
void derivedMethodClassFlatPrj() {  
    var posts = postRepository.findAllByTitleContainsIgnoreCase(
      &amp;quot;spring&amp;quot;,
      PostWithAuthorFlatDto.class
    );  
    assertEquals(1, posts.size());  
    var postFirst = posts.getFirst();  
    assertEquals(POST1_SLUG, postFirst.slug());  
    assertEquals(POST1_AUTHOR_NAME, postFirst.authorUsername());  
}&lt;/pre&gt;
  &lt;p id=&quot;hrL5&quot;&gt;&lt;strong&gt;Spring Data JPA&lt;/strong&gt; передает result type в Hibernate, который в свою очередь производит мапинг. На плечах Spring&amp;#x27;а только лишь грамотное формирование &lt;strong&gt;JPQL&lt;/strong&gt; запроса. Этот вариант нас полностью устраивает.&lt;/p&gt;
  &lt;p id=&quot;vWIQ&quot;&gt;Результат:&lt;/p&gt;
  &lt;pre id=&quot;zUWU&quot; data-lang=&quot;java&quot;&gt;Hibernate: 
    select
        p1_0.id,
        p1_0.slug,
        p1_0.title,
        p1_0.author_id,
        a1_0.username 
    from
        posts p1_0 
    left join
        users a1_0 
            on a1_0.id=p1_0.author_id 
    where
        upper(p1_0.title) like upper(?) escape &amp;#x27;\&amp;#x27;&lt;/pre&gt;
  &lt;h4 id=&quot;9zSx&quot;&gt;Class-based nested dto projections&lt;/h4&gt;
  &lt;p id=&quot;ESkV&quot;&gt;Идем хорошим темпом, однако стоило бы рассмотреть и негативные, нерабочие сценарии, чтобы лучше понять логику работы с проекциями. Создадим такой record класс с вложенной DTO-проекцией:&lt;/p&gt;
  &lt;pre id=&quot;WzBp&quot; data-lang=&quot;java&quot;&gt;public record PostWithAuthorNestedDto(Long id,  
                                      String slug,  
                                      String title,  
                                      UserPresentationDto author) {  
}&lt;/pre&gt;
  &lt;p id=&quot;4R4V&quot;&gt;Вроде, все должно заработать, однако при попытке &amp;quot;взлететь&amp;quot; мы получим ошибку:&lt;/p&gt;
  &lt;pre id=&quot;hARD&quot;&gt;Cannot set field &amp;#x27;author&amp;#x27; to instantiate &amp;#x27;io.spring.jpa.projection.PostWithAuthorNestedDto&amp;#x27;&lt;/pre&gt;
  &lt;p id=&quot;61t4&quot;&gt;О чем нас &lt;a href=&quot;https://docs.spring.io/spring-data/jpa/reference/repositories/projections.html#projections.dtos&quot; target=&quot;_blank&quot;&gt;предупреждали&lt;/a&gt;:&lt;/p&gt;
  &lt;figure id=&quot;kMg2&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/751/e8b/0a2/751e8b0a2f3d8c3c326de3544dbc7383.png&quot; width=&quot;1588&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;TLPq&quot;&gt;В данном случае вся логика лежит на стороне Hibernate, Spring Data здесь не оказывает никакого влияния.&lt;/p&gt;
  &lt;h3 id=&quot;ERTv&quot;&gt;Class-based nested entity projections&lt;/h3&gt;
  &lt;p id=&quot;XDAP&quot;&gt;Мы также можем указать для record целую сущность. Однако данный способ обязывает нас получить все поля из вложенной сущности. Придется положить в копилку еще один негативный кейс. Не наш вариант, но знать про это, кажется, было бы полезно:&lt;/p&gt;
  &lt;pre id=&quot;VtwX&quot; data-lang=&quot;java&quot;&gt;public record PostWithAuthorEntity(Long id,  
                                   String slug,  
                                   String title,  
                                   User author) {  
}&lt;/pre&gt;
  &lt;p id=&quot;zw07&quot;&gt;Получаем:&lt;/p&gt;
  &lt;pre id=&quot;TPI2&quot; data-lang=&quot;java&quot;&gt;Hibernate: 
    select
        p1_0.id,
        p1_0.slug,
        p1_0.title,
        a1_0.id,
        a1_0.bio,
        a1_0.email,
        a1_0.image,
        a1_0.password,
        a1_0.token,
        a1_0.username 
    from
        posts p1_0 
    left join
        users a1_0 
            on a1_0.id=p1_0.author_id 
    where
        upper(p1_0.title) like upper(?) escape &amp;#x27;\&amp;#x27;&lt;/pre&gt;
  &lt;h3 id=&quot;Ml7y&quot;&gt;Query-methods&lt;/h3&gt;
  &lt;p id=&quot;0wMD&quot;&gt;А теперь давайте рассмотрим способ, когда мы можем явно в запросе указывать, какие атрибуты нам нужно выгрузить, т.е. к &lt;code&gt;Query&lt;/code&gt;-методам. В этом случае мы сразу же решаем одну немаловажную задачу - явно указываем то, что хотим получить. В нашей зоне ответственности остается только лишь правильно реализовать маппинг.&lt;/p&gt;
  &lt;h4 id=&quot;sCfz&quot;&gt;Interface-based flat projections&lt;/h4&gt;
  &lt;p id=&quot;2FHn&quot;&gt;Spring богат своими возможностями, потому он, например, может обработать &lt;code&gt;JPQL&lt;/code&gt; который мы написали самостоятельно, а затем и вовсе сформировать &lt;code&gt;TupleBackedMap&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;5WCb&quot;&gt;Данный способ интересен тем, что нам ничего не мешает написать обычный &lt;code&gt;JPQL&lt;/code&gt; запрос, а в качестве возвращаемого значения указать проекцию:&lt;/p&gt;
  &lt;pre id=&quot;ouqR&quot; data-lang=&quot;java&quot;&gt;@Query(&amp;quot;&amp;quot;&amp;quot;
            select  a.id as id,
                    a.slug as slug,
                    a.title as title,
                    a.author.id as authorId,
                    a.author.username as authorUsername
            from Post a
            where lower(a.title) like lower(concat(&amp;#x27;%&amp;#x27;, ?1, &amp;#x27;%&amp;#x27;))&amp;quot;&amp;quot;&amp;quot;)
List&amp;lt;PostWithAuthorFlat&amp;gt; findAllPostWithAuthorFlat(String title);&lt;/pre&gt;
  &lt;p id=&quot;JkjM&quot;&gt;Сама проекция:&lt;/p&gt;
  &lt;pre id=&quot;u5Jg&quot; data-lang=&quot;java&quot;&gt;public interface PostWithAuthorFlat {  
    Long getId();  
    String getSlug();  
    String getTitle();  
    Long getAuthorId();  
    String getAuthorUsername();  
}&lt;/pre&gt;
  &lt;p id=&quot;QwEi&quot;&gt;Ключевым моментом формирования &lt;code&gt;JPQL&lt;/code&gt;-запроса является требование по указанию алиасов: они обязательно должны быть такими же, как свойства в проекции, иначе магии маппинга не произойдет. Кстати, как и ожидалось, под интерфейсом лежит все та же прокси с &lt;code&gt;TupleBackedMap&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;2MiF&quot;&gt;Сам результат:&lt;/p&gt;
  &lt;pre id=&quot;6Iyf&quot; data-lang=&quot;java&quot;&gt;Hibernate: 
    select
        p1_0.id,
        p1_0.slug,
        p1_0.title,
        p1_0.author_id,
        a1_0.username 
    from
        posts p1_0 
    join
        users a1_0 
            on a1_0.id=p1_0.author_id 
    where
        lower(p1_0.title) like lower((&amp;#x27;%&amp;#x27;||?||&amp;#x27;%&amp;#x27;)) escape &amp;#x27;&amp;#x27;&lt;/pre&gt;
  &lt;h4 id=&quot;Kwc3&quot;&gt;Class-based flat projections&lt;/h4&gt;
  &lt;p id=&quot;6bmC&quot;&gt;Данный способ по частичному получению данных является базовым для Hibernate. Нужно всего лишь создать класс-проекцию с конструктором. После чего инициализируем этот класс прямо в JPQL. Алиасы в этом случае нам не требуются. Прокси в этом случае также не создается, используется только DTO-проекция.&lt;/p&gt;
  &lt;pre id=&quot;OMfG&quot; data-lang=&quot;java&quot;&gt;@Query(&amp;quot;&amp;quot;&amp;quot;
            select
                new io.spring.jpa.projection.PostWithAuthorFlatDto(
                    a.id, 
                    a.slug, 
                    a.title, 
                    a.author.id, 
                    a.author.username
                ) 
            from Post a
            where lower(a.title) like lower(concat(&amp;#x27;%&amp;#x27;, ?1, &amp;#x27;%&amp;#x27;))&amp;quot;&amp;quot;&amp;quot;)
List&amp;lt;PostWithAuthorFlatDto&amp;gt; findAllPostWithAuthorFlatDto(String title);&lt;/pre&gt;
  &lt;p id=&quot;7DWv&quot;&gt;Проекция:&lt;/p&gt;
  &lt;pre id=&quot;8Wgh&quot; data-lang=&quot;java&quot;&gt;public record PostWithAuthorFlatDto(Long id,  
                                    String slug,  
                                    String title,  
                                    Long authorId,  
                                    String authorUsername) {  
}&lt;/pre&gt;
  &lt;p id=&quot;XQ0U&quot;&gt;Тест:&lt;/p&gt;
  &lt;pre id=&quot;NgwN&quot; data-lang=&quot;java&quot;&gt;@Test  
void queryMethodClassFlat() {  
    var posts = postRepository.findAllPostWithAuthorFlatDto(&amp;quot;spring&amp;quot;);  
    assertEquals(1, posts.size());  
    PostWithAuthorFlatDto post = posts.getFirst();  
    assertEquals(POST1_SLUG, post.slug());  
    assertEquals(POST1_AUTHOR_NAME, post.authorUsername());  
}&lt;/pre&gt;
  &lt;p id=&quot;cV0t&quot;&gt;Успех:&lt;/p&gt;
  &lt;pre id=&quot;pGQW&quot; data-lang=&quot;java&quot;&gt;Hibernate: 
    select
        p1_0.id,
        p1_0.slug,
        p1_0.title,
        p1_0.author_id,
        a1_0.username 
    from
        posts p1_0 
    join
        users a1_0 
            on a1_0.id=p1_0.author_id 
    where
        lower(p1_0.title) like lower((&amp;#x27;%&amp;#x27;||?||&amp;#x27;%&amp;#x27;)) escape &amp;#x27;&amp;#x27;&lt;/pre&gt;
  &lt;h4 id=&quot;hapX&quot;&gt;Class-based nested class projections&lt;/h4&gt;
  &lt;p id=&quot;VCxZ&quot;&gt;Важной особенностью метода с использованием вложенных проекций является то, что можно делать сколько угодно вложенностей. Поскольку мы используем HQL, то &lt;a href=&quot;https://docs.jboss.org/hibernate/orm/6.6/userguide/html_single/Hibernate_User_Guide.html#hql-select-new&quot; target=&quot;_blank&quot;&gt;можем инициализировать нашу DTO&lt;/a&gt; как нам удобно. В том числе и создавая новые DTO объекты внутри DTO.&lt;/p&gt;
  &lt;p id=&quot;IiAW&quot;&gt;Напишем такой метод:&lt;/p&gt;
  &lt;pre id=&quot;oD2M&quot; data-lang=&quot;java&quot;&gt;@Query(&amp;quot;&amp;quot;&amp;quot;
            select
                new io.spring.jpa.projection.PostWithAuthorNestedDto(
                    a.id,
                    a.slug,
                    a.title,
                    new io.spring.jpa.projection.UserPresentationDto(
                        a.author.id,
                        a.author.username
                    )
                )
            from Post a
            where lower(a.title) like lower(concat(&amp;#x27;%&amp;#x27;, ?1, &amp;#x27;%&amp;#x27;))&amp;quot;&amp;quot;&amp;quot;)
List&amp;lt;PostWithAuthorNestedDto&amp;gt; findAllPostWithAuthorNestedDto(String title);&lt;/pre&gt;
  &lt;p id=&quot;afrL&quot;&gt;А также пару проекций под него: проекцию для самого поста, внутри нее объявляем проекцию для автора.&lt;/p&gt;
  &lt;pre id=&quot;Xrlk&quot; data-lang=&quot;java&quot;&gt;public record PostWithAuthorNestedDto(Long id,  
                                      String slug,  
                                      String title,  
                                      UserPresentationDto author) {  
}&lt;/pre&gt;
  &lt;p id=&quot;f3oz&quot;&gt;Проекция для автора:&lt;/p&gt;
  &lt;pre id=&quot;sObc&quot; data-lang=&quot;java&quot;&gt;public record UserPresentationDto(Long id, String username) {  
}&lt;/pre&gt;
  &lt;p id=&quot;u6VN&quot;&gt;Напишем простой тест:&lt;/p&gt;
  &lt;pre id=&quot;qTTg&quot; data-lang=&quot;java&quot;&gt;@Test  
void queryMethodClassNested() {  
    var posts = postRepository.findAllPostWithAuthorNestedDto(&amp;quot;spring&amp;quot;);  
    assertEquals(1, posts.size());  
    PostWithAuthorNestedDto post = posts.getFirst();  
    assertEquals(POST1_SLUG, post.slug());  
    assertEquals(POST1_AUTHOR_NAME, post.author().username());  
}&lt;/pre&gt;
  &lt;p id=&quot;xm74&quot;&gt;Непосредственно запрос:&lt;/p&gt;
  &lt;pre id=&quot;woNC&quot; data-lang=&quot;java&quot;&gt;Hibernate: 
    select
        p1_0.id,
        p1_0.slug,
        p1_0.title,
        p1_0.author_id,
        a1_0.username 
    from
        posts p1_0 
    join
        users a1_0 
            on a1_0.id=p1_0.author_id 
    where
        lower(p1_0.title) like lower((&amp;#x27;%&amp;#x27;||?||&amp;#x27;%&amp;#x27;)) escape &amp;#x27;&amp;#x27;&lt;/pre&gt;
  &lt;h4 id=&quot;DjZk&quot;&gt;Tuple, Object[], List&amp;lt;&amp;gt;, Map&lt;/h4&gt;
  &lt;p id=&quot;SSdf&quot;&gt;Hibernate также позволяет возвращать select expression, которые мы можем написать в виде &lt;code&gt;Object[]&lt;/code&gt;, &lt;code&gt;Tuple&lt;/code&gt;, &lt;code&gt;Map&lt;/code&gt;, &lt;code&gt;List&lt;/code&gt;. Подробно останавливаться на каждом не станем, разница лишь в возвращаемом значении:&lt;/p&gt;
  &lt;pre id=&quot;1L4c&quot; data-lang=&quot;java&quot;&gt;@Query(&amp;quot;&amp;quot;&amp;quot;
            select
                a.id as id,
                a.slug as slug,
                a.title as title
            from Post a
            where lower(a.title) like lower(concat(&amp;#x27;%&amp;#x27;, ?1, &amp;#x27;%&amp;#x27;))&amp;quot;&amp;quot;&amp;quot;)
List&amp;lt;Tuple&amp;gt; findAllTupleBasic(String title);&lt;/pre&gt;
  &lt;pre id=&quot;TmlV&quot; data-lang=&quot;java&quot;&gt;@Query(&amp;quot;&amp;quot;&amp;quot;
            select
                a.id as id,
                a.slug as slug,
                a.title as title
            from Post a
            where lower(a.title) like lower(concat(&amp;#x27;%&amp;#x27;, ?1, &amp;#x27;%&amp;#x27;))&amp;quot;&amp;quot;&amp;quot;)
List&amp;lt;Object[]&amp;gt; findAllObjectWithAuthor(String title);&lt;/pre&gt;
  &lt;pre id=&quot;qooc&quot; data-lang=&quot;java&quot;&gt;@Query(&amp;quot;&amp;quot;&amp;quot;
            select
                a.id as id,
                a.slug as slug,
                a.title as title
            from Post a
            where lower(a.title) like lower(concat(&amp;#x27;%&amp;#x27;, ?1, &amp;#x27;%&amp;#x27;))&amp;quot;&amp;quot;&amp;quot;)
List&amp;lt;List&amp;lt;?&amp;gt;&amp;gt; findAllListWithAuthor(String title);&lt;/pre&gt;
  &lt;pre id=&quot;9eQi&quot; data-lang=&quot;java&quot;&gt;@Query(&amp;quot;&amp;quot;&amp;quot;
            select
                a.id as id,
                a.slug as slug,
                a.title as title
            from Post a
            where lower(a.title) like lower(concat(&amp;#x27;%&amp;#x27;, ?1, &amp;#x27;%&amp;#x27;))&amp;quot;&amp;quot;&amp;quot;)
List&amp;lt;Map&amp;lt;String, Object&amp;gt;&amp;gt; findAllMapWithAuthor(String title);&lt;/pre&gt;
  &lt;p id=&quot;d3R5&quot;&gt;P.S.: для &lt;code&gt;Derived&lt;/code&gt;-методов аналогично можно указывать такие же возвращаемые значения. Spring Data все возьмет на себя и выгрузит все поля.&lt;/p&gt;
  &lt;h3 id=&quot;1mfy&quot;&gt;Entity Manager&lt;/h3&gt;
  &lt;p id=&quot;ef1o&quot;&gt;Все способы, описанные для &lt;code&gt;@Query&lt;/code&gt;, работают и в &lt;strong&gt;Entity Manager&lt;/strong&gt;, кроме &lt;strong&gt;Interface-based projection&lt;/strong&gt;, так как Interface-based projection является концепцией самого Spring, сам же Hibernate ничего про нее не знает.&lt;/p&gt;
  &lt;h3 id=&quot;0wHZ&quot;&gt;Criteria API&lt;/h3&gt;
  &lt;p id=&quot;dJH5&quot;&gt;&lt;strong&gt;Criteria API&lt;/strong&gt; используется для создания типобезопасных и гибких запросов к базе данных на языке Java. Сам по себе Criteria API является &amp;quot;type-safe alternative to HQL&amp;quot;.&lt;/p&gt;
  &lt;p id=&quot;khp8&quot;&gt;Мы будем использовать стандартные функции &lt;code&gt;CriteriaBuilder&lt;/code&gt;, однако, если возникнет потребность в дополнительных функциях из &lt;a href=&quot;https://docs.jboss.org/hibernate/orm/6.4/introduction/html_single/Hibernate_Introduction.html#criteria-queries&quot; target=&quot;_blank&quot;&gt;HibernateCriteriaBuilder&lt;/a&gt;, приведенные ниже примеры будут актуальны и для него.&lt;/p&gt;
  &lt;p id=&quot;Yojq&quot;&gt;Для начала нам нужно указать атрибуты, которые мы собираемся передать в запрос. Имя атрибута мы можем очень удобно указать с помощью константы, которая была сгенерирована автоматически с использованием зависимости:&lt;/p&gt;
  &lt;pre id=&quot;2lQw&quot; data-lang=&quot;java&quot;&gt;dependencies {
	annotationProcessor &amp;#x27;org.hibernate:hibernate-jpamodelgen:{version}&amp;#x27;
}&lt;/pre&gt;
  &lt;pre id=&quot;t5WF&quot; data-lang=&quot;java&quot;&gt;var idPath = owner.&amp;lt;Long&amp;gt;get(Post_.ID);  
var slugPath = owner.&amp;lt;String&amp;gt;get(Post_.SLUG);  
var titlePath = owner.&amp;lt;String&amp;gt;get(Post_.TITLE);  
var authorIdPath = owner.get(Post_.AUTHOR).&amp;lt;Long&amp;gt;get(User_.ID);  
var authorUsernamePath = owner.get(Post_.AUTHOR).&amp;lt;String&amp;gt;get(User_.USERNAME);&lt;/pre&gt;
  &lt;p id=&quot;ex6e&quot;&gt;В последней документации Hibernate данный способ используется во всех примерах, из чего можно сделать вывод, что это &amp;quot;тихая&amp;quot; рекомендация.&lt;/p&gt;
  &lt;p id=&quot;O5s3&quot;&gt;По итогу константы всегда будут перегенерированы и всегда будут находиться в актуальном состоянии, в результате чего мы выигрываем в безопасности и надежности приложения.&lt;/p&gt;
  &lt;h4 id=&quot;HKVf&quot;&gt;DTO&lt;/h4&gt;
  &lt;p id=&quot;lfUQ&quot;&gt;Итак, мы объявили path&amp;#x27;ы, остается лишь написать запрос, закинуть результат в коллекцию, после чего убедиться в жизнеспособности подхода:&lt;/p&gt;
  &lt;pre id=&quot;xVgF&quot; data-lang=&quot;java&quot;&gt;@Test
void criteriaDto() {
  var cb = em.getCriteriaBuilder();
  var query = cb.createQuery(PostWithAuthorFlatDto.class);
  
  var owner = query.from(Post.class);

  var idPath = owner.&amp;lt;Long&amp;gt;get(Post_.ID);
  var slugPath = owner.&amp;lt;String&amp;gt;get(Post_.SLUG);
  var titlePath = owner.&amp;lt;String&amp;gt;get(Post_.TITLE);
  var authorIdPath = owner.get(Post_.AUTHOR).&amp;lt;Long&amp;gt;get(User_.ID);
  var authorUsernamePath = owner.get(Post_.AUTHOR).&amp;lt;String&amp;gt;get(User_.USERNAME);

  query.multiselect(idPath, slugPath, titlePath, authorIdPath, authorUsernamePath)
           .where(cb.like(cb.lower(titlePath), &amp;quot;%spring%&amp;quot;));

  var resultList = em.createQuery(query).getResultList();

  for (PostWithAuthorFlatDto post : resultList) {
      assertEquals(POST1_SLUG, post.slug());
      assertEquals(POST1_AUTHOR_NAME, post.authorUsername());
  }
}&lt;/pre&gt;
  &lt;p id=&quot;znNN&quot;&gt;Проекция:&lt;/p&gt;
  &lt;pre id=&quot;HDH2&quot; data-lang=&quot;java&quot;&gt;public record PostWithAuthorFlatDto(Long id,  
                                    String slug,  
                                    String title,  
                                    Long authorId,  
                                    String authorUsername) {  
}&lt;/pre&gt;
  &lt;p id=&quot;PqWR&quot;&gt;В данном случае мы возвращаем множество элементов (атрибутов) и мапим их на DTO, которое указываем при создании query&lt;/p&gt;
  &lt;pre id=&quot;gFUQ&quot; data-lang=&quot;java&quot;&gt;jakarta.persistence.criteria.CriteriaBuilder#createQuery(java.lang.Class)&lt;/pre&gt;
  &lt;p id=&quot;voeC&quot;&gt;Успешный успех:&lt;/p&gt;
  &lt;pre id=&quot;2HlI&quot; data-lang=&quot;java&quot;&gt;Hibernate: 
    select
        p1_0.id,
        p1_0.slug,
        p1_0.title,
        p1_0.author_id,
        a1_0.username 
    from
        posts p1_0 
    join
        users a1_0 
            on a1_0.id=p1_0.author_id 
    where
        lower(p1_0.title) like ? escape &amp;#x27;&amp;#x27;&lt;/pre&gt;
  &lt;p id=&quot;a8dy&quot;&gt;К минусам работы через DTO можно отнести только то, что сохраняется риск случайного рефактиринга DTO. В этом случае мы поймаем ошибку, что не нашлось, например, подходящего конструктора. Для решения этой проблемы рассмотрим вариант с использованием &lt;code&gt;Tuple&lt;/code&gt;.&lt;/p&gt;
  &lt;h4 id=&quot;ciWy&quot;&gt;Tuple&lt;/h4&gt;
  &lt;p id=&quot;lNK9&quot;&gt;В случае с &lt;code&gt;Tuple&lt;/code&gt; инициализация объекта происходит ручным способом и мы попросту не сможем создать сам объект и, конечно, поймаем ошибку ещё до запуска приложения на этапе компиляции.&lt;/p&gt;
  &lt;p id=&quot;Wv0x&quot;&gt;Представляем вашему вниманию, пожалуй, самый безопасный способ выполнения частичных запросов:&lt;/p&gt;
  &lt;pre id=&quot;W2k9&quot; data-lang=&quot;java&quot;&gt;@Test
void criteriaTuple() {
  var cb = em.getCriteriaBuilder();
  var query = cb.createTupleQuery();

  var owner = query.from(Post.class);

  var idPath = owner.&amp;lt;Long&amp;gt;get(Post_.ID);
  var slugPath = owner.&amp;lt;String&amp;gt;get(Post_.SLUG);
  var titlePath = owner.&amp;lt;String&amp;gt;get(Post_.TITLE);
  var authorIdPath = owner.get(Post_.AUTHOR).&amp;lt;Long&amp;gt;get(User_.ID);
  var authorUsernamePath = owner.get(Post_.AUTHOR).&amp;lt;String&amp;gt;get(User_.USERNAME);

  query.select(cb.tuple(idPath, slugPath, titlePath, authorIdPath, authorUsernamePath))
         .where(cb.like(cb.lower(titlePath), &amp;quot;%spring%&amp;quot;));

  var resultList = em.createQuery(query).getResultList().stream()
           .map(tuple -&amp;gt; new PostWithAuthorNestedDto(
                  tuple.get(idPath),
                  tuple.get(slugPath),
                  tuple.get(titlePath),
                  new UserPresentationDto(
                           tuple.get(authorIdPath),
                           tuple.get(authorUsernamePath)
                  )
            )).toList();

  for (PostWithAuthorNestedDto post : resultList) {
      assertEquals(POST1_SLUG, post.slug());
      assertEquals(POST1_AUTHOR_NAME, post.author().username());
  }
}&lt;/pre&gt;
  &lt;p id=&quot;HBf1&quot;&gt;Сами проекции:&lt;/p&gt;
  &lt;pre id=&quot;4wuf&quot; data-lang=&quot;java&quot;&gt;public record PostWithAuthorNestedDto(Long id,  
                                      String slug,  
                                      String title,  
                                      UserPresentationDto author) {  
}&lt;/pre&gt;
  &lt;pre id=&quot;qjdT&quot; data-lang=&quot;java&quot;&gt;public record UserPresentationDto(Long id, String username) {  
}&lt;/pre&gt;
  &lt;p id=&quot;aqRd&quot;&gt;После формирования Path мы достаем значения из &lt;code&gt;Tuple&lt;/code&gt; и складываем в наше DTO (если не требуются дополнительные манипуляции с выборкой). Таким образом мы получаем не только &amp;quot;type-safe alternative to HQL&amp;quot;, но и безопасную и контролируемую работу с результатом нашего запроса. Концепция очень похожа на работу с &lt;code&gt;Tuple&lt;/code&gt; в библиотеке &lt;a href=&quot;http://querydsl.com/static/querydsl/3.4.2/reference/html/ch03s02.html&quot; target=&quot;_blank&quot;&gt;QueryDSL&lt;/a&gt;.&lt;/p&gt;
  &lt;p id=&quot;E47Z&quot;&gt;В результате получаем запрос, удовлетворяющий наши требования. Остальные примеры для Criteria API рассмотрены вот &lt;a href=&quot;https://github.com/comru/jpa-partial-load/blob/main/src/test/java/io/amplicode/jpa/repository/BasicAttributesTest.java&quot; target=&quot;_blank&quot;&gt;тут&lt;/a&gt;. Работает все максимально типично относительно случаев, рассмотренных выше.&lt;/p&gt;
  &lt;h4 id=&quot;YsF7&quot;&gt;CriteriaDefinition в Hibernate 6.3+&lt;/h4&gt;
  &lt;p id=&quot;19OE&quot;&gt;&lt;strong&gt;UPD&lt;/strong&gt;: Начиная с версии 6.3, Hibernate позволяет нам использовать вспомогательный класс &lt;code&gt;CriteriaDefinition&lt;/code&gt; для уменьшения многословности criteria-запросов.&lt;/p&gt;
  &lt;p id=&quot;dLfx&quot;&gt;Пример с &lt;strong&gt;DTO&lt;/strong&gt; будет выглядеть следующим образом:&lt;/p&gt;
  &lt;pre id=&quot;jc4D&quot; data-lang=&quot;java&quot;&gt;@Test
void criteriaDefinitionDto() {
    var query = new CriteriaDefinition&amp;lt;&amp;gt;(em, PostWithAuthorFlatDto.class) {{
    var owner = from(Post.class);

    var titlePath = owner.&amp;lt;String&amp;gt;get(Post_.TITLE);
    var authorPath = owner.get(Post_.AUTHOR);

    multiselect(
              owner.get(Post_.ID),
              owner.get(Post_.SLUG),
              titlePath,
              authorPath.get(User_.ID),
              authorPath.get(User_.USERNAME)
      ).where(like(lower(titlePath), &amp;quot;%spring%&amp;quot;));
  }};

    var resultList = em.createQuery(query).getResultList();

    assertEquals(1, resultList.size());
    for (var post : resultList) {
        assertEquals(POST1_SLUG, post.slug());
        assertEquals(POST1_AUTHOR_NAME, post.authorUsername());
    }
}&lt;/pre&gt;
  &lt;p id=&quot;FMTW&quot;&gt;Пример с &lt;strong&gt;Tuple&lt;/strong&gt;:&lt;/p&gt;
  &lt;pre id=&quot;Rj0v&quot; data-lang=&quot;java&quot;&gt;@Test
void criteriaDefinitionTuple() {
    var query = new CriteriaDefinition&amp;lt;&amp;gt;(em, Tuple.class) {};
    var owner = query.from(Post.class);

    var idPath = owner.&amp;lt;Long&amp;gt;get(Post_.ID);
    var slugPath = owner.&amp;lt;String&amp;gt;get(Post_.SLUG);
    var titlePath = owner.&amp;lt;String&amp;gt;get(Post_.TITLE);
    var authorPath = owner.get(Post_.AUTHOR);
    var authorIdPath = authorPath.&amp;lt;Long&amp;gt;get(User_.ID);
    var authorUsernamePath = authorPath.&amp;lt;String&amp;gt;get(User_.USERNAME);

    query.where(query.like(query.lower(titlePath), &amp;quot;%spring%&amp;quot;))
            .multiselect(idPath, slugPath, titlePath, authorIdPath, authorUsernamePath);

    var resultList = em.createQuery(query).getResultList().stream()
            .map(tuple -&amp;gt; new PostWithAuthorNestedDto(
                    tuple.get(idPath),
                    tuple.get(slugPath),
                    tuple.get(titlePath),
                    new UserPresentationDto(
                            tuple.get(authorIdPath),
                            tuple.get(authorUsernamePath)
                    )
            )).toList();

    assertEquals(1, resultList.size());
  
    for (var post : resultList) {
        assertEquals(POST1_SLUG, post.slug());
        assertEquals(POST1_AUTHOR_NAME, post.author().username());
    }
}&lt;/pre&gt;
  &lt;p id=&quot;BAvb&quot;&gt;Ознакомиться с результатами работы всех способов вы можете в &lt;a href=&quot;https://github.com/comru/jpa-partial-load&quot; target=&quot;_blank&quot;&gt;репозитории&lt;/a&gt;.&lt;/p&gt;
  &lt;p id=&quot;rsrg&quot;&gt;Кстати, до 6 версии Hibernate &lt;code&gt;Criteria API&lt;/code&gt;-запросы к сущностям выполнялись &lt;a href=&quot;https://vladmihalcea.com/hibernate-sqm-semantic-query-model/&quot; target=&quot;_blank&quot;&gt;следующим образом&lt;/a&gt;:&lt;/p&gt;
  &lt;figure id=&quot;AN9f&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/974/399/b0b/974399b0bef5b0e727c746c5b5553bf0.png&quot; width=&quot;1149&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;yWHH&quot;&gt;Criteria API генерировал обычный JPQL, Hibernate же в свою очередь проводил его анализ в соответствии с грамматикой HQL, только затем генерировался SQL-запрос.&lt;/p&gt;
  &lt;p id=&quot;pznv&quot;&gt;Начиная с Hibernate 6 Criteria API сразу преобразуется в SQM:&lt;/p&gt;
  &lt;figure id=&quot;jFBB&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/d3d/07c/12e/d3d07c12e54d389bac24f0ea34a22107.png&quot; width=&quot;1570&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;hipj&quot;&gt;Подробнее про SQM можно почитать &lt;a href=&quot;https://vladmihalcea.com/hibernate-sqm-semantic-query-model/&quot; target=&quot;_blank&quot;&gt;тут&lt;/a&gt;.&lt;/p&gt;
  &lt;h4 id=&quot;nwOC&quot;&gt;Выводы&lt;/h4&gt;
  &lt;ol id=&quot;9Nfu&quot;&gt;
    &lt;li id=&quot;78fs&quot;&gt;Если мы пишем HQL/JPQL query, то мы контролируем запрос и возвращаем только то что мы хотим. Вопрос только в том, как мапить.&lt;/li&gt;
    &lt;li id=&quot;ZjCy&quot;&gt;Если мы пишем HQL/JPQL всегда можно вернуть &lt;code&gt;Tuple&lt;/code&gt; или &lt;code&gt;Map&lt;/code&gt; и помапить с него на DTO.&lt;/li&gt;
    &lt;li id=&quot;9h9C&quot;&gt;&amp;quot;Использовать ли repository derived method?&amp;quot; - каждый решает сам. В простых случаях и HQL будет простым, в сложных - длина имени метода будет стремиться выйти за пределы нашей солнечной системы. В рамках решаемых задач для этого проекта, конкретно для частичной выгрузки данных, repository derived method выглядит самым небезопасным. Мы не можем контролировать запрос, а HQL может поменяться из-за изменения DTO/Projection или самой Entity.&lt;/li&gt;
    &lt;li id=&quot;xwbR&quot;&gt;Когда мы работаем с &lt;code&gt;Tuple&lt;/code&gt; , очень удобным решением является использование библиотеки hibernate-jpamodelgen, что позволяет нам пользоваться автосгенерированными константами. В последней документации Hibernate данный способ используется во всех примерах, можно сказать, что это &amp;quot;тихая&amp;quot; рекомендация. Также, используя эти константы, легко создавать jakarta.persistence.criteria.Path для Criteria API.&lt;/li&gt;
    &lt;li id=&quot;oEAQ&quot;&gt;Когда мы пишем Query в Spring Data и используем DTO, по дефолту будут валидироваться выражения с DTO: будут проверены как типы, так и количество аргументов в конструкторе. А самое главное - никакой прокси-магии.&lt;/li&gt;
    &lt;li id=&quot;aE39&quot;&gt;Не знаете что вернуть? Верните &lt;code&gt;Tuple&lt;/code&gt;. Это очень удобно.&lt;/li&gt;
    &lt;li id=&quot;htox&quot;&gt;HQL + Class-Based Projection, он же DTO, он же select new class конструкция работают всегда, включая вложенные классы.&lt;/li&gt;
    &lt;li id=&quot;cOd3&quot;&gt;Для ToMany, при выгрузки данных в виде Projection/DTO/Tuple нам придется решать вопрос о мердже дублирующихся данных.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;xiUA&quot;&gt;&lt;a href=&quot;https://habr.com/ru/companies/spring_aio/articles/833918/&quot; target=&quot;_blank&quot;&gt;Источник&lt;/a&gt;&lt;/p&gt;

</content></entry><entry><id>javalib:37Zr4Uq6jqJ</id><link rel="alternate" type="text/html" href="https://teletype.in/@javalib/37Zr4Uq6jqJ?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=javalib"></link><title>Быстрое нахождение чисел Фибоначчи</title><published>2024-05-08T06:56:53.527Z</published><updated>2024-05-08T06:56:53.527Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img1.teletype.in/files/43/41/43413ab7-a4dc-4e1c-97b2-7b2b4c4dc9c0.png"></media:thumbnail><category term="java" label="Java"></category><summary type="html">&lt;img src=&quot;https://habrastorage.org/getpro/habr/post_images/323/4c1/240/3234c124030e87462028bc49d9f9b08a.svg&quot;&gt;Описание способа нахождения значения произвольного элемента последовательности Фибоначчи за логарифмическое время.</summary><content type="html">
  &lt;h2 id=&quot;cb4J&quot;&gt;Быстрое нахождение чисел Фибоначчи&lt;/h2&gt;
  &lt;p id=&quot;S6xX&quot;&gt;Описание способа нахождения значения произвольного элемента последовательности Фибоначчи за логарифмическое время.&lt;/p&gt;
  &lt;figure id=&quot;bPVd&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/getpro/habr/post_images/323/4c1/240/3234c124030e87462028bc49d9f9b08a.svg&quot; width=&quot;241&quot; /&gt;
    &lt;figcaption&gt;Спираль Фибоначчи&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;YN7j&quot;&gt;Многие из тех, кто только приступает к изучению программирования, с одной из первых, сталкиваются с задачей о кузнечике.&lt;/p&gt;
  &lt;p id=&quot;63qU&quot;&gt;На координатном луче, в точке с координатой 1 находится кузнечик. Этот кузнечик может прыгать только вперёд на расстояние 1 либо 2. Сколькими способами он может добраться до точки с координатой &lt;em&gt;n&lt;/em&gt;?&lt;br /&gt;Формула для решения этой задачи выводится достаточно просто.&lt;br /&gt;Понятно, что до точки с координатой 0 кузнечик допрыгнуть не может - двигаться назад он не умеет. Способов попасть в эту точку 0. Попасть в точку с координатой 1, в которой он изначально находится, кузнечик может ровно одним способом - оставаться на месте. В любую другую точку с координатой &lt;em&gt;n&lt;/em&gt;, кузнечик может добраться либо из точки с координатой &lt;em&gt;n-1&lt;/em&gt;, либо из точки с координатой &lt;em&gt;n-2&lt;/em&gt;. Соответственно, количество способов которыми он может достичь точки с координатой &lt;em&gt;n&lt;/em&gt; равно сумме количеств способов которыми он может достичь точек с координатами &lt;em&gt;n-1&lt;/em&gt;, и &lt;em&gt;n-2&lt;/em&gt;. Иными словами, если функция &lt;em&gt;f(n)&lt;/em&gt; вычисляет количество способов имеющихся у кузнечика чтобы попасть в точку &lt;em&gt;n&lt;/em&gt;, то &lt;em&gt;f(n) = f(n-1) + f(n-2)&lt;/em&gt;.&lt;/p&gt;
  &lt;p id=&quot;5Mye&quot;&gt;Другая задача. В начале первого месяца вы получаете пару новорождённых кроликов. Через месяц, эти кролики повзрослеют. Начиная с третьего месяца и далее они будут стабильно давать приплод - ещё одну пару новорождённых кроликов. Таким образом, каждая появившаяся пара кроликов, начиная с двух месяцев после своего появления, каждый месяц порождает ещё пару кроликов. Количество кроликов никогда не уменьшается - кролики не болеют и не умирают. Задача: рассчитать какое количество пар кроликов будет у вас, на &lt;em&gt;n&lt;/em&gt;-й месяц.&lt;br /&gt;Формула для решения этой задачи, совпадает с формулой, полученной нами в задаче о кузнечике - &lt;em&gt;f(n) = f(n-1) + f(n-2)&lt;/em&gt;. Вполне очевидно, что каждый месяц к тому количеству кроликов, что было у вас в прошлом месяце прибавляется приплод, от тех кроликов которые у вас были два месяца назад, так как все они к этому моменту уже достаточно повзрослели, чтобы начать активно размножаться.&lt;br /&gt;К счастью, у реальных кроликов несколько иные характеристики, иначе &lt;a href=&quot;https://xkcd.com/605/&quot; target=&quot;_blank&quot;&gt;всего лишь через 9 лет&lt;/a&gt;, после появления первой пары кроликов, вся поверхность планеты была бы покрыта более чем 100-километровым плотным слоем кроличьих тушек.&lt;/p&gt;
  &lt;p id=&quot;LHxT&quot;&gt;Для решения обеих задач используется одна и та же формула: &lt;strong&gt;&lt;em&gt;f(n) = f(n-1) + f(n-2)&lt;/em&gt;&lt;/strong&gt;. Это формула чисел последовательности Фибоначчи. Эта последовательность начинается с элемента под номером 0 и значением 0. За ним следует элемент под номером 1 и значением 1. Значения всех остальных элементов вычисляются согласно приведённой формуле - значение каждого из элементов равно сумме значений двух предыдущих.&lt;/p&gt;
  &lt;p id=&quot;9T6o&quot;&gt;Для неотрицательных номеров элементов значения принимают следующий вид:&lt;br /&gt;0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, ...&lt;/p&gt;
  &lt;p id=&quot;Ugun&quot;&gt;Однако, из формулы &lt;em&gt;f(n) = f(n-1) + f(n-2)&lt;/em&gt; вытекает, что &lt;em&gt;f(n-2) = f(n) - f(n-1)&lt;/em&gt;. Зная это, мы можем определить значения элементов последовательности и с отрицательными номерами. Так, элемент под номером -1 имеет значение 1, элемент под номером -2 - значение -1. Мы можем представить общий вид вот так:&lt;br /&gt;..., -377, 233, -144, 89, -55, 34, -21, 13, -8, 5, -3, 2, -1, 1, 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, ...&lt;/p&gt;
  &lt;p id=&quot;XAkL&quot;&gt;Несложно заметить, что значения элементов с отрицательными номерами, являются практически зеркальным отражением значений элементов с положительными номерами, с тем исключением, что элементы под чётными номерами имеют противоположный знак. Мы можем выразить это следующей формулой &lt;strong&gt;&lt;em&gt;f(-n) = (-1)^(n+1)f(n)&lt;/em&gt;&lt;/strong&gt;. Благодаря данной формуле мы легко можем получить значение элемента под тем же номером с противоположным знаком. Доказательство корректности этой формулы оставляю в качестве небольшого упражнения читателям.&lt;/p&gt;
  &lt;p id=&quot;sbnL&quot;&gt;Теперь перейдём к задаче вычисления числа Фибоначчи со сколько-нибудь большим положительным номером. Скажем, 10000000.&lt;/p&gt;
  &lt;p id=&quot;M0s2&quot;&gt;Прежде чем приступать к решению, напишем несколько вспомогательных методов.&lt;/p&gt;
  &lt;pre id=&quot;DHYB&quot; data-lang=&quot;java&quot;&gt;package fibonacci;

import java.math.BigInteger;

/**
 * Нахождение чисел Фибоначчи.
 * &amp;lt;p&amp;gt;Класс предоставляет статический метод, для нахождения чисел Фибоначчи.&amp;lt;/p&amp;gt;
 */
public class Fibonacci {

// static fields

	/**
	 * Полная форма опции печати.
	 */
	private static final String PRINT_OPTION_FULL_FORM = &amp;quot;--print&amp;quot;;

	/**
	 * Короткая форма опции печати.
	 */
	private static final String PRINT_OPTION_SHORT_FORM = &amp;quot;-p&amp;quot;;

// static methods

	/**
	 * Точка входа в приложение.
	 * &amp;lt;p&amp;gt;Метод, предполагая, что начальным аргументом запуска указан номер желаемого числа Фибоначчи, вычисляет это число и выводит в стандартный поток ошибок время его вычисления. Опционально, если вторым аргументом является строка {@value #PRINT_OPTION_FULL_FORM}, или {@value #PRINT_OPTION_SHORT_FORM} в стандартный поток выводится значение выбранного числа, а в поток ошибок дополнительно выводится время вывода числа. Значение числа выводится до вывода длительностей вычисления и вывода.&amp;lt;/p&amp;gt;
	 * @param args Набор аргументов запуска.
	 * @throws NullPointerException Если набор аргументов запуска не существует.
	 * @throws ArrayIndexOutOfBoundsException Если набор аргументов запуска пуст.
	 * @throws NumberFormatException Если начальный аргумент запуска не является строкой, содержащей запись целого числа.
	 * @throws IllegalArgumentException Если в качестве начального аргумента запуска указанно значение {@link Integer#MIN_VALUE}.
	 */
	public static void main (
		final String... args
	) { // method body
		final int n = Integer.parseInt(args[0]);
		final boolean doPrint = (args.length &amp;gt; 1) &amp;amp;&amp;amp; (args[1].equals(PRINT_OPTION_FULL_FORM) || args[1].equals(PRINT_OPTION_SHORT_FORM));
		Instant start;
		Instant finish;
		final Duration calculationDuration;
		Duration outputDuration = null;
		start = Instant.now();
		final BigInteger fibValue = fib(n);
		finish = Instant.now();
		calculationDuration = Duration.between(start, finish);
		if (doPrint) {
			start = Instant.now();
			System.out.println(fibValue);
			finish = Instant.now();
			outputDuration = Duration.between(start, finish);
		} // if
		System.err.println(calculationDuration);
		if (doPrint) {
			System.err.println(outputDuration);
		} // if
	} // main()

	/**
	 * Число Фибоначчи под указанным номером.
	 * &amp;lt;p&amp;gt;Метод вычисляет и возвращает число Фибоначчи под указанным номером. Номер может быть как положительным, так и отрицательным.&amp;lt;/p&amp;gt;
	 * @param n Номер числа в последовательности. Не должен являться {@link Integer#MIN_VALUE}.
	 * @return Выбранное число Фибоначчи.
	 * @throws IllegalArgumentException Если указанный номер равен {@link Integer#MIN_VALUE}.
	 */
	public static BigInteger fib (
		final int n
	) { // method body
		if (n == Integer.MIN_VALUE) throw new IllegalArgumentException();
		BigInteger answer = null; // Здесь вычисляем абсолютное значение ответа, обратившись к одному из методов.
		if ((n &amp;lt; 0) &amp;amp;&amp;amp; (n % 2 == 0)) {
			answer = answer.negate();
		} // if
		return answer;
	} // fib()

// constructors

	/**
	 * Конструктор, предотвращающий создание экземпляров класса.
	 * &amp;lt;p&amp;gt;Конструктор объявлен с единственной целью - предотвратить создание экземпляров класса.&amp;lt;/p&amp;gt;
	 * @throws NoSuchMethodError При любом обращении к данному конструктору.
	 */
	private Fibonacci (
	) { // method body
		throw new NoSuchMethodError();
	} // Fibonacci()

} // Fibonacci&lt;/pre&gt;
  &lt;h3 id=&quot;wy9q&quot;&gt;Рекурсия.&lt;/h3&gt;
  &lt;p id=&quot;4n2v&quot;&gt;Мы можем напрямую воспользоваться формулой &lt;em&gt;f(n) = f(n-1) + f(n-2)&lt;/em&gt;, и выразить функцию вычисления рекурсивно.&lt;/p&gt;
  &lt;p id=&quot;kdXj&quot;&gt;На языке Scheme это будет выглядеть так:&lt;/p&gt;
  &lt;pre id=&quot;zpzq&quot; data-lang=&quot;java&quot;&gt;(define (fib n)
        ((if (and (negative? n) (even? n))
             -
             +) (fib-naive (abs n))))

(define (fib-naive n)
        (cond ((= n 0) 0)
              ((= n 1) 1)
              (else (+ (fib-naive (- n 2)) (fib-naive (- n 1))))))&lt;/pre&gt;
  &lt;p id=&quot;CgtC&quot;&gt;На Java можно выразиться следующим образом:&lt;/p&gt;
  &lt;pre id=&quot;ZFih&quot; data-lang=&quot;java&quot;&gt;/**
 * Рекурсивное нахождение чисел Фибоначчи.
 * &amp;lt;p&amp;gt;Метод реализует рекурсивный алгоритм нахождения чисел Фибоначчи.&amp;lt;/p&amp;gt;
 * @param n Номер числа в последовательности. Если номер отрицателен, то поведение метода не определено.
 * @return Значение выбранного числа Фибоначчи.
 */
private static BigInteger fibNaive (
	final int n
) { // method body
	return switch (n) {
		case 0 -&amp;gt; BigInteger.ZERO;
		case 1 -&amp;gt; BigInteger.ONE;
		default -&amp;gt; fibNaive(n-1).add(fibNaive(n-2));
	}; // switch
} // fibNaive()&lt;/pre&gt;
  &lt;p id=&quot;XlUa&quot;&gt;Однако, так как рекурсия требует многократного повторного вычисления одних и тех же значений, такой подход является крайне неэффективным, хотя и позволяет получить правильный результат. Но, по причине неэффективности, о сколько-нибудь больших номерах чисел в последовательности речи не идёт. Уже запрос 45-го числа Фибоначчи, заставляет машину задуматься на заметное время, что делает нашу цель - 10000000-е число последовательности, практически недостижимой.&lt;/p&gt;
  &lt;h3 id=&quot;V2gJ&quot;&gt;Итеративный подход&lt;/h3&gt;
  &lt;p id=&quot;dhvv&quot;&gt;Мы могли бы значительно сократить количество вычислений, воспользовавшись динамическим программированием. Например, просто сохраняя значения уже вычисленных элементов последовательности в массиве, и используя их повторно, при рекурсивных запросах. Это так называемая &amp;quot;ленивая динамика&amp;quot;. Можно заметить, что в процессе рекурсивные вызовы всегда доходят до нулевого элемента, и, таким образом, используются значения всех элементов вплоть до целевого. Это позволило бы заменить рекурсию на последовательное заполнение массива вычисленных элементов. Но в процессе написания такого кода, стала бы очевидной вещь, явным образом проистекающая из формулы чисел. Давайте вспомним эту формулу:&lt;/p&gt;
  &lt;p id=&quot;iHTK&quot;&gt;&lt;em&gt;f(n) = f(n-2) + f(n-1);&lt;/em&gt;&lt;/p&gt;
  &lt;p id=&quot;9Sw7&quot;&gt;Для получения значения любого из элементов нам нужны значения только двух элементов предшествующих целевому, окружающих его, либо следующих за ним, но не остальные. В сущности, зная значения только любых двух смежных элементов последовательности, мы можем, постепенно продвигаясь в одном из направлений, узнать значение любого другого из элементов последовательности.&lt;/p&gt;
  &lt;p id=&quot;OdrG&quot;&gt;Так, зная значения нулевого и первого элементов последовательности, мы можем получить значение второго. Зная значения первого и второго, получаем значение третьего, и так далее.&lt;/p&gt;
  &lt;p id=&quot;pbGL&quot;&gt;Давайте выразим всё это в коде, для упрощения реализации начиная с пары минус первого и нулевого элементов.&lt;/p&gt;
  &lt;p id=&quot;iWk0&quot;&gt;На Scheme можем выразиться так:&lt;/p&gt;
  &lt;pre id=&quot;0SnU&quot; data-lang=&quot;java&quot;&gt;(define (fib n)
        ((if (and (negative? n) (even? n))
             -
             +) (fib-iter 1 0 (abs n))))

(define (fib-iter prev cur n)
        (cond ((zero? n) cur)
              (else (fib-iter cur (+ prev cur) (- n 1)))))&lt;/pre&gt;
  &lt;p id=&quot;78tK&quot;&gt;А на Java - так:&lt;/p&gt;
  &lt;pre id=&quot;cTtd&quot; data-lang=&quot;java&quot;&gt;/**
 * Последовательное нахождение чисел Фибоначчи.
 * &amp;lt;p&amp;gt;Метод реализует итеративный алгоритм нахождения чисел Фибоначчи.&amp;lt;/p&amp;gt;
 * @param n Номер числа в последовательности. Если номер отрицателен, то поведение метода не определено.
 * @return Значение выбранного числа Фибоначчи.
 */
private static BigInteger fibIter (
	final int n
) { // method body
	BigInteger prev = BigInteger.ONE;
	BigInteger cur = BigInteger.ZERO;
	for (int i = n; i &amp;gt; 0; i--) {
		final BigInteger next = cur.add(prev);
		prev = cur;
		cur = next;
	} // for
	return cur;
} // fibIter()&lt;/pre&gt;
  &lt;p id=&quot;GVT6&quot;&gt;Теперь вычисления происходят несравненно быстрее, так как количество операций линейное. Вычислительная сложность алгоритма &lt;em&gt;O(n)&lt;/em&gt;. Для вычисления числа Фибоначчи с номером 1000000 придётся немного подождать, но оно стало достижимо. Наша цель - число Фибоначчи с номером 10000000, тоже достижима для этого алгоритма, но из-за большого числа сложений действительно больших чисел, ждать придётся достаточно времени, для того чтобы не спеша выпить чашечку кофе. Хотелось бы уменьшить это время.&lt;/p&gt;
  &lt;h3 id=&quot;j2uH&quot;&gt;Задача о быстром умножении&lt;/h3&gt;
  &lt;p id=&quot;u0ro&quot;&gt;Для того, чтобы понять как мы можем находить числа Фибоначчи быстрее, рассмотрим следующую задачу:&lt;/p&gt;
  &lt;p id=&quot;Fn3m&quot;&gt;Предположим, у нас имеется некий числовой тип данных, для значений которого определена только операция сложения. А нам нужно умножить значение этого типа &lt;em&gt;a&lt;/em&gt; на не отрицательный, целый скаляр &lt;em&gt;n&lt;/em&gt;.&lt;/p&gt;
  &lt;p id=&quot;YldD&quot;&gt;Для начала, мы можем вспомнить, что операция умножения - это многократное повторение операции сложения. Так &lt;em&gt;mul(a, n) = a + a + a + ... + a&lt;/em&gt;, где значение &lt;em&gt;a&lt;/em&gt; повторяется &lt;em&gt;n&lt;/em&gt; раз.&lt;/p&gt;
  &lt;p id=&quot;qFcA&quot;&gt;Операция сложения, может быть выражена через рекурсию &lt;em&gt;mul(a, n) = a + mul(a, n-1)&lt;/em&gt;, что на Scheme можно высказать так:&lt;/p&gt;
  &lt;pre id=&quot;CCa7&quot; data-lang=&quot;java&quot;&gt;(define (mul a n)
        (cond ((zero? n) 0)
              (else (+ a (mul a (- n 1))))))&lt;/pre&gt;
  &lt;p id=&quot;uARH&quot;&gt;Мы можем отказаться от использования рекурсии, заменив её итерациями. Это вытекает из формулы &lt;em&gt;mul(a, n) = mul(a, n-1) + a&lt;/em&gt;, если предположить, что к моменту совершения данного шага операции значение &lt;em&gt;mul(a, n-1)&lt;/em&gt; уже вычислено. Выразим этот подход на Scheme:&lt;/p&gt;
  &lt;pre id=&quot;NIbm&quot; data-lang=&quot;java&quot;&gt;(define (mul a n)
        (mul-iter a 0 n))

(define (mul-iter val accum count)
        (cond ((zero? count) accum)
              (else (mul-iter val (+ accum val) (- count 1)))))&lt;/pre&gt;
  &lt;p id=&quot;vVSn&quot;&gt;Вычислительная сложность обоих этих походов, в данной задаче &lt;em&gt;O(n)&lt;/em&gt;, однако итеративный подход потребляет константное количество памяти, тогда как рекурсивному требуется &lt;em&gt;O(n)&lt;/em&gt; дополнительной памяти для хранения промежуточных результатов.&lt;/p&gt;
  &lt;p id=&quot;8yHe&quot;&gt;Мы можем улучшить вычислительную сложность до &lt;em&gt;O(log(n))&lt;/em&gt; если вспомним, как значение скалярного числа &lt;em&gt;n&lt;/em&gt; вычисляется из его записи в двоичной системе счисления. К примеру число 140 в двоичной системе записывается как 10001100. Значение числа из этой записи получается суммированием произведений значения каждой из цифр, на значение единиц её разряда. В случае числа 140 это выглядит как &lt;em&gt;128*1 + 64*0 + 32*0 + 16*0 + 8*1 + 4*1 + 2*0 + 1*0 = 128 + 8 + 4 = 140&lt;/em&gt;. Таким образом, если значение &lt;em&gt;a&lt;/em&gt; умножается на скаляр 140, мы можем выразить это, как &lt;em&gt;a*140 = a*128 + a*8 + a*4&lt;/em&gt;. Теперь вспомним, что степени двойки, это число 2 умноженное само на себя несколько раз. Так &lt;em&gt;128 = 2*2*2*2*2*2*2&lt;/em&gt;, &lt;em&gt;8 = 2*2*2&lt;/em&gt; и &lt;em&gt;4 = 2*2&lt;/em&gt;. Получается, что выражение &lt;em&gt;a*128 + a*8 + a*4&lt;/em&gt; эквивалентно выражению &lt;em&gt;(((((((a*2)*2)*2)*2)*2)*2)*2) + (((a*2)*2)*2) + ((a*2)*2)&lt;/em&gt;. Используя это, а так же то, что операцию удвоения можно представить как сложение числа с самим собой, мы можем получить алгоритм умножения, работающий за логарифмическое время. Запишем его на языке Scheme:&lt;/p&gt;
  &lt;pre id=&quot;zfha&quot; data-lang=&quot;java&quot;&gt;(define (mul a n)
        (quickMul a 0 n))

(define (quickMul val accum count)
        (cond ((zero? count) accum)
              ((even? count) (quickMul (double val) accum (/ count 2)))
              (else (quickMul val (+ accum val) (- count 1)))))

(define (double a)
        (+ a a))&lt;/pre&gt;
  &lt;p id=&quot;wOT2&quot;&gt;Заметьте, что для значений того же типа, что и &lt;em&gt;a&lt;/em&gt;, в этом алгоритме используется только операция сложения, а вычислительная сложность улучшена до логарифмической, при константном потреблении памяти.&lt;/p&gt;
  &lt;h3 id=&quot;XnhF&quot;&gt;Формулы&lt;/h3&gt;
  &lt;p id=&quot;yxtc&quot;&gt;Рассмотрев задачу о быстром умножении мы снова можем вернуться к числам Фибоначчи.&lt;/p&gt;
  &lt;p id=&quot;sS0C&quot;&gt;Сначала давайте подробнее рассмотрим формулу числа с номером &lt;em&gt;n&lt;/em&gt;:&lt;/p&gt;
  &lt;p id=&quot;ms4l&quot;&gt;&lt;em&gt;f(n) = f(n-2) + f(n-1)&lt;/em&gt;&lt;/p&gt;
  &lt;p id=&quot;kdvV&quot;&gt;Каждое из чисел Фибоначчи образовано суммой двух предшествующих чисел Фибоначчи. Каждое из двух предшествующих тоже является суммой, уже предшествующих ему чисел последовательности. Давайте попробуем последовательно раскладывать на слагаемые самый старший элемент последовательности, из правой части уравнения:&lt;br /&gt;&lt;code&gt;f(n) = 1f(n) = 0f(n-1) + 1f(n-0);&lt;/code&gt;&lt;br /&gt;&lt;code&gt;f(n) = 0f(n-1) + 1(f(n-1) + f(n-2)) = 1f(n-2) + 1f(n-1);&lt;/code&gt;&lt;br /&gt;&lt;code&gt;f(n) = 1f(n-2) + 1(f(n-2) + f(n-3)) = 1f(n-3) + 2f(n-2);&lt;/code&gt;&lt;br /&gt;&lt;code&gt;f(n) = 1f(n-3) + 2(f(n-3) + f(n-4)) = 2f(n-4) + 3f(n-3);&lt;/code&gt;&lt;br /&gt;&lt;code&gt;f(n) = 2f(n-4) + 3(f(n-4) + f(n-5)) = 3f(n-5) + 5f(n-4);&lt;/code&gt;&lt;br /&gt;&lt;code&gt;f(n) = 3f(n-5) + 5(f(n-5) + f(n-6)) = 5f(n-6) + 8f(n-5);&lt;/code&gt;&lt;br /&gt;&lt;code&gt;f(n) = 5f(n-6) + 8(f(n-6) + f(n-7)) = 8f(n-7) + 13f(n-6);&lt;/code&gt;&lt;/p&gt;
  &lt;p id=&quot;Cy0z&quot;&gt;Обратим внимание на последовательности коэффициентов слагаемых в самой правой части уравнений. Это две последовательности чисел: 0, 1, 1, 2, 3, 5, 8, ... и 1, 1, 2, 3, 5, 8, 13, .... Не правда ли, эти последовательности схожи между собой, и по какой-то причине кажутся нам знакомыми... Неудивительно, ведь это первые числа последовательности Фибоначчи. Итак, взглянув на эти коэффициенты, мы можем предположить существование следующей формулы:&lt;/p&gt;
  &lt;p id=&quot;JvCo&quot;&gt;&lt;strong&gt;&lt;em&gt;f(n) = f(x)f(n-(x+1)) + f(x+1)f(n-x)&lt;/em&gt;&lt;/strong&gt;; Где &lt;em&gt;x&lt;/em&gt; - некоторое целое число. &lt;strong&gt;(1)&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;crlH&quot;&gt;Покажем, с помощью математической индукции, справедливость этой формулы при любом значении &lt;em&gt;x&lt;/em&gt;.&lt;/p&gt;
  &lt;p id=&quot;yorU&quot;&gt;&lt;strong&gt;База индукции.&lt;/strong&gt;&lt;br /&gt;Покажем, что формула справедлива при &lt;em&gt;x = 0&lt;/em&gt;:&lt;br /&gt;&lt;em&gt;f(n) = f(0)f(n-(0+1)) + f(0+1)f(n-0);&lt;/em&gt;&lt;br /&gt;&lt;em&gt;f(n) = f(0)f(n-1) + f(1)f(n);&lt;/em&gt;&lt;br /&gt;&lt;em&gt;f(n) = 0f(n-1) + 1f(n);&lt;/em&gt;&lt;br /&gt;&lt;em&gt;f(n) = f(n);&lt;/em&gt;&lt;/p&gt;
  &lt;p id=&quot;eOIV&quot;&gt;&lt;strong&gt;Индуктивный переход при увеличении значения &lt;em&gt;x&lt;/em&gt;.&lt;/strong&gt;&lt;br /&gt;Покажем, что если формула справедлива при &lt;em&gt;x=k&lt;/em&gt;, то формула справедлива и при &lt;em&gt;x = k + 1&lt;/em&gt;.&lt;br /&gt;Предположим, что равенство &lt;em&gt;f(n) = f(k)f(n-(k+1)) + f(k+1)f(n-k)&lt;/em&gt; верно. Тогда, при &lt;em&gt;x = k + 1&lt;/em&gt;:&lt;br /&gt;&lt;em&gt;f(n) = f(k+1)f(n-((k+1)+1)) + f((k+1)+1)f(n-(k+1));&lt;/em&gt;&lt;br /&gt;&lt;em&gt;f(n) = f(k+1)f(n-(k+2)) + f(k+2)f(n-(k+1));&lt;/em&gt;&lt;br /&gt;&lt;em&gt;f(n) = f(k+1)f(n-k-2) + f(k+2)f(n-(k+1));&lt;/em&gt;&lt;br /&gt;&lt;em&gt;f(n) = f(k+1)(f(n-k) - f(n-k-1)) + (f(k) + f(k+1))f(n-(k+1));&lt;/em&gt;&lt;br /&gt;&lt;em&gt;f(n) = f(k+1)f(n-k) + f(k)f(n-(k+1)) + f(k+1)f(n-(k+1)) - f(k+1)f(n-k-1);&lt;/em&gt;&lt;br /&gt;&lt;em&gt;f(n) = f(k+1)f(n-k) + f(k)f(n-(k+1));&lt;/em&gt;&lt;br /&gt;&lt;em&gt;f(n) = f(n);&lt;/em&gt;&lt;/p&gt;
  &lt;p id=&quot;RAVh&quot;&gt;&lt;strong&gt;Индуктивный переход при уменьшении значения &lt;em&gt;x&lt;/em&gt;.&lt;/strong&gt;&lt;br /&gt;Покажем, что если формула справедлива при &lt;em&gt;x=k&lt;/em&gt;, то формула справедлива и при &lt;em&gt;x = k - 1&lt;/em&gt;.&lt;br /&gt;Предположим, что равенство &lt;em&gt;f(n) = f(k)f(n-(k+1)) + f(k+1)f(n-k)&lt;/em&gt; верно. Тогда, при &lt;em&gt;x = k - 1&lt;/em&gt;:&lt;br /&gt;&lt;em&gt;f(n) = f(k-1)f(n-(k-1+1)) + f(k-1+1)f(n-(k-1));&lt;/em&gt;&lt;br /&gt;&lt;em&gt;f(n) = f(k-1)f(n-k) + f(k)f(n-k+1);&lt;/em&gt;&lt;br /&gt;&lt;em&gt;f(n) = (f(k+1) - f(k))f(n-k) + f(k)(f(n-k) + f(n-k-1));&lt;/em&gt;&lt;br /&gt;&lt;em&gt;f(n) = f(k)f(n-k) - f(k)f(n-k) + f(k)f(n-k-1) + f(k+1)f(n-k);&lt;/em&gt;&lt;br /&gt;&lt;em&gt;f(n) = f(k)f(n-(k+1)) + f(k+1)f(n-k);&lt;/em&gt;&lt;br /&gt;&lt;em&gt;f(n) = f(n);&lt;/em&gt;&lt;/p&gt;
  &lt;p id=&quot;E959&quot;&gt;Теперь, благодаря формуле &lt;em&gt;(1)&lt;/em&gt;, мы можем вывести формулу числа Фибоначчи от суммы индексов. Для этого предположим, что &lt;em&gt;n = a + b&lt;/em&gt;. Подставим это выражение в формулу &lt;em&gt;(1)&lt;/em&gt;:&lt;br /&gt;&lt;em&gt;f(a+b) = f(x)f((a+b)-(x+1)) + f(x+1)f((a+b)-x);&lt;/em&gt;&lt;/p&gt;
  &lt;p id=&quot;nBrn&quot;&gt;Теперь, установим &lt;em&gt;x = a&lt;/em&gt;, и подставим в формулу, полученную на предыдущем шаге:&lt;br /&gt;&lt;em&gt;f(a+b) = f(a)f((a+b)-(a+1)) + f(a+1)f((a+b)-a);&lt;/em&gt;&lt;br /&gt;&lt;em&gt;f(a+b) = f(a)f(a+b-a-1) + f(a+1)f(a+b-a);&lt;/em&gt;&lt;br /&gt;&lt;em&gt;f(a+b) = f(a)f(b-1) + f(a+1)f(b);&lt;/em&gt;&lt;/p&gt;
  &lt;p id=&quot;wsJL&quot;&gt;Таким образом:&lt;br /&gt;&lt;strong&gt;&lt;em&gt;f(a+b) = f(a)f(b-1) + (f(a-1) + f(a))f(b)&lt;/em&gt;&lt;/strong&gt;; &lt;strong&gt;(2)&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;XL9B&quot;&gt;Кроме того, воспользовавшись формулой &lt;em&gt;(2)&lt;/em&gt;, мы можем вывести формулу числа Фибоначчи от удвоенного индекса.&lt;/p&gt;
  &lt;p id=&quot;gHqU&quot;&gt;Установим в формуле &lt;em&gt;(2)&lt;/em&gt; оба слагаемых равными &lt;em&gt;n&lt;/em&gt;:&lt;br /&gt;&lt;em&gt;f(n+n) = f(n)f(n-1) + (f(n-1) + f(n))f(n);&lt;/em&gt;&lt;/p&gt;
  &lt;p id=&quot;9oYD&quot;&gt;&lt;strong&gt;&lt;em&gt;f(2n) = f(n)(2f(n-1) + f(n))&lt;/em&gt;&lt;/strong&gt;; &lt;strong&gt;(3)&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;wTWB&quot;&gt;Теперь почти всё готово для быстрого вычисления произвольных элементов последовательности Фибоначчи, на основе принципов, раскрытых при решении задачи о быстром умножении. Только перед этим следует обратить внимание на то, что в формулах &lt;em&gt;(2)&lt;/em&gt; и &lt;em&gt;(3)&lt;/em&gt; для вычисления нам требуется не только знание значения &lt;em&gt;f(k)&lt;/em&gt;, но и знание предшествующего ему значения &lt;em&gt;f(k-1)&lt;/em&gt;. Опираясь на формулу &lt;em&gt;(2)&lt;/em&gt;, мы легко можем вывести формулы для получения значения &lt;em&gt;f(a+b-1)&lt;/em&gt; и, затем, &lt;em&gt;f(2n-1)&lt;/em&gt;:&lt;/p&gt;
  &lt;p id=&quot;wlpA&quot;&gt;В формуле &lt;em&gt;(2)&lt;/em&gt; подставим вместо &lt;em&gt;a&lt;/em&gt; выражение &lt;em&gt;a - 1&lt;/em&gt;:&lt;br /&gt;&lt;em&gt;f((a-1)+b) = f(a-1)f(b-1) + (f((a-1)-1) + f(a-1))f(b);&lt;/em&gt;&lt;br /&gt;&lt;em&gt;f(a+b-1) = f(a-1)f(b-1) + (f(a-2) + f(a-1))f(b);&lt;/em&gt;&lt;/p&gt;
  &lt;p id=&quot;Kleh&quot;&gt;&lt;strong&gt;&lt;em&gt;f(a+b-1) = f(a)f(b) + f(a-1)f(b-1)&lt;/em&gt;&lt;/strong&gt;; &lt;strong&gt;(4)&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;9o8H&quot;&gt;Подставим в формулу &lt;em&gt;(4)&lt;/em&gt; значение &lt;em&gt;n&lt;/em&gt; вместо значений &lt;em&gt;a&lt;/em&gt; и &lt;em&gt;b&lt;/em&gt;:&lt;br /&gt;&lt;em&gt;f(n+n-1) = f(n)f(n) + f(n-1)f(n-1);&lt;/em&gt;&lt;/p&gt;
  &lt;p id=&quot;n4xO&quot;&gt;&lt;strong&gt;&lt;em&gt;f(2n-1) = f(n)^2 + f(n-1)^2&lt;/em&gt;&lt;/strong&gt;; &lt;strong&gt;(5)&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;sT95&quot;&gt;Теперь у нас есть всё, что необходимо для реализации быстрого вычисления чисел Фибоначчи.&lt;/p&gt;
  &lt;h3 id=&quot;1RUY&quot;&gt;Реализация быстрого вычисления. Scheme&lt;/h3&gt;
  &lt;p id=&quot;p5pz&quot;&gt;Поскольку в формулах, как было замечено выше, для произведения вычислений требуется не только значение &lt;em&gt;f(k)&lt;/em&gt;, но и предшествующее ему значение &lt;em&gt;f(k-1)&lt;/em&gt;, будет удобно представлять элемент последовательности сразу парой чисел - самим значением элемента, и предшествующим ему. Для представления пар элементов, в языке Scheme, имеется стандартная конструкция Pair, которая, в основном, используется для создания списков, но подойдёт и в нашем случае. Для создания пары предназначена функция &lt;code&gt;cons&lt;/code&gt;, а для доступа к полям пары - функции &lt;code&gt;car&lt;/code&gt; и &lt;code&gt;cdr&lt;/code&gt;. Само значение элемента последовательности будем хранить в первом поле, получая доступ при помощи функции &lt;code&gt;car&lt;/code&gt;, а предшествующее ему значение - во втором поле, для доступа используя функцию &lt;code&gt;cdr&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;EwPD&quot;&gt;Действуя описанным образом, функцию быстрого вычисления элементов последовательности Фибоначчи, мы можем определить на Scheme так:&lt;/p&gt;
  &lt;pre id=&quot;AVsM&quot; data-lang=&quot;java&quot;&gt;(define (fib n)
        (cond ((and (negative? n) (even? n)) (- (fib (abs n))))
              (else (qfib (abs n) (cons 1 0) (cons 0 1)))))

(define (qfib n term accum)
        (cond ((zero? n) (car accum))
              ((even? n) (qfib (halve n) (fib-double term) accum))
              (else (qfib (- n 1) term (fib-sum accum term)))))

(define (halve x)
        (quotient x 2))

(define (fib-double x)
        (cons (* (car x) (+ (* 2 (cdr x)) (car x)))
              (+ (sqr (car x)) (sqr (cdr x)))))

(define (fib-sum a b)
        (cons (+ (* (car a) (cdr b)) (* (+ (car a) (cdr a)) (car b)))
              (+ (* (car a) (car b)) (* (cdr a) (cdr b)))))

(define (sqr x)
        (* x x))&lt;/pre&gt;
  &lt;h3 id=&quot;KFVV&quot;&gt;Реализация быстрого вычисления. Java&lt;/h3&gt;
  &lt;p id=&quot;hyxm&quot;&gt;На Java, для повышения наглядности, вытесним реализацию элементов последовательности, сочетающих пару из текущего и предшествующего значений, а так же функций преобразования номера и быстрого вычисления в отдельный класс. Кроме того, воспользовавшись тем, что у нас есть быстрый доступ ко всем двоичным разрядам номера требуемого элемента, мы, для уменьшения количества вычислений, будем не удваивать номер слагаемого, проходя по битам целевого номера от младших к старшим, а наоборот - проходя от старших битов к младшим, будем удваивать номер аккумулятора, при необходимости увеличивая его на единицу. Для пояснения, вспомним пример из рассмотренной задачи о быстром умножении, где мы умножали значение &lt;em&gt;a&lt;/em&gt; на скаляр 140. Число 140 можно представить не только как сумму &lt;em&gt;(128 + 8 + 4)&lt;/em&gt;, но и в виде выражения &lt;em&gt;(((1*16 + 1)*2 + 1)*4)&lt;/em&gt;.&lt;/p&gt;
  &lt;p id=&quot;i7YE&quot;&gt;Вспомогательный класс реализующий быстрое вычисление элементов последовательности Фибоначчи:&lt;/p&gt;
  &lt;pre id=&quot;zNvC&quot; data-lang=&quot;java&quot;&gt;package fibonacci;

import java.math.BigInteger;

/**
 * Быстрое вычисление элементов последовательности Фибоначчи.
 * &amp;lt;p&amp;gt;Экземпляры класса являются immutable-объектами представляющими элементы последовательности Фибоначчи. Каждый из таких объектов предоставляет методы, необходимые для реализации быстрого вычисления других элементов последовательности.&amp;lt;/p&amp;gt;
 * &amp;lt;p&amp;gt;Кроме того, класс предоставляет статический фабричный метод, реализующий алгоритм быстрого вычисления элементов последовательности.&amp;lt;/p&amp;gt;
 */
public class QFib {

// static fields

	/**
	 * Нулевой элемент последовательности.
	 */
	public static final QFib ZERO = new QFib(BigInteger.ZERO, BigInteger.ONE, BigInteger.ZERO);

// instance fields

	/**
	 * Значение данного элемента последовательности.
	 */
	private final BigInteger cur;

	/**
	 * Значение предшествующего элемента последовательности.
	 */
	private final BigInteger prev;

	/**
	 * Номер данного элемента последовательности.
	 */
	private final BigInteger n;

// static methods

	/**
	 * Получение элемента последовательности с заданным номером.
	 * &amp;lt;p&amp;gt;Метод, реализуя алгоритм быстрого вычисления элементов последовательности Фибоначчи, находит и возвращает элемент с заданным номером.&amp;lt;/p&amp;gt;
	 * @param n Номер элемента в последовательности.
	 * @return Элемент последовательности с заданным номером.
	 * @throws NullPointerException Если указанный номер элемента не существует.
	 */
	public static QFib of (
		final BigInteger n
	) throws NullPointerException
	{ // method body
		QFib accum = ZERO;
		final BigInteger absN = n.abs();
		for (int i = absN.bitLength() - 1; i &amp;gt;= 0; i--) {
			accum = accum.doubleN();
			if (absN.testBit(i)) {
				accum = accum.next();
			} // if
		} // for
		if (n.signum() &amp;lt; 0) {
			accum = accum.negateN();
		} // if
		return accum;
	} // of()

// constructors

	/**
	 * Конструктор элемента последовательности.
	 * @param cur Значение данного элемента последовательности.
	 * @param prev Значение предшествующего элемента последовательности.
	 * @param n Номер данного элемента последовательности.
	 * @throws AssertionError Если разрешены операторы контроля, и любой из аргументов не существует.
	 */
	private QFib (
		final BigInteger cur,
		final BigInteger prev,
		final BigInteger n
	) throws AssertionError
	{ // method body
		assert cur != null;
		assert prev != null;
		assert n != null;
		this.cur = cur;
		this.prev = prev;
		this.n = n;
	} // QFib()

// instance methods

	/**
	 * Значение элемента.
	 * &amp;lt;p&amp;gt;Метод возвращает значение данного элемента последовательности.&amp;lt;/p&amp;gt;
	 * @return Значение данного элемента.
	 */
	public BigInteger value (
	) { // method body
		return cur;
	} // value()

	/**
	 * Следующий элемент последовательности.
	 * @return Следующий элемент последовательности.
	 */
	public QFib next (
	) { // method body
		final BigInteger nextCur = cur.add(prev);
		final BigInteger nextPrev = cur;
		final BigInteger nextN = n.add(BigInteger.ONE);
		return new QFib(nextCur, nextPrev, nextN);
	} // next()

	/**
	 * Удвоение номера элемента.
	 * &amp;lt;p&amp;gt;Метод возвращает элемент последовательности номер которого равен удвоенному номеру данного.&amp;lt;/p&amp;gt;
	 * @return Элемент с номером равным удвоенному номеру данного.
	 */
	public QFib doubleN (
	) { // method body
		final BigInteger doubleCur = cur.parallelMultiply(prev.add(prev).add(cur));
		final BigInteger doublePrev = cur.parallelMultiply(cur).add(prev.parallelMultiply(prev));
		final BigInteger doubleN = n.add(n);
		return new QFib(doubleCur, doublePrev, doubleN);
	} // doubleN()

	/**
	 * Смена знака номера.
	 * &amp;lt;p&amp;gt;Метод возвращает элемент последовательности с номером равным по абсолютному значению, но противоположным по знаку номеру данного элемента.&amp;lt;/p&amp;gt;
	 * @return Элемент последовательности с номером противоположным по знаку.
	 */
	public QFib negateN (
	) { // method body
		BigInteger negCur = cur;
		BigInteger negPrev = cur.add(prev);
		if (n.testBit(0)) {
			negPrev = negPrev.negate();
		} else {
			negCur = negCur.negate();
		} // if
		final BigInteger negN = n.negate();
		return new QFib(negCur, negPrev, negN);
	} // negateN()

} // QFib&lt;/pre&gt;
  &lt;p id=&quot;bb5r&quot;&gt;Теперь, в основном классе &lt;code&gt;Fibonacci&lt;/code&gt;, реализуем метод &lt;code&gt;qfib()&lt;/code&gt;, использующий написанный нами вспомогательный класс для быстрого вычисления значений элементов последовательности Фибоначчи:&lt;/p&gt;
  &lt;pre id=&quot;wW5a&quot; data-lang=&quot;java&quot;&gt;/**
 * Быстрое нахождение значения произвольного элемента.
 * &amp;lt;p&amp;gt;Метод, обращаясь к вспомогательному классу, находит за логарифмическое время и возвращает значение произвольного элемента последовательности Фибоначчи.&amp;lt;/p&amp;gt;
 * @param n Номер элемента в последовательности.
 * @return Значение указанного элемента последовательности.
 */
private static BigInteger qfib (
	final int n
) { // method body
	return QFib.of(BigInteger.valueOf(n)).value();
} // qfib()&lt;/pre&gt;
  &lt;p id=&quot;XMIC&quot;&gt;Теперь мы достигли своей цели: с быстрым подходом вычисления стали действительно быстрыми - поиск 10000000-го числа последовательности занимает менее секунды! Так происходит потому, что вычислительная сложность быстрого алгоритма является логарифмической - в самом деле, на каждом шаге в методе &lt;code&gt;of()&lt;/code&gt; класса &lt;code&gt;QFib&lt;/code&gt; рассматривается один бит номера выбранного элемента. Таким образом, число итераций, совершаемых алгоритмом для нахождения выбранного элемента последовательности, в точности соответствует числу разрядов в двоичном представлении номера выбранного элемента, что в терминах выражения количества вычислений означает, что вычислительная сложность быстрого алгоритма равна &lt;em&gt;O(log(n))&lt;/em&gt;. Печать найденного 10000000-го числа Фибоначчи на экране занимает в четыре с половиной раза больше времени, чем его вычисление - вывод таких больших чисел требует немало времени, ведь количество десятичных цифр в выводимом значении лишь на 30% меньше, чем общее число символов в четырёхтомнике &amp;quot;Война и мир&amp;quot;.&lt;/p&gt;
  &lt;p id=&quot;IdWN&quot;&gt;С этим алгоритмом, для нас становятся вполне достижимы элементы последовательности Фибоначчи с совсем уже &amp;quot;неприличными&amp;quot; номерами: например, вычисление 1000000000-го числа Фибоначчи, на моей машине, занимает чуть меньше шести минут, а вот формирование и вывод его десятичного представления - без нескольких секунд, 50 минут. Последнее не удивительно, так как десятичное представление этого числа состоит из 208987640 знаков.&lt;/p&gt;
  &lt;h3 id=&quot;pKC2&quot;&gt;Заключение&lt;/h3&gt;
  &lt;p id=&quot;i9z5&quot;&gt;Использованный алгоритм по сути манипулирует номерами элементов, а не их значениями. Этот алгоритм с равным успехом может быть использован как для быстрого умножения и быстрого получения чисел Фибоначчи, так и для нахождения элементов любой подходящей последовательности за логарифмическое время. Единственным условием, для такой последовательности, является определение функции нахождения элемента от суммы номеров двух других элементов &lt;em&gt;seq(n+m)&lt;/em&gt;, либо двух функций: удвоения номера элемента &lt;em&gt;seq(2n)&lt;/em&gt;, и увеличения номера элемента на единицу &lt;em&gt;seq(n+1)&lt;/em&gt;.&lt;/p&gt;
  &lt;p id=&quot;8wGh&quot;&gt;В заключение хотелось бы сказать несколько слов о целесообразности применения рассмотренных нами алгоритмов к получению значений последовательности Фибоначчи. Если требуется вычислить произвольный элемент последовательности, то тут, вне всяких сомнений, пальму первенства удерживает быстрый алгоритм, так как он требует только &lt;em&gt;O(log(n))&lt;/em&gt; вычислений. Однако, если нам необходимо однократно вывести непрерывную подпоследовательность из &lt;em&gt;m&lt;/em&gt; элементов, то применение быстрого алгоритма не оправдано, поскольку для этого потребуется &lt;em&gt;O(m*log(n))&lt;/em&gt; операций, в то время как простой итеративный алгоритм, если уже известны два начальных элемента подпоследовательности, потребует только &lt;em&gt;O(m)&lt;/em&gt; операций. Впрочем, если начальные элементы выводимой подпоследовательности нам ещё не известны, то получить их поможет как раз быстрый алгоритм, а вот дальше за дело возьмётся итеративный. Наконец, вспомним про незаслуженно обделённый нашим вниманием алгоритм на основе методов динамического программирования, заполняющий линейный массив однажды вычисленных элементов, позволяющий заполнить этот массив за линейное время, как с помощью рекурсивной реализации использующей &amp;quot;ленивую динамику&amp;quot;, так и с помощью итеративного подхода. Этот алгоритм имеет важное преимущество перед другими, получаемое в случае множественных последующих обращений к уже вычисленным элементам последовательности: поскольку результаты вычислений кешируются, то, при повторных обращениях, стоимость получения значений вычисленных элементов является константной. То есть, алгоритм на основе динамического программирования требует &lt;em&gt;O(n)&lt;/em&gt; памяти для хранения вычисленных элементов, совершает &lt;em&gt;O(n)&lt;/em&gt; операций при заполнении кеша, но в дальнейшем, стоимость обращения к вычисленным элементам будет составлять &lt;em&gt;O(1)&lt;/em&gt;.&lt;/p&gt;
  &lt;p id=&quot;0hSW&quot;&gt;&lt;a href=&quot;https://habr.com/ru/articles/812611/&quot; target=&quot;_blank&quot;&gt;Источник&lt;/a&gt;&lt;/p&gt;

</content></entry><entry><id>javalib:7byEUwaWv56</id><link rel="alternate" type="text/html" href="https://teletype.in/@javalib/7byEUwaWv56?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=javalib"></link><title>Context receivers — новые extension functions</title><published>2024-03-27T06:01:17.022Z</published><updated>2024-03-27T06:01:17.022Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img4.teletype.in/files/76/a4/76a48afe-a6e8-4ee6-92d0-e76ebe1cdf03.png"></media:thumbnail><category term="kotlin" label="Kotlin"></category><summary type="html">&lt;img src=&quot;https://img3.teletype.in/files/20/40/2040d8de-4d14-48e7-b18d-cf882d56a20e.png&quot;&gt;Думаю, не раскрою большой секрет, что Ozon разработал энное количество мобильных приложений: для покупателей, для продавцов, банк и т. д. В каждом из них требуется авторизация. Для этого существует наша команда Ozon ID с SDK собственного производства. Частью команды Ozon ID являюсь я — Android-разработчик с непомерной любовью к синтаксическому сахару Kotlin.</summary><content type="html">
  &lt;figure id=&quot;48qf&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/20/40/2040d8de-4d14-48e7-b18d-cf882d56a20e.png&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;Frpc&quot;&gt;Думаю, не раскрою большой секрет, что Ozon разработал энное количество мобильных приложений: для покупателей, для продавцов, банк и т. д. В каждом из них требуется авторизация. Для этого существует наша команда Ozon ID с SDK собственного производства. Частью команды Ozon ID являюсь я — Android-разработчик с непомерной любовью к синтаксическому сахару Kotlin.&lt;/p&gt;
  &lt;h2 id=&quot;Zt4X&quot;&gt;Введение&lt;/h2&gt;
  &lt;p id=&quot;QORQ&quot;&gt;Поговорим сегодня про context receivers — фиче Kotlin, про которую я узнал давно, но смог найти применение лишь пару месяцев назад. Расскажу о том, что такое context receivers, где их можно использовать, и, конечно же, про «успешный успех» — минус 60% самописного DI в Ozon ID SDK. Но обо всём по порядку.&lt;/p&gt;
  &lt;h2 id=&quot;wCqs&quot;&gt;Функции расширения&lt;/h2&gt;
  &lt;p id=&quot;yvB8&quot;&gt;В коде Ozon ID SDK есть следующее расширение для &lt;code&gt;ComponentActvity&lt;/code&gt;.&lt;/p&gt;
  &lt;pre id=&quot;og2R&quot; data-lang=&quot;kotlin&quot;&gt;inline fun &amp;lt;T&amp;gt; ComponentActvity.collectWhenStarted(
    data: Flow&amp;lt;T&amp;gt;,
    crossinline collector: (T) -&amp;gt; Unit
) {
    lifecycleScope.launchWhenStarted { 
        data.collect {
            collector.invoke(it)
        } 
    }
}&lt;/pre&gt;
  &lt;p id=&quot;k7wm&quot;&gt;&lt;strong&gt;Ремарка&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;GkV8&quot;&gt;Я знаю, что существует &lt;a href=&quot;https://medium.rip/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda&quot; target=&quot;_blank&quot;&gt;более надёжный способ collect’ить Flow на ui-потоке&lt;/a&gt;. Например, с помощью &lt;code&gt;flowWithLifecycle&lt;/code&gt;. Но в нём больше параметров, что будет только отвлекать от основной темы.&lt;/p&gt;
  &lt;p id=&quot;I8vy&quot;&gt;Реализовано расширение &lt;code&gt;collectWhenStarted&lt;/code&gt; исключительно для удобства сбора данных с множества &lt;code&gt;Flow&lt;/code&gt; из &lt;code&gt;ViewModel&lt;/code&gt; внутри &lt;code&gt;Activity&lt;/code&gt;. Ниже пример использования этого расширения.&lt;/p&gt;
  &lt;pre id=&quot;z0rk&quot; data-lang=&quot;kotlin&quot;&gt;class AuthFlowActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        collectWhenStarted(viewModel.backStack) { onBackStackChanged(it) }
        collectWhenStarted(viewModel.navigationEvents) { onNavigationEvent(it) }
        ...
    }
}&lt;/pre&gt;
  &lt;p id=&quot;wooG&quot;&gt;Удобно? В общем-то да. Вызов бесспорно короче, чем без использования расширения. Идеально? Отнюдь. Лично мне хотелось бы видеть вызов &lt;code&gt;collectWhenStarted&lt;/code&gt; примерно следующим образом:&lt;/p&gt;
  &lt;pre id=&quot;cj46&quot; data-lang=&quot;kotlin&quot;&gt;class AuthFlowActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        viewModel.backStack.collectWhenStarted { onBackStackChanged(it) }
        viewModel.navigationEvents.collectWhenStarted { onNavigationEvent(it) }
        ...
    }
}&lt;/pre&gt;
  &lt;p id=&quot;YAQ2&quot;&gt;Вызвать &lt;code&gt;collect*&lt;/code&gt; у &lt;code&gt;Flow&lt;/code&gt; более интуитивно, чем &lt;code&gt;collect*&lt;/code&gt; у &lt;code&gt;Activity&lt;/code&gt;, не правда ли?&lt;/p&gt;
  &lt;p id=&quot;Ljwo&quot;&gt;Для того чтобы реализовать улучшенный вариант расширения &lt;code&gt;collectWhenStarted&lt;/code&gt;, технически нам понадобится, чтобы механизм методов-расширений мог принимать 2 receiver-параметра. К сожалению, JetBrains пока не реализовали такую возможность в языке Kotlin... Или реализовали?&lt;/p&gt;
  &lt;h3 id=&quot;xOCL&quot;&gt;Реализация через scope&lt;/h3&gt;
  &lt;p id=&quot;gIls&quot;&gt;На самом деле есть такой подход, как scope. При таком подходе метод-расширение реализуется в новом классе. В случае с &lt;code&gt;Activity&lt;/code&gt; решение на scope можно применить следующим образом.&lt;/p&gt;
  &lt;pre id=&quot;hAIH&quot; data-lang=&quot;kotlin&quot;&gt;interface LifecycleOwnerScope : LifecycleOwner {
    fun &amp;lt;T&amp;gt; Flow&amp;lt;T&amp;gt;.collectWhenStarted(collector: (T) -&amp;gt; Unit) {
        lifecycleScope.launchWhenStarted { collect { collector.invoke(it) } }
    }
}&lt;/pre&gt;
  &lt;p id=&quot;XXj4&quot;&gt;Имплементируем в &lt;code&gt;Activity&lt;/code&gt; интерфейс &lt;code&gt;LifecycleOwnerScope&lt;/code&gt;, и готово — у &lt;code&gt;Flow&lt;/code&gt; появляется расширение &lt;code&gt;collectWhenStarted&lt;/code&gt;. При этом реализация интерфейса &lt;code&gt;LifecycleOwnerScope&lt;/code&gt; внутри &lt;code&gt;Activity&lt;/code&gt; не требуется за счёт «реализации по умолчанию».&lt;/p&gt;
  &lt;pre id=&quot;ML9G&quot; data-lang=&quot;kotlin&quot;&gt;class AuthFlowActivity : AppCompatActivity(), LifecycleOwnerScope {
                                            // ^ Добавили scope ^
    override fun onCreate(savedInstanceState: Bundle?) {
        ...             // collectWhenStarted берётся из LifecycleOwnerScope
        viewModel.backStack.collectWhenStarted { onBackStackChanged(it) }
        viewModel.navigationEvents.collectWhenStarted { onNavigationEvent(it) }
        ...
    }
}&lt;/pre&gt;
  &lt;p id=&quot;vrgF&quot;&gt;Подход вполне жизнеспособный. Но, как по мне, не без недостатков. Например, чтобы расширение &lt;code&gt;collectWhenStarted&lt;/code&gt; заработало нужно собственноручно отнаследоваться от &lt;code&gt;LifecycleOwnerScope&lt;/code&gt;. Это не так удобно, как глобальные расширения, которые IDE услужливо сама подсказывает при вводе имени.&lt;/p&gt;
  &lt;p id=&quot;PvpK&quot;&gt;Давайте уже перейдём к context receivers.&lt;/p&gt;
  &lt;h2 id=&quot;sYIi&quot;&gt;Context receivers спешат на помощь&lt;/h2&gt;
  &lt;p id=&quot;6MH2&quot;&gt;Context receivers — это концепт, фича языка Kotlin. Context receivers добавлены в язык как инструмент, позволяющий преодолеть ограничения extension-функций. Технически context receivers, как и extension-функции, компилируются в статический метод с дополнительным &lt;code&gt;this&lt;/code&gt;-параметром. То есть context receivers являются очередным синтаксическим сахаром Kotlin, но, конечно же, круче и «слаще», чем extension.&lt;/p&gt;
  &lt;p id=&quot;LYtm&quot;&gt;Context receivers появились аж в Kotlin 1.6.20. Вместе с context receivers в язык добавили новое ключевое слово &lt;code&gt;context&lt;/code&gt;. Фича в версии Kotlin 1.9.22 всё ещё экспериментальная, поэтому если попытаться сходу ей воспользоваться, то IDE выдаст следующее сообщение.&lt;/p&gt;
  &lt;blockquote id=&quot;dHk7&quot;&gt;The feature &amp;quot;context receivers&amp;quot; is experimental and should be enabled explicitly&lt;/blockquote&gt;
  &lt;p id=&quot;f3aI&quot;&gt;Для того чтобы context receivers заработали, нужно дополнить файл &lt;code&gt;build.gradle&lt;/code&gt; модуля, в котором будет использован &lt;code&gt;context&lt;/code&gt;.&lt;/p&gt;
  &lt;pre id=&quot;H5v0&quot; data-lang=&quot;kotlin&quot;&gt;tasks.withType&amp;lt;org.jetbrains.kotlin.gradle.tasks.KotlinCompile&amp;gt;().configureEach {
    kotlinOptions {
        freeCompilerArgs = freeCompilerArgs + &amp;quot;-Xcontext-receivers&amp;quot;
    }
}&lt;/pre&gt;
  &lt;p id=&quot;EDOk&quot;&gt;Теперь можно использовать &lt;code&gt;context&lt;/code&gt; в коде. Приступим. Будем модифицировать расширение поэтапно.&lt;/p&gt;
  &lt;p id=&quot;1w5F&quot;&gt;&lt;strong&gt;Шаг 1:&lt;/strong&gt; Перенести receiver-параметр &lt;code&gt;ComponentActvity&lt;/code&gt; в аргументы &lt;code&gt;context&lt;/code&gt;.&lt;/p&gt;
  &lt;pre id=&quot;8uvY&quot; data-lang=&quot;kotlin&quot;&gt;context (ComponentActvity)
inline fun &amp;lt;T&amp;gt; /*ComponentActvity.*/collectWhenStarted(
    data: Flow&amp;lt;T&amp;gt;,
    crossinline collector: (T) -&amp;gt; Unit
) {
    lifecycleScope.launchWhenStarted { 
        data.collect {
            collector.invoke(it)
        } 
    }
}&lt;/pre&gt;
  &lt;p id=&quot;Djuo&quot;&gt;Байт-код&lt;/p&gt;
  &lt;p id=&quot;tESo&quot;&gt;Байт-код скомпилированной функции с context receivers получается точно такой же, как и его прямой аналог, реализованный через extension. К сожалению, декомпилировать байт-код с context receivers у меня не вышло. Может, кто-нибудь из читателей подробнее расследует данный вопрос и приведёт свои примеры в комментариях.&lt;/p&gt;
  &lt;p id=&quot;42AS&quot;&gt;Ниже приведена сигнатура декомпилированной из байт-кода extension-функции. Напомню, что байт-код идентичен аналогу на context receivers.&lt;/p&gt;
  &lt;pre id=&quot;3xyZ&quot; data-lang=&quot;kotlin&quot;&gt;public static final void collectWhenStarted(
    @NotNull ComponentActivity $this$collectWhenStarted, 
    @NotNull final Flow data, 
    @NotNull final Function1 collector
) &lt;/pre&gt;
  &lt;p id=&quot;MLfZ&quot;&gt;Код скомпилируется и выполнится, как и на прежней реализации через расширение. Но есть один нюанс, на котором стоит остановиться. Дело в том, что context receivers — это не extension (ваш капитан Очевидность). Функции с context receivers нельзя вызвать, как будто это метод класса. Пример в сниппете ниже.&lt;/p&gt;
  &lt;pre id=&quot;bEw7&quot; data-lang=&quot;kotlin&quot;&gt;// Реализация через extension
activity.collectWhenStarted() // компилятор позволяет вызвать функцию-расширение &amp;#x60;collectWhenStarted&amp;#x60;, как будто это метод класса&lt;/pre&gt;
  &lt;pre id=&quot;WtzP&quot; data-lang=&quot;kotlin&quot;&gt;// Реализация через context receivers
activity.collectWhenStarted() // Ошибка: Unresolved reference: collectWhenStarted
with(activity) { // apply, run тоже подойдут
    collectWhenStarted() // Функцию с context можно вызвать только внутри &amp;quot;контекста&amp;quot; 
}&lt;/pre&gt;
  &lt;p id=&quot;NUT0&quot;&gt;&lt;strong&gt;Шаг 2:&lt;/strong&gt; Перенести аргумент &lt;code&gt;data: Flow&lt;/code&gt; в receiver функции&lt;/p&gt;
  &lt;pre id=&quot;1OPF&quot; data-lang=&quot;kotlin&quot;&gt;context (ComponentActvity)
inline fun &amp;lt;T&amp;gt; Flow&amp;lt;T&amp;gt;.collectWhenStarted(
    /*data: Flow&amp;lt;T&amp;gt;,*/
    crossinline collector: (T) -&amp;gt; Unit
) {
    lifecycleScope.launchWhenStarted { 
        /*data.*/collect {
            collector.invoke(it)
        } 
    }
}&lt;/pre&gt;
  &lt;p id=&quot;E3YH&quot;&gt;Вуаля! Готово. Теперь у &lt;code&gt;Flow&lt;/code&gt; внутри (иначе говоря, «в контексте») &lt;code&gt;ComponentActvity&lt;/code&gt; появится метод &lt;code&gt;collectWhenStarted&lt;/code&gt;.&lt;/p&gt;
  &lt;pre id=&quot;72Cm&quot; data-lang=&quot;kotlin&quot;&gt;class AuthFlowActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...            // powered by context receivers
        viewModel.backStack.collectWhenStarted { onBackStackChanged(it) }
        viewModel.navigationEvents.collectWhenStarted { onNavigationEvent(it) }
        ...
    }
}&lt;/pre&gt;
  &lt;h2 id=&quot;vOx2&quot;&gt;We need to go deeper&lt;/h2&gt;
  &lt;p id=&quot;wTIh&quot;&gt;Приоткрою ещё немного информации относительно ключевого слова &lt;code&gt;context&lt;/code&gt;:&lt;/p&gt;
  &lt;ol id=&quot;e4zd&quot;&gt;
    &lt;li id=&quot;Bk3T&quot;&gt;&lt;code&gt;context&lt;/code&gt; может принимать более одного аргумента. Например, &lt;code&gt;context (classA, classB)&lt;/code&gt;.&lt;/li&gt;
    &lt;li id=&quot;IuIV&quot;&gt;&lt;code&gt;context&lt;/code&gt; можно прописывать над классами.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;blockquote id=&quot;cxoL&quot;&gt;Прямо как аннотация &lt;code&gt;@Component(dependencies = [...])&lt;/code&gt; в Dagger, не правда ли?&lt;/blockquote&gt;
  &lt;p id=&quot;61Vn&quot;&gt;Начну разбор этих пунктов с последнего — &lt;code&gt;context&lt;/code&gt; над классом. В качестве основы для примеров возьму DI из Ozon ID SDK.&lt;/p&gt;
  &lt;pre id=&quot;lS34&quot; data-lang=&quot;kotlin&quot;&gt;internal class RootModule(
    val application: Application
)

context(RootModule)
internal class ChildModule {

    val dependency by lazy {
        Dependency(
            /*this@RootModule.*/application,
            ...
        )
    }
}&lt;/pre&gt;
  &lt;p id=&quot;RWva&quot;&gt;Как видно из примера выше, внутри &lt;code&gt;ChildModule&lt;/code&gt; можно обратиться к &lt;code&gt;application&lt;/code&gt;, как-будто это свойство &lt;code&gt;ChildModule&lt;/code&gt;. При этом к свойству можно обратиться и через &lt;code&gt;this&lt;/code&gt; —  &lt;code&gt;this@RootModule.application&lt;/code&gt;. Пригодится на случай конфликта имён context receivers или ради внесения ясности в вопрос «откуда взялась эта зависимость?».&lt;/p&gt;
  &lt;p id=&quot;LPmR&quot;&gt;Продолжим погружение. Рассмотрим, как создавать зависимости с context receivers.&lt;/p&gt;
  &lt;pre id=&quot;IqaE&quot; data-lang=&quot;kotlin&quot;&gt;context(RootModule)
internal class SubChildModule

context(RootModule)
internal class ChildModule {
    ...

    val subChildModule by lazy {
        // with не нужен, уже в нужном контексте
        SubChildModule()
    }
}

val childModule = with(rootModule) {
    // Нужен with для создания
    ChildModule()
}&lt;/pre&gt;
  &lt;p id=&quot;3aHU&quot;&gt;Для того чтобы создать объект класса с context receivers, нужно, чтобы вызов конструктора происходил в требуемом контексте. Контекст можно задать следующим образом:&lt;/p&gt;
  &lt;ol id=&quot;b7mK&quot;&gt;
    &lt;li id=&quot;6FvV&quot;&gt;создать экземпляр внутри класса, требуемого в context receiver;&lt;/li&gt;
    &lt;li id=&quot;pBg9&quot;&gt;создать экземпляр внутри независимого класса, в context receivers которого есть нужный класс. Например, как создан модуль &lt;code&gt;SubChildModule&lt;/code&gt; внутри &lt;code&gt;ChildModule&lt;/code&gt;;&lt;/li&gt;
    &lt;li id=&quot;NWex&quot;&gt;создать внутри scope-функции c лямбдой-расширением в качестве аргумента (with, apply, run). Подойдут и собственные функции с аналогичным параметром.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;oDmO&quot;&gt;Теперь рассмотрим создание объектов с несколькими context receivers.&lt;/p&gt;
  &lt;pre id=&quot;vqnz&quot; data-lang=&quot;kotlin&quot;&gt;context(RootModule, RepositoryModule, NetworkModule, CookieModule)
internal class MultiContextModule

val multiContextModule = with(rootModule) {
    with(repositoryModule) {
        with(networkModule) {
            with(cookieModule) {
                MultiContextModule()
            }
        }
    }
}&lt;/pre&gt;
  &lt;p id=&quot;U9U3&quot;&gt;Как можно заметить, код создания экземпляра класса с несколькими context receivers может оказаться не столь изящным, как хотелось бы. Но это поправимо, потому что &lt;code&gt;context&lt;/code&gt; можно прописывать к лямбда-аргументам. Возьмём за основу код &lt;code&gt;with&lt;/code&gt; из стандартной библиотеки Kotlin и обогатим его &lt;code&gt;context&lt;/code&gt;.&lt;/p&gt;
  &lt;pre id=&quot;Nrvf&quot; data-lang=&quot;kotlin&quot;&gt;// Пример из stdlib
@kotlin.internal.InlineOnly
public inline fun &amp;lt;T, R&amp;gt; with(receiver: T, block: T.() -&amp;gt; R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}&lt;/pre&gt;
  &lt;pre id=&quot;WNja&quot; data-lang=&quot;kotlin&quot;&gt;// with context receivers
@OptIn(ExperimentalContracts::class)
@Suppress(&amp;quot;SUBTYPING_BETWEEN_CONTEXT_RECEIVERS&amp;quot;)
internal inline fun &amp;lt;T1, T2, R&amp;gt; with(c1: T1, c2: T2, block: context(T1, T2) () -&amp;gt; R): R {
    contract {                                           // ^^^^^^^
        callsInPlace(block, InvocationKind.EXACTLY_ONCE) // см. сюда
    }
    return block(c1, c2)
}&lt;/pre&gt;
  &lt;p id=&quot;iZ2Z&quot;&gt;В примере выше к типу лямбды &lt;code&gt;block&lt;/code&gt; добавлено ключевое слово &lt;code&gt;context&lt;/code&gt;. Параметры &lt;code&gt;context&lt;/code&gt; — generic-типы. Подобных &lt;code&gt;with&lt;/code&gt; можно написать больше под нужное количество context receivers. В свою очередь, это позволяет нам значительно уменьшить сдвиг кода вправо при создании &lt;code&gt;MultiContextModule&lt;/code&gt;.&lt;/p&gt;
  &lt;pre id=&quot;ZvMw&quot; data-lang=&quot;kotlin&quot;&gt;context(RootModule, RepositoryModule, NetworkModule, CookieModule)
internal class MultiContextModule

val multiContextModule = with(
    rootModule,
    repositoryModule,
    networkModule,
    cookieModule
) {
    MultiContextModule()
}&lt;/pre&gt;
  &lt;h2 id=&quot;K2tk&quot;&gt;Итоги&lt;/h2&gt;
  &lt;p id=&quot;icib&quot;&gt;Подведём черту под тем, что мы сегодня узнали. В этом нам поможет файл &lt;a href=&quot;https://github.com/Kotlin/KEEP/blob/master/proposals/context-receivers.md&quot; target=&quot;_blank&quot;&gt;KEEP&lt;/a&gt; по context receivers.&lt;/p&gt;
  &lt;ul id=&quot;1pos&quot;&gt;
    &lt;li id=&quot;aYD9&quot;&gt;Context receivers — это механизм, призванный расширить возможности extension-функций. Причём, как мы видели на примере с &lt;code&gt;Flow&lt;/code&gt;, именно расширить, а не заменить.&lt;/li&gt;
    &lt;li id=&quot;0gdp&quot;&gt;Context receivers можно прописывать как над функциями и свойствами, так и над классами.&lt;/li&gt;
    &lt;li id=&quot;ID3l&quot;&gt;Context receivers позволяют использовать более одного receiver-аргумента.&lt;/li&gt;
    &lt;li id=&quot;njYN&quot;&gt;Context receivers непонятно где и когда применять. Как минимум у меня пока не сложилось чёткого мнения, чтобы я мог однозначно сказать «вот тут extension, тут передать аргументом, а тут обязательно context». В видео в конце статьи можете найти размышления на эту тему. Я же пока в данном вопросе буду придерживаться исключительно технического момента.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;h2 id=&quot;ZiPl&quot;&gt;Заключение&lt;/h2&gt;
  &lt;p id=&quot;SmzV&quot;&gt;Про context receivers я узнал почти 2 года назад из &lt;a href=&quot;https://www.youtube.com/watch?v=GISPalIVdQY&quot; target=&quot;_blank&quot;&gt;видео от Jetbrains&lt;/a&gt;, не смог придумать им никакого полезного применения и отложил на дальнюю полку знаний про Kotlin. Однако пару месяцев назад мне довелось посмотреть &lt;a href=&quot;https://www.droidcon.com/2023/10/06/getting-ready-for-kotlin-context-receivers/&quot; target=&quot;_blank&quot;&gt;доклад с Droidcon&lt;/a&gt;, который помог открыть глаза на всю мощь данного механизма. И тут понеслось: рефакторинг DI в Ozon ID SDK, доклад внутри мобильной команды и в результате эта статья. Надеюсь, что у меня получилось донести до читателей мощь context receivers и подтолкнуть их на дальнейший поиск применимости этой фичи.&lt;/p&gt;
  &lt;p id=&quot;JMlW&quot;&gt;&lt;a href=&quot;https://habr.com/ru/companies/ozontech/articles/802641/&quot; target=&quot;_blank&quot;&gt;Источник&lt;/a&gt;&lt;/p&gt;

</content></entry><entry><id>javalib:fuozzaRft-j</id><link rel="alternate" type="text/html" href="https://teletype.in/@javalib/fuozzaRft-j?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=javalib"></link><title>Реализации Microkernel архитектуры с помощью Java OSGI</title><published>2024-03-21T12:42:44.013Z</published><updated>2024-03-21T12:42:44.013Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img2.teletype.in/files/1b/fb/1bfbafa0-5b2b-48e2-a599-2b1a2dc7e746.png"></media:thumbnail><category term="java" label="Java"></category><summary type="html">&lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/8b5/3ff/ed4/8b53ffed49232a3047580f6face8f8af.png&quot;&gt;Я хотел бы поделиться опытом реализации микроядерной архитектуры (microkernel) на Java с помощью OSGI (Open Service Gateway Initiative). Этот подход является промежуточным вариантом между микро-сервисной и монолитной архитектурой. С одной стороны присутствует разделение между компонентами на уровне VM с другой - межкомпонентное взаимодействие происходит без участия сети, что ускоряет запросы.</summary><content type="html">
  &lt;p id=&quot;lh69&quot;&gt;Я хотел бы поделиться опытом реализации микроядерной архитектуры (microkernel) на Java с помощью OSGI (Open Service Gateway Initiative). Этот подход является промежуточным вариантом между микро-сервисной и монолитной архитектурой. С одной стороны присутствует разделение между компонентами на уровне VM с другой - межкомпонентное взаимодействие происходит без участия сети, что ускоряет запросы.&lt;/p&gt;
  &lt;h2 id=&quot;aG9s&quot;&gt;Введение&lt;/h2&gt;
  &lt;figure id=&quot;HaF1&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/8b5/3ff/ed4/8b53ffed49232a3047580f6face8f8af.png&quot; width=&quot;718&quot; /&gt;
    &lt;figcaption&gt;Источник: Изображение &lt;a href=&quot;https://www.oreilly.com/content/software-architecture-patterns/&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;o&amp;#x27;reilly&lt;/u&gt;&lt;/a&gt;&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;VE1m&quot;&gt;Микроядерная архитектура подразумевает разделение функционала приложения на множество плагинов, каждый из которых гарантирует расширяемость, обеспечивает изоляцию и разделение функционала. Предполагается разделение компонентов на два типа: ядро и плагины. Ядро содержит минимальную функциональность, необходимую для работы системы, а логика приложения разделена между плагинами. При этом ожидается, что взаимодействие между плагинами будет сведено к минимуму, это позволит улучшить изоляцию каждого компонента, что улучшит тестируемость и упростит сопровождение.&lt;/p&gt;
  &lt;p id=&quot;WthT&quot;&gt;При этом ядру системы требуется информация о запущенных модулях и способах взаимодействия с ними. Наиболее распространенный подход для решения этой задачи лежит через организацию реестра плагинов, включающий в себя информацию о названии плагина и доступных интерфейсах.&lt;/p&gt;
  &lt;p id=&quot;sWg1&quot;&gt;Данный паттерн может быть реализован с использованием совершенно разных технологий. Например, мы можем выделить ядро и подключать плагины через динамическую загрузку jar файлов без дополнительной изоляции.&lt;/p&gt;
  &lt;p id=&quot;A4a4&quot;&gt;OSGI предлагает подход к изоляции плагинов с помощью разделения кода для каждого плагина на уровне загрузчика классов. Каждый плагин может загружаться отдельным загрузчиком, тем самым обеспечивая дополнительную изоляцию. Недостаток такого решения в потенциальных конфликтах классов: одинаковые классы, загруженные с помощью разных загрузчиков, не могут взаимодействовать.&lt;/p&gt;
  &lt;p id=&quot;o9XB&quot;&gt;В качестве высокоуровневого решения можно рассмотреть Apache Karaf, который позиционирует себя как Modulith Runtime и предоставляет интеграцию с основными фреймворками: JAX-RS и Spring Boot. Данный инструмент упрощает взаимодействие с OSGI технологией, предоставляя высокоуровневые абстракции.&lt;/p&gt;
  &lt;figure id=&quot;Qe1R&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/341/5e3/04e/3415e304ed0d01c1ec675575af6261cd.png&quot; width=&quot;481&quot; /&gt;
    &lt;figcaption&gt;Источник: &lt;a href=&quot;https://karaf.apache.org/&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;Apache Keraf&lt;/u&gt;&lt;/a&gt;&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;I1kY&quot;&gt;В качестве альтернативных вариантов можно рассмотреть непосредственные реализации OSGI: Apache Felix, Eclipse Equinox и Knopflerfish. Использование низкоуровневых решений даст нам большую свободу в процессе проектирования.&lt;/p&gt;
  &lt;h2 id=&quot;mdSD&quot;&gt;Плагинизированная архитектура на базе Apache Felix&lt;/h2&gt;
  &lt;p id=&quot;hguE&quot;&gt;&lt;strong&gt;Контекст&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;Zdmn&quot;&gt;Для интеграции с различными источниками данных заказчика нами использовалось решение на базе Apache Camel, которое на основе пользовательской конфигурации подключалось к произвольному источнику данных (от FTP до OPC UA) и применяло определенные пользователем трансформации над получаемыми данными. Такое решение зарекомендовало себя своей надежностью, а также легкостью в расширении для случая протоколов, которые уже есть в Apache Camel. Недостаток данного решения заключался в сложности подключения новых протоколов, которых нет в Apache Camel. Проблема была в появлении dependency hell, который состоял в появлении несовместимых транзитивных зависимостях.&lt;/p&gt;
  &lt;p id=&quot;z4BK&quot;&gt;Именно это послужило основным драйвером для исследования иных подходов в построении сервиса интеграции. Помимо этого у меня было ощущение, что возможно реализовать более эффективную инициализацию приложения за счет исключения Spring из проекта и ручной конфигурации сервисов. Это было возможно из-за небольшого количества зависимостей между компонентами.&lt;/p&gt;
  &lt;p id=&quot;n3pd&quot;&gt;В качестве решения было предложено использовать Apache Felix, самостоятельно определить интерфейс для компонента обработки данных и динамически подключать плагины на этапе старта приложения. Стоит подчеркнуть, что нам требовалось реализовать конвейер обработки данных: получение данных из удаленного источника, несколько этапов трансформации и запись в нашу систему хранения данных или чтение из нашей системы, несколько этапов трансформации и запись результата в удаленный источник данных.&lt;/p&gt;
  &lt;ul id=&quot;99O5&quot;&gt;
    &lt;li id=&quot;TKez&quot;&gt;READ FLOW: Чтение из системы заказчика, Преобразование, Запись в нашу систему&lt;/li&gt;
    &lt;li id=&quot;ff7X&quot;&gt;WRITE FLOW: Чтение из нашей системы, Преобразование, Запись в систему заказчика&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;Qzkp&quot;&gt;Важно было учитывать контекст задачи, который заключается в наличии несложных взаимодействий между этапами обработки данных. Формат value object был унифицирован. При этом конвейер обработки данных не содержал логических блоков или связей один ко многим в процессе передачи данных. Это значительно упрощало обработку данных.&lt;/p&gt;
  &lt;p id=&quot;RixL&quot;&gt;&lt;strong&gt;Структура проекта&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;3y0v&quot;&gt;&lt;strong&gt;Launcher&lt;/strong&gt;. Был выделен отдельный проект - launcher, который выполнял функцию ядра системы. Его зона ответственности была ограничена запуском osgi Framework, чтением конфигурации и динамическим подключением необходимых плагинов, которые были явно указаны в конфигурации; а также связыванием всех плагинов в единых конвейер на основе пользовательской конфигурации.&lt;/p&gt;
  &lt;blockquote id=&quot;cyR3&quot;&gt;В процессе реализации ядра и подключения базового плагина оказалось, что документации недостаточно для корректной конфигурации приложения. Оказалось очень полезным использовать поиск по Github для сравнения своего и чужого, вероятно работающего решения.&lt;/blockquote&gt;
  &lt;p id=&quot;sdn0&quot;&gt;&lt;strong&gt;Shared Code&lt;/strong&gt;. Общий код был выделен в два проекта: api - набор интерфейсов для реализации конвейерной обработки данных и parent - общий parent для всех проектов содержащий api в качестве зависимости, а также конфигурацию maven plugin, который позволял получить jar файл с кодом плагина.&lt;/p&gt;
  &lt;p id=&quot;GUlz&quot;&gt;&lt;strong&gt;Plugins&lt;/strong&gt;. Каждый плагин размещался в отдельном maven проекте и упаковывался в jar файл со специальной структурой (bundle в терминах osgi). За генерацию правильной структуры отвечает maven плагин org.apache.felix:maven-bundle-plugin, который принимает в качестве настроек название проекта, активатор (entrypoint) и перечень private/export/import/embed зависимостей.&lt;/p&gt;
  &lt;p id=&quot;ONDo&quot;&gt;&lt;strong&gt;Структура плагина (bundle)&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;9wHa&quot;&gt;Каждый плагин содержит в себе activator - класс, который будет запущен в момент подключения плагина. Ожидается, что в этот момент плагин будет регистрировать свои сервисы в контексте. Каждый сервис может содержать мета-информацию, которую он запишет в Dictionary.&lt;/p&gt;
  &lt;pre id=&quot;t8ph&quot; data-lang=&quot;java&quot;&gt;public class Activator implements BundleActivator {
  @Override
  public void start(final BundleContext bundleContext) {    
      Dictionary&amp;lt;String, Object&amp;gt; dictionary = new Hashtable&amp;lt;&amp;gt;();
      dictionary.put(&amp;quot;CustomField&amp;quot;, &amp;quot;API_IMPL_V1&amp;quot;);
      bundleContext.registerService(ApiService.class, new ApiServiceImpl(), dictionary);
    }
}&lt;/pre&gt;
  &lt;p id=&quot;VCtb&quot;&gt;Ядро приложения (Host в терминах OSGI) может обратиться к контексту с запросом на получение зарегистрированных сервисов с указанием полей метаданных:&lt;/p&gt;
  &lt;pre id=&quot;Yl3u&quot; data-lang=&quot;java&quot;&gt;var references =
        context.getServiceReferences(ApiService.class, &amp;quot;(CustomField=*)&amp;quot;);
Map&amp;lt;String, ConnectorService&amp;gt; index = new HashMap&amp;lt;&amp;gt;();
for (ServiceReference&amp;lt;ConnectorService&amp;gt; reference : references) {
    var  service = context.getService(reference);
    index.put(reference.getProperty(&amp;quot;CustomField&amp;quot;).toString(), service);
}&lt;/pre&gt;
  &lt;p id=&quot;QpwS&quot;&gt;При этом плагин будет содержать зависимости, недоступные остальным плагинам, если они будут помечены как Private.&lt;/p&gt;
  &lt;h3 id=&quot;77DY&quot;&gt;Неочевидности, о которых хотелось бы знать&lt;/h3&gt;
  &lt;p id=&quot;L8V8&quot;&gt;&lt;strong&gt;№1. Спецификация не позволяет иметь классы в default package&lt;/strong&gt;. Данное требование распространяется не только на ваш проект, но и на все ваши зависимости. Ошибка, которая будет выведена в случае нарушения требования, не будет информативной:&lt;/p&gt;
  &lt;blockquote id=&quot;ulgV&quot;&gt;[ERROR] Bundle {groupId}:{artifactId}:bundle:{version} : The default package ‘.’ is not permitted by the Import-Package syntax.&lt;br /&gt;This can be caused by compile errors in Eclipse because Eclipse creates&lt;br /&gt;valid class files regardless of compile errors.&lt;br /&gt;The following package(s) import from the default package null&lt;br /&gt;[ERROR] Error(s) found in bundle configuration&lt;/blockquote&gt;
  &lt;p id=&quot;waOb&quot;&gt;Для решения этой проблемы нужно разместить условный breakpoint в коде плагина “org.apache.felix:maven-bundle-plugin” и самостоятельно найти зависимость, содержащую неправильную структуру классов.&lt;/p&gt;
  &lt;p id=&quot;tmzt&quot;&gt;Подробное решение этой проблемы я разместил в отдельной статье : &lt;a href=&quot;https://medium.com/@mark.andreev/how-to-fix-the-default-package-is-not-permitted-by-the-import-package-syntax-in-osgi-3b59a6c18e71&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;https://medium.com/@mark.andreev/how-to-fix-the-default-package-is-not-permitted-by-the-import-package-syntax-in-osgi-3b59a6c18e71&lt;/u&gt;&lt;/a&gt;&lt;/p&gt;
  &lt;p id=&quot;HBdS&quot;&gt;&lt;strong&gt;№2. Неочевидные обязательные настройки “org.osgi.framework.launch.Framework”&lt;/strong&gt;. У вас не получится запустить apache felix без указания временной директории “Constants.FRAMEWORK_STORAGE”. В случае возникновения проблем ошибка не будет информативной.&lt;/p&gt;
  &lt;p id=&quot;Imzs&quot;&gt;&lt;strong&gt;№3. Отсутствие ошибки в случае проблем во время загрузки bundle&lt;/strong&gt;. Единственный способ понять, что bundle не загрузился - это сравнить SymbolicName у bundle с null.&lt;/p&gt;
  &lt;pre id=&quot;BdG7&quot; data-lang=&quot;java&quot;&gt;Bundle addition = bundleContext.installBundle(location);
if (addition.getSymbolicName() != null) {
   // TODO: add error
}&lt;/pre&gt;
  &lt;p id=&quot;nC5X&quot;&gt;&lt;strong&gt;№4. Сложности в передаче библиотечных классов в плагин&lt;/strong&gt;. Решением оказалось унификация интерфейсов в библиотеке api и использование только этих классов для общения между плагинами.&lt;/p&gt;
  &lt;h2 id=&quot;vUZK&quot;&gt;Заключение&lt;/h2&gt;
  &lt;p id=&quot;Vxfm&quot;&gt;Решение на базе Apache Felix продемонстрировало не только сложность в адаптации недостаточно популярной технологии, которое выражалось в нехватке знаний на Stackoverflow и необходимости использовать отладчик для исследования большинства проблем, что усложняет разбор инцидентов. С другой стороны, благодаря данной технологии мы получили низкую связность между компонентами системы, изоляцию плагинов на уровне загрузчика классов и более простую структуру проекта за счет выделения каждого компонента конвейера в отдельный проект; и значимое ускорение запуска.&lt;/p&gt;
  &lt;p id=&quot;0XuE&quot;&gt;Важно учитывать, что позитивный опыт напрямую связан со слабой связанностью между плагинами и отсутствием общих совместно используемых зависимостей помимо api library.&lt;/p&gt;
  &lt;p id=&quot;cHjL&quot;&gt;Если вам требуется более тесное взаимодействие, то стоит обратить внимание все же на Apache Karaf. Скорее всего вам будет удобнее не реализовывать низкоуровневое взаимодействие с OSGI, аналогичное описанному в проекте.&lt;/p&gt;
  &lt;p id=&quot;Sjti&quot;&gt;&lt;strong&gt;Послесловие&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;eN5x&quot;&gt;А был ли у вас опыт реализации микроядерной архитектуры? Как вы решали данную проблему?&lt;/p&gt;
  &lt;p id=&quot;tvCX&quot;&gt;&lt;a href=&quot;https://habr.com/ru/articles/801785/&quot; target=&quot;_blank&quot;&gt;Источник&lt;/a&gt;&lt;/p&gt;

</content></entry><entry><id>javalib:0S6p5vxdqG7</id><link rel="alternate" type="text/html" href="https://teletype.in/@javalib/0S6p5vxdqG7?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=javalib"></link><title>Осознанная оптимизация Compose 2: В борьбе с композицией</title><published>2024-03-06T07:20:13.218Z</published><updated>2024-03-06T07:20:13.218Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img4.teletype.in/files/7d/9b/7d9b6788-df7a-4c6c-a270-56bd811732dd.png"></media:thumbnail><category term="kotlin" label="Kotlin"></category><summary type="html">&lt;img src=&quot;https://img3.teletype.in/files/63/30/63304568-8ffd-4a13-b18d-528a6a22e984.png&quot;&gt;Jetpack Compose постоянно развивается, открывая перед разработчиками новые горизонты для оптимизации. С момента нашего последнего обзора, мы добились значительного прогресса, сократив задержки при скролле с 5-7% до нуля. В этом материале мы поделимся свежими находками и передовыми практиками в оптимизации Compose. Чтобы максимально углубиться в тему, рекомендуем ознакомиться с первой частью.</summary><content type="html">
  &lt;figure id=&quot;CuUD&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img3.teletype.in/files/63/30/63304568-8ffd-4a13-b18d-528a6a22e984.png&quot; width=&quot;1024&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;8Rzs&quot;&gt;Jetpack Compose постоянно развивается, открывая перед разработчиками новые горизонты для оптимизации. С момента нашего последнего обзора, мы добились значительного прогресса, сократив задержки при скролле с 5-7% до нуля. В этом материале мы поделимся свежими находками и передовыми практиками в оптимизации Compose. Чтобы максимально углубиться в тему, рекомендуем ознакомиться с первой частью.&lt;/p&gt;
  &lt;p id=&quot;HSwq&quot;&gt;Серия статей:&lt;/p&gt;
  &lt;ol id=&quot;EcKq&quot;&gt;
    &lt;li id=&quot;t5Ke&quot;&gt;&lt;a href=&quot;https://teletype.in/@javalib/3xWBnSD99aC&quot; target=&quot;_blank&quot;&gt;Осознанная оптимизация Compose&lt;/a&gt;&lt;/li&gt;
    &lt;li id=&quot;pxfB&quot;&gt;Осознанная оптимизация Compose 2: В борьбе с композицией (текущая)&lt;/li&gt;
  &lt;/ol&gt;
  &lt;h2 id=&quot;qa1D&quot;&gt;Композиция - низвергнутый бог&lt;/h2&gt;
  &lt;h3 id=&quot;9bJT&quot;&gt;Проблема начальной композиции&lt;/h3&gt;
  &lt;p id=&quot;vmdE&quot;&gt;В первой части была описана одна из проблем ленивых списков - использование под капотом &lt;code&gt;SubcomposeLayout&lt;/code&gt;. По словам разработчиков, ещё одной проблемой является скорость начальной композиции. Во время этого затратного этапа в первый раз строится дерево элементов. Для ленивых списков этот момент особенно критичен, поскольку начальная композиция происходит при создании каждого элемента.&lt;/p&gt;
  &lt;p id=&quot;xQ2o&quot;&gt;В &lt;a href=&quot;https://youtu.be/h-b3-DWYhjo?t=2025&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;подкасте&lt;/u&gt;&lt;/a&gt; разработчики подчеркивают, что Compose проектировался исходя из того, что людям проще думать в категориях простых лейаутов (&lt;code&gt;Box&lt;/code&gt;, &lt;code&gt;Column&lt;/code&gt;, &lt;code&gt;Row&lt;/code&gt;), а не таких сложных, как &lt;code&gt;ConstraintLayout&lt;/code&gt;. В отличие от View, у Compose нет проблемы экспоненциального увеличения количества измерений, так как они ограничены одним проходом. Пропуски функций также способствуют решению проблемы вложенности. Но не в случае начальной композиции, когда нужно пройтись по тысяче лейаутов. Разработчики стремятся снизить и эту нагрузку: переход с версии Compose 1.4 на 1.6, &lt;a href=&quot;https://www.droidcon.com/2023/11/15/performance-in-composelessons-learned-from-lazy-layouts/&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;по их словам&lt;/u&gt;&lt;/a&gt;, ускорит работу списков на 40%, что служит весомым аргументом в пользу обновления. Однако для уже оптимизированных списков прирост производительности может быть не так заметен.&lt;/p&gt;
  &lt;p id=&quot;cvlV&quot;&gt;Композиция в Compose обходится дорого, но это цена за удобство декларативного подхода. В данной статье мы предложим способы минимизации затрат на композицию и снижения их стоимости при рекомпозициях.&lt;/p&gt;
  &lt;p id=&quot;TgFQ&quot;&gt;Для начала давайте посмотрим, как время композиции соотносится с другими фазами Compose:&lt;/p&gt;
  &lt;figure id=&quot;JRtm&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/dbd/d0b/618/dbdd0b61861a07ca65e48cf7a725e0d7.png&quot; width=&quot;1360&quot; /&gt;
    &lt;figcaption&gt;Среднее время каждой фазы. Данные для слабого устройства. &lt;a href=&quot;https://www.droidcon.com/2023/11/15/performance-in-composelessons-learned-from-lazy-layouts/&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;Источник&lt;/u&gt;&lt;/a&gt;&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;e6lo&quot;&gt;Из этого графика становится ясно, почему перенос работы на следующую фазу из композиции оказывается таким эффективным. Однако при этом не следует забывать о затратах на создание лямбд и потенциальных проблемах с их равенством при неудачном замыкании. Поэтому и стоит их использовать для откладывания частых изменений, а не единичных.&lt;/p&gt;
  &lt;h3 id=&quot;tJ2D&quot;&gt;Modifier.Node&lt;/h3&gt;
  &lt;p id=&quot;BR2R&quot;&gt;&lt;code&gt;Modifier.composed&lt;/code&gt; долгое время был основным инструментом для создания кастомных модификаторов. Однако, несмотря на его гибкость, этот подход &lt;a href=&quot;https://www.youtube.com/watch?v=BjGX2RftXsU&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;имеет свои недостатки&lt;/u&gt;&lt;/a&gt;. &lt;code&gt;Modifier.composed&lt;/code&gt; требует передачи composable-лямбды, внутри которой генерируется restartable-группа и множество других конструкций. Во время композиции под капотом вызываются эти composable-лямбды для получения конечного модификатора. Этот процесс называется &lt;a href=&quot;https://developer.android.com/reference/kotlin/androidx/compose/ui/package-summary#(androidx.compose.runtime.Composer).materialize(androidx.compose.ui.Modifier)&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;материализацией&lt;/u&gt;&lt;/a&gt;, что усложняет начальную композицию. Кроме этого из-за лямбд страдало и сравнение модификаторов, так как каждый раз создаётся новая лямбда, которая не равна прошлой, следовательно цепочка модификаторов тоже считается другой из-за чего Compose делает меньше пропусков.&lt;/p&gt;
  &lt;p id=&quot;ImLY&quot;&gt;Теперь на помощь приходит &lt;code&gt;Modifier.Node&lt;/code&gt; – новый, более эффективный способ создания кастомных модификаторов. Этот подход позволяет реализовать все задачи, для которых ранее использовался &lt;code&gt;Modifier.composed&lt;/code&gt;, но без лишних накладных расходов, связанных с composable-лямбдами и композицией. Самое важное преимущество – классы, созданные с помощью &lt;code&gt;Modifier.Node&lt;/code&gt;, могут быть сравнены и переиспользованы, что значительно улучшает производительность.&lt;/p&gt;
  &lt;p id=&quot;bTih&quot;&gt;Google недавно обновила &lt;a href=&quot;https://developer.android.com/jetpack/compose/custom-modifiers&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;документацию&lt;/u&gt;&lt;/a&gt;, добавив множество примеров и рекомендаций по использованию &lt;code&gt;Modifier.Node&lt;/code&gt;. Это отличный повод пересмотреть существующие реализации модификаторов в вашем проекте и заменить их на более оптимизированные &lt;code&gt;Modifier.Node&lt;/code&gt;.&lt;/p&gt;
  &lt;h3 id=&quot;NeVy&quot;&gt;Проблемы DerivedState и remember&lt;/h3&gt;
  &lt;p id=&quot;asSJ&quot;&gt;Как мы уже выяснили, композиция сама по себе дорогая и стоит использовать &lt;code&gt;DerivedState&lt;/code&gt;, чтобы ограничить число рекомпозиций. Однако часто разработчики применяют его некорректно, что было подробно разобрано в первой части. Сейчас стоит объяснить, почему так важно следить за этим.&lt;/p&gt;
  &lt;p id=&quot;9xW2&quot;&gt;Рассмотрим время, затрачиваемое на чтение тех или иных данных, чтобы понять масштабы проблемы (&lt;a href=&quot;https://www.droidcon.com/2023/11/15/performance-in-composelessons-learned-from-lazy-layouts/&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;источник&lt;/u&gt;&lt;/a&gt;):&lt;/p&gt;
  &lt;ul id=&quot;Oj9B&quot;&gt;
    &lt;li id=&quot;GhoX&quot;&gt;Чтение локальной переменной - 1 нс&lt;/li&gt;
    &lt;li id=&quot;xnYl&quot;&gt;Чтение из поля класса - 5 нс&lt;/li&gt;
    &lt;li id=&quot;lsOS&quot;&gt;Вызов метода - 10 нс&lt;/li&gt;
    &lt;li id=&quot;Nsor&quot;&gt;Чтение с синхронизацией - 50 нс&lt;/li&gt;
    &lt;li id=&quot;VPbS&quot;&gt;map.get - 150 нс&lt;/li&gt;
    &lt;li id=&quot;OwED&quot;&gt;state.value - 2 500 нс&lt;/li&gt;
    &lt;li id=&quot;PcXw&quot;&gt;derivedState.value - 10 000 нс&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;h64l&quot;&gt;Учитывая, что для поддержания частоты обновления экрана в 120 кадров в секунду максимальное время отрисовки одного кадра не должно превышать 8 333 333 нс, становится очевидным, что неправильное использование &lt;code&gt;DerivedState&lt;/code&gt; может значительно замедлить ваше приложение. Если такой подход не приводит к заметному уменьшению числа рекомпозиций, это может сделать код медленнее, чем при прямом обращении к исходному State&amp;lt;T&amp;gt;.&lt;/p&gt;
  &lt;p id=&quot;grlL&quot;&gt;Та же логика применима и к функции &lt;code&gt;remember { }&lt;/code&gt;. Необдуманное &amp;quot;запоминание&amp;quot; простых вычислений может неоправданно замедлить работу приложения. Например, чтобы запомнить обычное выражение, потребуется сперва сравнить ключи с их предыдущими значениями, что уже само по себе будет дороже, и это не говоря про чтение и запись в Slot Table.&lt;/p&gt;
  &lt;pre id=&quot;zLjW&quot;&gt;// Антипримерval expr = remember(a, b, c) { a + b * c }&lt;/pre&gt;
  &lt;p id=&quot;mVz9&quot;&gt;Поэтому и нужно хоть немного знать, как Compose работает под капотом, чтобы не использовать его функции себе во вред.&lt;/p&gt;
  &lt;h3 id=&quot;5jJT&quot;&gt;Пересядь с иглы Compose на старый добрый Kotlin&lt;/h3&gt;
  &lt;p id=&quot;7qKb&quot;&gt;Для эффективной работы с Jetpack Compose ключевым является умение находить золотую середину между использованием возможностей Compose и стандартным Kotlin кодом. Как отметил разработчик Compose Андрей Шиков: &amp;quot;Изучая Compose, мы позабыли как использовать Kotlin&amp;quot;. Давайте взглянем на &lt;a href=&quot;https://www.droidcon.com/2023/11/15/performance-in-composelessons-learned-from-lazy-layouts/&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;его пример&lt;/u&gt;&lt;/a&gt;, демонстрирующий этот подход. В исходной версии кода использовалось множество отдельных состояний и сайд-эффектов:&lt;/p&gt;
  &lt;pre id=&quot;TwM8&quot; data-lang=&quot;kotlin&quot;&gt;@Composable 
fun MyComponent(modifier: Modifier, config: Config) {
    val interactionSource = remember { MutableInteractionSource() }
    val activeInteractions = remember { mutableStateListOf&amp;lt;Interaction&amp;gt;() }
    val config by rememberUpdatedState(config)

    LaunchedEffect(interaction Source) {
        interactionSource.interactions.collect {
            // Обновление списке взаимодействий
        }
    }

    val animatableColor = remember {
        Animatable(config.defaultColor, Color.VectorConverter)
    }

    LaunchedEffect(config) {
        // Обновление animatableColor
    }

    LaunchedEffect(interactions) {
        snapshotFlow { activeInteractions.lastOrNull() }.collect {
            // Обновление animatableColor на основе взаимодействий и конфига
        }
    }
    
    // Использование animatableColor при отрисовке
}&lt;/pre&gt;
  &lt;p id=&quot;aR6P&quot;&gt;Этот код был оптимизирован путём объединения нескольких состояний и сокращения количества сайд-эффектов за счёт переноса логики в одну корутину:&lt;/p&gt;
  &lt;pre id=&quot;jyOX&quot; data-lang=&quot;kotlin&quot;&gt;@Composable 
fun MyComponent(modifier: Modifier, config: Config) {
    val state = remember { MyComponentState(config) }

    LaunchedEffect(state) {
        state.collectUpdates()
    }

    SideEffect {
        state.config.value = config
    }

    // Использование state.animatableColor при отрисовке
}&lt;/pre&gt;
  &lt;p id=&quot;mp01&quot;&gt;Благодаря такому рефакторингу удалось ускорить работу кода на &lt;strong&gt;9%&lt;/strong&gt;.&lt;/p&gt;
  &lt;h3 id=&quot;rUmq&quot;&gt;Пререндер&lt;/h3&gt;
  &lt;p id=&quot;bUd6&quot;&gt;При первом открытии экрана приложения, время ожидания пользователя состоит из двух основных этапов: загрузки данных и отрисовки UI. Для минимизации времени ожидания появления контента на экране можно использовать технику пререндера.&lt;/p&gt;
  &lt;p id=&quot;KdEW&quot;&gt;Суть метода заключается в том, что во время загрузки данных параллельно происходит отрисовка экрана с использованием моковых данных. Очень важно, чтобы структура композиции UI не менялась сильно после замены моковых данных на реальные. Учитывая высокую стоимость процесса композиции, такой подход позволяет заранее &amp;quot;прогреть&amp;quot; композицию, существенно сокращая время до появления актуального контента на экране.&lt;/p&gt;
  &lt;p id=&quot;mIw6&quot;&gt;Один из вариантов реализации пререндеринга – использование индикатора загрузки, отрисованного поверх всего экрана:&lt;/p&gt;
  &lt;pre id=&quot;zlsx&quot; data-lang=&quot;kotlin&quot;&gt;ProductDetailScreen(state: ProductUiState) {
    // Вначале state содержит пустые данные для пререндера контента
    ProductDetailContent(state)

    // Отображение индикатора загрузки поверх контента, а не вместо него
    if (state.isLoading) {
        FullscreenLoader()
    }
}&lt;/pre&gt;
  &lt;p id=&quot;Kpim&quot;&gt;Также можно применять эффект шиммера на моковых данных, используя модификатор &lt;code&gt;placeholder&lt;/code&gt; из библиотеки accompanist. Этот метод не влияет на структуру композиции после загрузки реальных данных и обеспечивает плавный визуальный переход. Однако, он требует дополнительной адаптации существующих элементов для корректной отрисовки шиммера поверх контента.&lt;/p&gt;
  &lt;p id=&quot;HCHo&quot;&gt;В контексте использования ленивых списков особое внимание следует уделить однородности элементов с одинаковым &lt;code&gt;contentType&lt;/code&gt;. Если элементы списка не будут сильно отличаться по структуре композиции, это снизит необходимость в дополнительной работе по перестройке дерева композиции при их переиспользовании, что также способствует ускорению скролла.&lt;/p&gt;
  &lt;h3 id=&quot;2d82&quot;&gt;Отложенная композиция&lt;/h3&gt;
  &lt;p id=&quot;H1Wd&quot;&gt;Ленивые лейауты используют &lt;code&gt;SubcomposeLayout&lt;/code&gt; для определения того, какие элементы должны быть отображены на экране. Для этого происходит внутренняя композиция во время фазы компоновки (layout). Этот процесс происходит за один кадр, что может стать проблемой при работе с тяжёлыми экранами.&lt;/p&gt;
  &lt;p id=&quot;HC3W&quot;&gt;В таких случаях может быть полезной техника отложенной композиции, которая позволяет распределить процесс композиции элементов по нескольким кадрам, улучшая тем самым скорость отрисовки кадров. О применении данного метода можно почитать в &lt;a href=&quot;https://www.bam.tech/en/article/android-app-performance-optimize-startup-time-with-above-the-fold-technique&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;статье по ссылке&lt;/u&gt;&lt;/a&gt;.&lt;/p&gt;
  &lt;p id=&quot;AGSf&quot;&gt;В примере ниже демонстрируется как отложить композицию части экрана, используя метод &lt;code&gt;withFrameNanos { }&lt;/code&gt;, который аналогично &lt;code&gt;delay()&lt;/code&gt; останавливает корутины, но делает это ровно до начала следующего кадра:&lt;/p&gt;
  &lt;pre id=&quot;Z40v&quot; data-lang=&quot;kotlin&quot;&gt;@Composable
fun ProductDetail(
    productInfo: ProductInfo
) {
    var blockState by remember { mutableStateOf(0) }

    LaunchedEffect(Unit) {
        while (blockState &amp;lt; 3) {
            // Откладываем композицию каждого блока на 1 кадр
            withFrameNanos {  }
            blockState += 1
        }
    }

    ProductBlock0(productInfo)

    if (blockState &amp;gt;= 1) {
        ProductBlock1(productInfo)
    }
    if (blockState &amp;gt;= 2) {
        ProductBlock2(productInfo)
    }
    if (blockState &amp;gt;= 3) {
        ProductBlock2(productInfo)
    }
}&lt;/pre&gt;
  &lt;h2 id=&quot;gOpP&quot;&gt;Painter&lt;/h2&gt;
  &lt;h3 id=&quot;T6kh&quot;&gt;Xml-иконки vs Compose-иконки&lt;/h3&gt;
  &lt;p id=&quot;mncR&quot;&gt;При работе с иконками в Compose существует два основных подхода: использование иконок в формате XML и прямое создание иконок с помощью кода. Рассмотрим процесс и эффективность каждого из них.&lt;/p&gt;
  &lt;pre id=&quot;tRgI&quot; data-lang=&quot;kotlin&quot;&gt;// Xml-иконка
Image(
    painter = painterResource(R.drawable.my_icon), 
    contentDescription = null
)

// Compose-иконка
Image(
    painter = rememberVectorPainter(Icons.Filled.Home), 
    contentDescription = null
)&lt;/pre&gt;
  &lt;p id=&quot;DaQy&quot;&gt;Для XML иконки процесс загрузки включает вызов &lt;code&gt;painterResource(R.drawable.my_icon)&lt;/code&gt;, состоящий из следующих шагов:&lt;/p&gt;
  &lt;ol id=&quot;y8d5&quot;&gt;
    &lt;li id=&quot;9VVL&quot;&gt;Чтение ресурса из файла или получение его из кэша.&lt;/li&gt;
    &lt;li id=&quot;pPL0&quot;&gt;Преобразования XML в &lt;code&gt;ImageVector&lt;/code&gt;.&lt;/li&gt;
    &lt;li id=&quot;kkMe&quot;&gt;Создание &lt;code&gt;Painter&lt;/code&gt; с помощью функции &lt;code&gt;rememberVectorPainter()&lt;/code&gt;.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;yeWS&quot;&gt;В случае с иконками, созданными непосредственно в Compose, процесс выглядит следующим образом:&lt;/p&gt;
  &lt;ol id=&quot;D82j&quot;&gt;
    &lt;li id=&quot;y0GS&quot;&gt;Вызов &lt;code&gt;Icons.Filled.Home&lt;/code&gt; сразу инициирует создание объекта &lt;code&gt;ImageVector&lt;/code&gt;.&lt;/li&gt;
    &lt;li id=&quot;JGOa&quot;&gt;Создание &lt;code&gt;Painter&lt;/code&gt; с помощью функции &lt;code&gt;rememberVectorPainter()&lt;/code&gt;.&lt;/li&gt;
  &lt;/ol&gt;
  &lt;p id=&quot;9Q5e&quot;&gt;Тесты показали, что иконки, созданные с помощью Compose, загружаются от 5% до 18% быстрее по сравнению с иконками в формате XML. Скорость загрузки напрямую зависит от сложности структуры иконки и размера исходного файла. Важно отметить, что общее влияние иконок на производительность приложения может сильно варьироваться в зависимости от их количества на экране и использования в ленивых списках.&lt;/p&gt;
  &lt;p id=&quot;W21K&quot;&gt;Для создания собственных Compose-иконок можно воспользоваться инструментом &lt;a href=&quot;https://github.com/DevSrSouza/svg-to-compose&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;SVG to Compose&lt;/u&gt;&lt;/a&gt;, который поддерживает конвертацию как SVG, так и XML файлов. Также стоит упомянуть, что эти иконки убавят вам проблем с ресурсами при использовании Compose Multiplatform.&lt;/p&gt;
  &lt;h3 id=&quot;ZAlX&quot;&gt;Нестабильность Painter&lt;/h3&gt;
  &lt;p id=&quot;3aSO&quot;&gt;В этом параграфе предполагалось обсудить, как нестабильность &lt;code&gt;Painter&lt;/code&gt; влияет на производительность отображения изображений и иконок и в каких случаях целесообразно использовать обёртку для &lt;code&gt;Painter&lt;/code&gt;. Однако, с внедрением возможности объявлять внешние типы как стабильные, данный вопрос теряет актуальность. Об этом в главе про нововведения.&lt;/p&gt;
  &lt;h3 id=&quot;s33A&quot;&gt;Вынос Painter&lt;/h3&gt;
  &lt;p id=&quot;KgDR&quot;&gt;Вынесение создания объекта &lt;code&gt;Painter&lt;/code&gt; за пределы списка позволяет заметно увеличить скорость отрисовки. Это особенно критично в списках, где каждый элемент требует инициализации собственного &lt;code&gt;Painter&lt;/code&gt;. Несмотря на наличие кэша для XML ресурсов, каждый такой вызов создает дополнительную нагрузку. Создание единого &lt;code&gt;Painter&lt;/code&gt; для всего списка значительно уменьшает эту нагрузку. Однако, как и в случае с выносом модификаторов, может пострадать читабельность кода.&lt;/p&gt;
  &lt;p id=&quot;P7z6&quot;&gt;Пример до оптимизации:&lt;/p&gt;
  &lt;pre id=&quot;vDHJ&quot; data-lang=&quot;kotlin&quot;&gt;@Composable
fun MyList(products: ImmutableList&amp;lt;ProductUiState&amp;gt;) {
    LazyColumn { 
        items(products) { product -&amp;gt; 
            // Painter внутри элемента списка
            MyProductItem(product, painterResource(R.drawable.ic_menu)) 
        } 
    }
}&lt;/pre&gt;
  &lt;p id=&quot;TakP&quot;&gt;Пример после оптимизации:&lt;/p&gt;
  &lt;pre id=&quot;eBN1&quot; data-lang=&quot;kotlin&quot;&gt;@Composable
fun MyList(products: ImmutableList&amp;lt;ProductUiState&amp;gt;) {
    // Выносим Painter для общей иконки из списка
    val menuPainter = painterResource(R.drawable.ic_menu)

    LazyColumn { 
        items(products) { product -&amp;gt; 
            MyProductItem(product, menuPainter) 
        } 
    }
}&lt;/pre&gt;
  &lt;p id=&quot;FBVn&quot;&gt;Этот метод эффективен не только для иконок, но и для других ресурсов, хотя для последних прирост производительности может быть менее заметен. Подход актуален не только для списков, но и для мест с частой рекомпозицией из-за анимаций, где рекомендуется вообще избегать создания дорогих объектов.&lt;/p&gt;
  &lt;h2 id=&quot;dgE7&quot;&gt;Дизайн система&lt;/h2&gt;
  &lt;p id=&quot;Gq7x&quot;&gt;Создание дизайн-системы является ключевым этапом при внедрении Jetpack Compose в проекты. Разработчики на этом этапе могут столкнуться с проблемами из-за недостаточного опыта разработки на новой технологии, что ведет к появлению неэффективного кода в важнейших частях дизайн-системы.&lt;/p&gt;
  &lt;h3 id=&quot;Gv0V&quot;&gt;Цветовая схема&lt;/h3&gt;
  &lt;p id=&quot;jhT6&quot;&gt;Традиционно при создании кастомной цветовой палитры разработчики копируют подход, используемый в &lt;code&gt;MaterialTheme&lt;/code&gt;. Однако недавние &lt;a href=&quot;https://android-review.googlesource.com/c/platform/frameworks/support/+/2733678&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;коммиты разработчиков&lt;/u&gt;&lt;/a&gt; указывают на недостатки такой реализации.&lt;/p&gt;
  &lt;p id=&quot;tEQz&quot;&gt;В традиционной реализации для каждого цвета создавался отдельный &lt;code&gt;State&amp;lt;T&amp;gt;&lt;/code&gt;, что вело к необходимости подписки на изменение состояния каждый раз при его чтении. Это могло негативно повлиять на производительность, особенно если в дизайн-системе использовалось множество цветов.&lt;/p&gt;
  &lt;p id=&quot;Ko7c&quot;&gt;Пример до оптимизации:&lt;/p&gt;
  &lt;pre id=&quot;Xyz5&quot; data-lang=&quot;kotlin&quot;&gt;@Stable
class ColorScheme(
    primary: Color,
    onPrimary: Color,
) {
    // State&amp;lt;T&amp;gt; для каждого цвета
    var primary by mutableStateOf(primary)
        internal set
    var onPrimary by mutableStateOf(onPrimary)
        internal set
}&lt;/pre&gt;
  &lt;p id=&quot;cERR&quot;&gt;Изначально такая реализация имела преимущество в гибкости — возможность изменять каждый цвет отдельно без значительных затрат на производительность. Однако, как показывает практика, в большинстве приложений цветовая схема меняется лишь при переключении между светлой и темной темами, а не при индивидуальном изменении отдельных цветов.&lt;/p&gt;
  &lt;p id=&quot;v9mn&quot;&gt;Переход к использованию обычного &lt;code&gt;data class&lt;/code&gt; для описания цветовой схемы устраняет необходимость в подписках на состояние и улучшает производительность приложения.&lt;/p&gt;
  &lt;p id=&quot;0yGF&quot;&gt;Пример после оптимизации:&lt;/p&gt;
  &lt;pre id=&quot;js4S&quot; data-lang=&quot;kotlin&quot;&gt;@Immutable
data class ColorScheme(
    val primary: Color,
    val onPrimary: Color,
)&lt;/pre&gt;
  &lt;h3 id=&quot;dlUP&quot;&gt;Ошибки прошлого&lt;/h3&gt;
  &lt;p id=&quot;nbQg&quot;&gt;Код, о котором будет идти речь, был написан давно и с тех пор не подвергался изменениям. Однако именно он использовался на всех наших экранах и существенно влиял на время отрисовки кадров из-за неэффективной работы с типографией в &lt;code&gt;AppTheme&lt;/code&gt;.&lt;/p&gt;
  &lt;p id=&quot;vFKz&quot;&gt;&lt;code&gt;AppTheme.typography&lt;/code&gt; &lt;u&gt;создаёт&lt;/u&gt; новую типографию &lt;u&gt;при каждом вызове&lt;/u&gt;, загружая шрифты и цвета из ресурсов для каждого стиля текста отдельно. Это приводило к множественным обращениям к ресурсам и чтению &lt;code&gt;AppTheme.colors.textPrimary&lt;/code&gt; 16 раз для каждого &lt;code&gt;TextStyle&lt;/code&gt;.&lt;/p&gt;
  &lt;pre id=&quot;JeyP&quot; data-lang=&quot;kotlin&quot;&gt;object AppTheme {
    @Composable
    @ReadonlyComposable
    val typography
        get() = DefaultAppTypography
}

@Composable
@ReadonlyComposable
val DefaultAppTypography
    get() = AppTypography(
        headXXL = TextStyle(
            color = AppTheme.colors.textPrimary,
            ...
        ),
        // ...
        // Создание 15 других стилей текста
    )&lt;/pre&gt;
  &lt;p id=&quot;R03F&quot;&gt;Эта реализация особенно заметно замедляла отрисовку на экранах с большим количеством текста и различными стилями, поскольку каждый текстовый элемент инициировал вызов условного &lt;code&gt;AppTheme.typography.headXXL&lt;/code&gt;. Нашли мы эту проблему после исследования нескольких экранов с помощью &lt;a href=&quot;https://habr.com/ru/companies/ozontech/articles/742854/#7.3&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;трассировки композиции&lt;/u&gt;&lt;/a&gt;.&lt;/p&gt;
  &lt;p id=&quot;7biv&quot;&gt;Решение проблемы заключалась в изменении подхода к созданию и использованию типографии. Теперь типография &lt;code&gt;AppTheme&lt;/code&gt; &lt;u&gt;инициализируется единожды&lt;/u&gt; и доступна через &lt;code&gt;LocalAppTypography.current&lt;/code&gt;, что значительно сокращает количество обращений к ресурсам и ускоряет работу с типографией во всем приложении:&lt;/p&gt;
  &lt;pre id=&quot;VnVT&quot; data-lang=&quot;kotlin&quot;&gt;object AppTheme {
    @Composable
    @ReadonlyComposable
    val typography
        get() = LocalAppTypography.current
}&lt;/pre&gt;
  &lt;h2 id=&quot;QICX&quot;&gt;Форматтеры&lt;/h2&gt;
  &lt;p id=&quot;rmWO&quot;&gt;Важно помнить о правильном размещении логики форматирования чисел и валют. Интуитивно может показаться, что использование форматирования непосредственно в коде Compose — это удобно. Однако, этот подход может привести к неожиданным проблемам производительности. Рекомендуется переносить создание и использование форматтеров в бизнес-логику вашего приложения. Такой шаг позволяет не только уменьшить нагрузку на главный поток, но и избежать излишнего дублирования объектов форматтера, обеспечивая их переиспользование между экранами. Этот совет кажется простым, но на практике часто упускается из виду, особенно когда форматирование спрятано за утилитарные функции.&lt;/p&gt;
  &lt;p id=&quot;2nLI&quot;&gt;Антипример форматирования в Compose-коде:&lt;/p&gt;
  &lt;pre id=&quot;2la6&quot; data-lang=&quot;kotlin&quot;&gt;Text(
    text = productItem.price.toMoneyFormat()
)&lt;/pre&gt;
  &lt;p id=&quot;6Qtd&quot;&gt;Однако, необходимо учитывать, что оптимизация за счёт переноса форматирования может не всегда давать ожидаемый результат. В случае обработки списка в бизнес-логике, форматирование применяется ко всему списку сразу, в то время как в Compose, в контексте ленивых списков, форматирование выполняется исключительно для элементов, видимых пользователю. Это означает, что перемещение логики форматирования может потенциально увеличить время до отображения содержимого на экране. Важно помнить, что не каждая оптимизация ведёт к улучшению производительности, и нужно тщательно анализировать изменения, прежде чем их внедрять&lt;/p&gt;
  &lt;h2 id=&quot;KQrA&quot;&gt;Оптимизация на спичках&lt;/h2&gt;
  &lt;p id=&quot;hlgD&quot;&gt;Эти рекомендации пригодятся вам при разработке утилитарных функций, основных компонентов приложения или при создании дизайн-системы. Особенно актуально это становится при работе с анимациями и графикой. При написании кода, который исполняется в фоновом потоке и не используется повсеместно, стремление к использованию структур, более эффективных, чем стандартные, может оказаться излишним. Важно помнить, что оптимизация алгоритмической сложности даст более заметный прирост в производительности, чем экономия на спичках.&lt;/p&gt;
  &lt;h3 id=&quot;8veb&quot;&gt;Автоупаковка&lt;/h3&gt;
  &lt;p id=&quot;hJG8&quot;&gt;Автоупаковка примитивных типов данных может незаметно замедлить выполнение кода, особенно когда он используется в большом количестве мест. В Jetpack Compose разработчики активно стремятся минимизировать подобные затраты, применяя различные способы:&lt;/p&gt;
  &lt;ul id=&quot;0Np7&quot;&gt;
    &lt;li id=&quot;MQOw&quot;&gt;Использование специальных &lt;code&gt;MutableState&lt;/code&gt; для примитивных типов (например, &lt;code&gt;mutableIntStateOf()&lt;/code&gt;) помогает избежать упаковки.&lt;/li&gt;
    &lt;li id=&quot;N3Av&quot;&gt;Введение специальных значений (&lt;code&gt;Unspecified&lt;/code&gt;) для определенных классов вместо &lt;code&gt;null&lt;/code&gt; позволяет избежать автоупаковки благодаря инлайнингу value классов. Например, недавно &lt;a href=&quot;https://issuetracker.google.com/issues/299490814&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;добавили Unspecified&lt;/u&gt;&lt;/a&gt; для &lt;code&gt;TextAlign&lt;/code&gt;, &lt;code&gt;TextDirection&lt;/code&gt; и др., чтобы избежать &lt;code&gt;null&lt;/code&gt;.&lt;/li&gt;
    &lt;li id=&quot;9kZ9&quot;&gt;Замена &lt;code&gt;Pair&amp;lt;Int, Int&amp;gt;&lt;/code&gt; на &lt;code&gt;value class&lt;/code&gt; с полем типа &lt;code&gt;Long&lt;/code&gt; значительно снижает затраты на хранение данных, используя стек вместо кучи. Тип &lt;code&gt;Long&lt;/code&gt; содержит в два раза больше бит, чем &lt;code&gt;Int&lt;/code&gt;, что позволяет ему хранить два числа типа &lt;code&gt;Int&lt;/code&gt; и обращаться к ним с помощью побитовых операций.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;h3 id=&quot;7kCx&quot;&gt;Быстрые методы&lt;/h3&gt;
  &lt;p id=&quot;j18n&quot;&gt;Не все стандартные методы Kotlin идеально подходят для конкретных задач. Jetpack Compose предлагает альтернативы, например, &lt;code&gt;fastForEach&lt;/code&gt; или &lt;code&gt;fastFirst&lt;/code&gt;. О быстрых методах можно прочитать в блоге &lt;a href=&quot;https://www.romainguy.dev/posts/&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;Romain Guy&lt;/u&gt;&lt;/a&gt;.&lt;/p&gt;
  &lt;h3 id=&quot;NXex&quot;&gt;Эффективные структуры&lt;/h3&gt;
  &lt;p id=&quot;UShB&quot;&gt;Стандартные структуры данных в Kotlin могут быть не лучшим выбором для специализированных задач.&lt;/p&gt;
  &lt;ul id=&quot;b0ZI&quot;&gt;
    &lt;li id=&quot;dKv9&quot;&gt;Например, &lt;code&gt;mutableListOf&lt;/code&gt; использует &lt;code&gt;ArrayList&lt;/code&gt;, что может быть избыточным, если не требуется динамическое изменение размера коллекции или использование дженериков. В критически важных частях кода лучше применять специализированные массивы (например, &lt;code&gt;IntArray&lt;/code&gt;).&lt;/li&gt;
    &lt;li id=&quot;B6qU&quot;&gt;Метод &lt;code&gt;mutableMapOf&lt;/code&gt; по умолчанию создаёт &lt;code&gt;LinkedHashMap&lt;/code&gt;, что может быть менее эффективным, чем другие типы данных. Jetpack Compose использует, например, &lt;code&gt;ScatterMap&lt;/code&gt;.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;tCnm&quot;&gt;Внутри &lt;a href=&quot;https://developer.android.com/reference/androidx/collection/package-summary&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;AndroidX Collections&lt;/u&gt;&lt;/a&gt; много оптимизированных структур, которые стоит использовать в критическом коде.&lt;/p&gt;
  &lt;h3 id=&quot;sK23&quot;&gt;Вложенные if&lt;/h3&gt;
  &lt;p id=&quot;3Vja&quot;&gt;При работе с условными конструкциями важно учитывать влияние вложенности на производительность. Каждая условная ветка создаёт дополнительные вызовы replaceable-группы для поддержки быстрой замены кода. Поэтому в случаях, когда вложенность условных блоков не является необходимостью, предпочтение следует отдавать плоской структуре. Это позволяет не только упростить код, но и повысить его производительность за счёт уменьшения количества операций.&lt;/p&gt;
  &lt;p id=&quot;zAhL&quot;&gt;Пример генерации кода для плоских if (также и для when):&lt;/p&gt;
  &lt;pre id=&quot;ZsZx&quot; data-lang=&quot;kotlin&quot;&gt;if (condition1) {
    $composer.startReplaceableGroup()
    Content1()
    $composer.endReplaceableGroup()
} else if (condition2) {
    $composer.startReplaceableGroup()
    Content2()
    $composer.endReplaceableGroup()
} else {
    $composer.startReplaceableGroup()
    Content3()
    $composer.endReplaceableGroup()
}&lt;/pre&gt;
  &lt;p id=&quot;DPVf&quot;&gt;Пример генерации кода для вложенных if:&lt;/p&gt;
  &lt;pre id=&quot;GnF5&quot; data-lang=&quot;kotlin&quot;&gt;if (condition1) {
    $composer.startReplaceableGroup()
    Content1()
    $composer.endReplaceableGroup()
} else {
    $composer.startReplaceableGroup()
    if (condition2) {
        $composer.startReplaceableGroup()
        Content2()
        $composer.endReplaceableGroup()
    } else {
        $composer.startReplaceableGroup()
        Content3()
        $composer.endReplaceableGroup()
    }
    $composer.endReplaceableGroup()
}&lt;/pre&gt;
  &lt;h2 id=&quot;3QqG&quot;&gt;Нововведения&lt;/h2&gt;
  &lt;h3 id=&quot;YG9K&quot;&gt;Указание стабильности внешних типов&lt;/h3&gt;
  &lt;p id=&quot;qAB3&quot;&gt;С релизом Compose Compiler версии 1.5.5 появилась возможность явно &lt;a href=&quot;https://developer.android.com/jetpack/compose/performance/stability/fix#configuration-file&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;указывать стабильность внешних типов&lt;/u&gt;&lt;/a&gt;. Это нововведение позволяет избежать использования дополнительных обёрток для обеспечения стабильности. Рекомендуем добавить в список стабильных типов такие часто используемые классы, как стандартные коллекции из Kotlin и Painter. Это нужно указать в каждом модуле, где используется Compose. Указание коллекций особенно актуально для тех, кто предпочитает не использовать &lt;code&gt;immutable&lt;/code&gt; коллекции из-за нахождения их в альфа-версии или из-за необходимости переписывания большого объема кода.&lt;/p&gt;
  &lt;pre id=&quot;JKw7&quot; data-lang=&quot;kotlin&quot;&gt;// Все коллекции из котлин 
kotlin.collections.* 

// Painter 
androidx.compose.ui.graphics.painter.Painter&lt;/pre&gt;
  &lt;h3 id=&quot;rtub&quot;&gt;Режим сильной пропускаемости&lt;/h3&gt;
  &lt;p id=&quot;vSFc&quot;&gt;На данный момент &lt;a href=&quot;https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/compiler/design/strong-skipping.md&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;режим сильной пропускаемости&lt;/u&gt;&lt;/a&gt; является экспериментальным и активируется через специальный флаг. В перспективе он может стать настройкой по умолчанию. Что он даёт:&lt;/p&gt;
  &lt;ul id=&quot;yN61&quot;&gt;
    &lt;li id=&quot;vHFQ&quot;&gt;Все перезапускаемые (restartable) функции станут пропускаемыми (skippable). Для нестабильных параметров сравнение по экземплярам, для стабильных - через &lt;code&gt;equals&lt;/code&gt;.&lt;/li&gt;
    &lt;li id=&quot;vNsh&quot;&gt;Лямбды, захватывающие нестабильные переменные, будут тоже обёрнуты в &lt;code&gt;remember&lt;/code&gt;.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;h2 id=&quot;i4Gx&quot;&gt;Инструментарий&lt;/h2&gt;
  &lt;h3 id=&quot;EFM9&quot;&gt;Просмотр исходного кода Compose&lt;/h3&gt;
  &lt;p id=&quot;ClYY&quot;&gt;Каждый раз, когда вы сомневаетесь в том, как будет работать тот или иной код после компилятора Compose, стоит просто посмотреть финальный Java код. Хоть вы и можете отлично знать генерацию Compose-кода из Jetpack Compose Internals или множества статей, это не спасёт вас от устаревания информации там. Для этого есть удобный gradle-плагин - &lt;a href=&quot;https://github.com/takahirom/decomposer/&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;decomposer&lt;/u&gt;&lt;/a&gt;.&lt;/p&gt;
  &lt;h3 id=&quot;2dG6&quot;&gt;Vkcompose плагин&lt;/h3&gt;
  &lt;p id=&quot;RRsu&quot;&gt;Среди полезных инструментов есть &lt;a href=&quot;https://github.com/VKCOM/vkompose&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;плагины от VK для Kotlin и IDE&lt;/u&gt;&lt;/a&gt;, которые выполняют:&lt;/p&gt;
  &lt;ul id=&quot;Kbua&quot;&gt;
    &lt;li id=&quot;T7rh&quot;&gt;Подсветку нестабильных параметров и непропускаемых функций непосредственно в IDE.&lt;/li&gt;
    &lt;li id=&quot;KVFz&quot;&gt;Визуальное выделение происходящих рекомпозиций в UI с помощью цветных границ.&lt;/li&gt;
    &lt;li id=&quot;OZXs&quot;&gt;Логирование причин, по которым произошла рекомпозиция.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;h3 id=&quot;ViaP&quot;&gt;Detekt&lt;/h3&gt;
  &lt;p id=&quot;Lsxh&quot;&gt;Detekt, позволяет не только следить за стилем кода, но и защищать от не очень хороших практик, в том числе приводящих к проседанию производительности.&lt;/p&gt;
  &lt;ul id=&quot;Eyg4&quot;&gt;
    &lt;li id=&quot;7iQh&quot;&gt;&lt;a href=&quot;https://mrmans0n.github.io/compose-rules/rules/&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;compose-rules&lt;/u&gt;&lt;/a&gt; - большой список разнообразных правил.&lt;/li&gt;
    &lt;li id=&quot;ECSn&quot;&gt;&lt;a href=&quot;https://github.com/VKCOM/vkompose&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;vkcompose&lt;/u&gt;&lt;/a&gt; - кроме плагина предоставляет правило, которое проверяет функции на пропускаемость.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;h2 id=&quot;5Dlr&quot;&gt;Итог&lt;/h2&gt;
  &lt;p id=&quot;zsP4&quot;&gt;Подводя итог, мы разобрались, как избегать проблемы с начальной композицией и что не стоит перегружать Compose лишней логикой, ведь за удобство и простоту приходится платить. Однако благодаря неустанной работе разработчиков, производительность Compose значительно выросла, давая нам свободу сосредоточиться на других аспектах разработки. А экраны с огромным DAU и на View приходилось оптимизировать за гранью обычных приёмов. Для Compose это далеко не предел: &lt;a href=&quot;https://developer.android.com/jetpack/androidx/compose-roadmap&quot; target=&quot;_blank&quot;&gt;&lt;u&gt;будущие оптимизации&lt;/u&gt;&lt;/a&gt; сделают его ещё более мощным инструментом, идеально подходящим для любых задач.&lt;/p&gt;
  &lt;p id=&quot;Sotx&quot;&gt;&lt;a href=&quot;https://habr.com/ru/articles/796437/&quot; target=&quot;_blank&quot;&gt;Источник&lt;/a&gt;&lt;/p&gt;

</content></entry><entry><id>javalib:Vqsx8cuunR_</id><link rel="alternate" type="text/html" href="https://teletype.in/@javalib/Vqsx8cuunR_?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=javalib"></link><title>Интегрируем Kotlin сервис с AI чат-ботом с помощью Spring AI за 5 минут</title><published>2024-03-05T07:44:15.093Z</published><updated>2024-03-05T07:44:15.093Z</updated><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://img1.teletype.in/files/cb/6d/cb6d783a-10a4-4e85-bf65-6df1a569df9f.png"></media:thumbnail><category term="kotlin" label="Kotlin"></category><summary type="html">&lt;img src=&quot;https://img2.teletype.in/files/10/28/10283e96-f0ef-4cb3-90c4-56697e358ed4.png&quot;&gt;Чат-боты с генеративным искусственным интеллектом получили широкую известность после релиза ChatGPT в ноябре 2022 года. Сейчас вряд ли найдётся человек из IT, который не слышал про данный инструмент от OpenAI. Именно он вызвал настоящий бум в данной сфере, вынудив конкурентов разрабатывать свои аналоги, чтобы побороться за место на рынке. Таким образом созданная лавина изменений затронула многие языки программирования. Не обошли они и Java-сообщество. Spring Framework, один из наиболее популярных Java фреймворков обзавёлся модулем Spring AI, который обещает упростить разработку приложений с функциями ИИ.</summary><content type="html">
  &lt;figure id=&quot;koV6&quot; class=&quot;m_original&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/10/28/10283e96-f0ef-4cb3-90c4-56697e358ed4.png&quot; width=&quot;761&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;M2uq&quot;&gt;Чат-боты с генеративным искусственным интеллектом получили широкую известность после релиза ChatGPT в ноябре 2022 года. Сейчас вряд ли найдётся человек из IT, который не слышал про данный инструмент от OpenAI. Именно он вызвал настоящий бум в данной сфере, вынудив конкурентов разрабатывать свои аналоги, чтобы побороться за место на рынке. Таким образом созданная лавина изменений затронула многие языки программирования. Не обошли они и Java-сообщество. Spring Framework, один из наиболее популярных Java фреймворков обзавёлся модулем Spring AI, который обещает упростить разработку приложений с функциями ИИ.&lt;/p&gt;
  &lt;p id=&quot;zHMI&quot;&gt;Давайте вместе взглянем на него в деле и опробуем на демо проекте. В данном гайде мы создадим и подключим Kotlin сервис к чат-боту всего за пять минут, используя Spring AI!&lt;/p&gt;
  &lt;p id=&quot;TdEy&quot;&gt;Spring AI — это экспериментальный проект, цель которого — упростить разработку приложений с искусственным интеллектом (либо интеграцию с такими приложениями). На момент написания статьи актуальной является версия 0.8.1-SNAPSHOT. В неё входят следующие части:&lt;/p&gt;
  &lt;ul id=&quot;W4qy&quot;&gt;
    &lt;li id=&quot;l0Ov&quot;&gt;&lt;strong&gt;Embeddings API&lt;/strong&gt;. В терминах ML эмбеддинг (embedding) означает векторное представление каких-либо данных. Эмбеддинги позволяют преобразовать информацию, которую понимаем мы, в информацию, которую понимает компьютер. В том же NLP они используются, чтобы компьютер мог анализировать и/или преобразовывать текст (переводить, извлекать смысл, перефразировать частично или полностью). Embeddings API позволяет преобразовывать текст в векторы.&lt;/li&gt;
    &lt;li id=&quot;5EfQ&quot;&gt;&lt;strong&gt;Chat Completion API&lt;/strong&gt;. Данное API используется для взаимодействия с AI чат-ботами. Уже есть клиенты для OpenAI, Ollama, Microsoft Azure, HuggingFace, Google Vertex, Amazon Bedrock.&lt;/li&gt;
    &lt;li id=&quot;USGi&quot;&gt;&lt;strong&gt;Function API&lt;/strong&gt;. У моделей OpenAI есть интересная &lt;a href=&quot;https://platform.openai.com/docs/guides/function-calling&quot; target=&quot;_blank&quot;&gt;фича &lt;/a&gt;— регистрация функций. Пользователь описывает, какие входные параметры принимает его функция и модель может вывести JSON, который можно в неё передать.Это упрощает составление запросов к модели и парсинг ответов. Модель по контексту будет понимать, что вы хотите и выдавать данные в необходимом формате. Function API позволяет работать с этой фичей — регистрировать функции, описывать условия вызова и т.д.&lt;/li&gt;
    &lt;li id=&quot;SKc6&quot;&gt;&lt;strong&gt;Image Generation API&lt;/strong&gt;. API для взаимодействия с моделями, заточенными на генерирование изображений. Есть готовые клиенты для OpenAI (DALL·E) и Stability AI (Stable Diffusion).&lt;/li&gt;
    &lt;li id=&quot;bBdE&quot;&gt;&lt;strong&gt;Prompts&lt;/strong&gt;. Промпт (prompt) — входной текст, на основе которого модель генерирует контент. В зависимости от того, с какой моделью вы взаимодействуете, будет менятся и структура промта. В Stable Diffusion, например, есть позитивный промпт (что мы хотим получить), и негативный промпт (что мы не хотим получить), а входной текст чаще всего представляет собой набор ключевых слов (masterpiece, best quality, photo и т.д.). А в ChatGPT промпт представлен как инструкция и ассоциируется с определённой ролью. (подробнее можно почитать &lt;a href=&quot;https://platform.openai.com/docs/guides/text-generation&quot; target=&quot;_blank&quot;&gt;тут&lt;/a&gt;). Модуль Prompts содержит классы для работы с промптами (шаблоны, интерфейсы, утилиты).&lt;/li&gt;
    &lt;li id=&quot;Mjwl&quot;&gt;&lt;strong&gt;Output Parsers&lt;/strong&gt;. Получаемые от модели данные нужно поместить в Java классы, для этого можно использовать готовые парсеры или создать свой на основе интерфейса OutputParser.&lt;/li&gt;
    &lt;li id=&quot;b9jL&quot;&gt;&lt;strong&gt;Vector Databases&lt;/strong&gt;. Векторая база данных — это база данных, которая заточена под хранение данных в векторном представлении. Она нужна для хранения данных, которые потом будут использоваться AI моделью. Одной из ключевых особенностей векторной базы данных является поиск данных по сходству, когда мы можем найти в базе векторы, наиболее похожие на заданный. Этот поиск играет важную роль в рекомендательных системах, распознавании изображений, языковой обработке. В Spring AI для взаимодействия с векторными базами данных используется интерфейс VectorStore. В настоящий момент есть реализации данного интерфейса для Weaviate, Redis, PineCone, pgvector, Neo4j, Milvus, Chroma, Azure.&lt;/li&gt;
    &lt;li id=&quot;AUMC&quot;&gt;&lt;strong&gt;ETL Pipeline&lt;/strong&gt;. ETL расшифровывается как &amp;quot;extract, transform, load&amp;quot; (извлечь, преобразовать, загрузить). Это процесс, который нужен для подготовки данных для BI-систем, датасетов для тренировки моделей ИИ, долгосрочного хранения и пр. Spring AI позволяет создавать ETL пайплайны для чтения данных в &amp;quot;сыром&amp;quot; виде, преобразования их в векторы и загрузка в векторную базу данных.&lt;/li&gt;
    &lt;li id=&quot;VFVg&quot;&gt;&lt;strong&gt;Generic Model API&lt;/strong&gt;. Данный модуль включает в себя набор интерфейсов для взаимодействия с AI моделями. Основная цель — упростить и стандартизировать поддержку новых AI моделей, которые будут добавлять в Spring AI.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;wwXW&quot;&gt;В рамках данной статьи, используя Spring AI, мы создадим простой Spring сервис для анализа настроения пользователя. Алгоритм его работы достаточно прост:&lt;/p&gt;
  &lt;figure id=&quot;JRnn&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/950/5e3/fd0/9505e3fd06021b581ba7d98da2a81ab4.png&quot; width=&quot;508&quot; /&gt;
    &lt;figcaption&gt;Пользователем может быть как человек, так и ПО&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;7oYr&quot;&gt;Для создания проекта будут использованы следующие инструменты:&lt;/p&gt;
  &lt;ul id=&quot;V9I2&quot;&gt;
    &lt;li id=&quot;ZhJz&quot;&gt;Kotlin версии 1.9.22.&lt;/li&gt;
    &lt;li id=&quot;GAKl&quot;&gt;Gradle версии 8.2.&lt;/li&gt;
    &lt;li id=&quot;3Nr8&quot;&gt;Spring (в том числе модуль Spring AI).&lt;/li&gt;
    &lt;li id=&quot;iXfo&quot;&gt;Ollama (инструмент для запуска языковых моделей). В данном примере я буду использовать модель OpenChat. Она поддерживает русский язык и даёт неплохой результат для моих скромных вычислительных ресурсов.&lt;/li&gt;
    &lt;li id=&quot;7WyI&quot;&gt;Postman (для тестирования).&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;Bs3L&quot;&gt;Для начала потребуется создать наш Spring проект. Можно воспользоваться &lt;a href=&quot;https://start.spring.io/&quot; target=&quot;_blank&quot;&gt;Spring Initializr&lt;/a&gt; или сделать это вручную. Не забудьте добавить зависимость для Spring AI, указанную ниже&lt;/p&gt;
  &lt;pre id=&quot;01AJ&quot; data-lang=&quot;kotlin&quot;&gt;implementation(&amp;quot;org.springframework.ai:spring-ai-ollama-spring-boot-starter:0.8.1-SNAPSHOT&amp;quot;)&lt;/pre&gt;
  &lt;p id=&quot;XAcN&quot;&gt;Далее набросаем базовые классы.&lt;br /&gt;Контроллер:&lt;/p&gt;
  &lt;pre id=&quot;w1kb&quot; data-lang=&quot;kotlin&quot;&gt;@RestController
class ChatController(
    private val chatService: ChatService
) {
    @PostMapping(&amp;quot;/chat&amp;quot;)
    fun sendMessage(@RequestBody message: String): String {
        return chatService.exchange(message)
    }
}&lt;/pre&gt;
  &lt;p id=&quot;Ma6y&quot;&gt;Сервис:&lt;/p&gt;
  &lt;pre id=&quot;RawA&quot; data-lang=&quot;kotlin&quot;&gt;@Service
class ChatService(
    private val client : OllamaApiClient
) {
    private val outputParser = BeanOutputParser(SentimentAnalysisResult::class.java)

    fun exchange(message: String): String {
        return client.sendMessage(message)
    }
}&lt;/pre&gt;
  &lt;p id=&quot;pTCx&quot;&gt;Далее создадим OllamaApiClient. В Spring AI уже есть реализация ChatClient для Ollama, поэтому будем использовать её для взаимодействия с моделью.&lt;/p&gt;
  &lt;pre id=&quot;9ES3&quot; data-lang=&quot;kotlin&quot;&gt;@Component
class OllamaApiClient(
    properties: OllamaClientProperties
) {
    private final var client = OllamaChatClient(OllamaApi(properties.url))

    init {
        val options = OllamaOptions.create()
        options.model = properties.model
    }

    fun sendMessage(message: String): String {
        return client.call(message)
    }
}&lt;/pre&gt;
  &lt;p id=&quot;GEy7&quot;&gt;OllamaClientProperties:&lt;/p&gt;
  &lt;pre id=&quot;GBt8&quot; data-lang=&quot;kotlin&quot;&gt;@ConfigurationProperties(prefix = &amp;quot;ai.ollama&amp;quot;)
data class OllamaClientProperties(
    val url: String,
    val model: String
)&lt;/pre&gt;
  &lt;p id=&quot;qcoH&quot;&gt;Адрес и имя модели указываем в application.yml.&lt;br /&gt;После первоначальной настройки проверяем корректность указанных параметров и отправляем запрос на наш эндпоинт:&lt;/p&gt;
  &lt;figure id=&quot;Nq51&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/r/w1560/getpro/habr/upload_files/388/144/6cb/3881446cb66f513368829d1adc2bbd7f.png&quot; width=&quot;869&quot; /&gt;
    &lt;figcaption&gt;Чем дольше я смотрю на этот ответ, тем меньше я хочу доверить ему написание статьи&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;APNU&quot;&gt;Теперь нам нужно попросить чат-бота оценивать настроение нашего сообщения. Для этого нужно добавлять дополнительную информацию к каждому запросу. Сделать это можно с помощью PromptTemplate:&lt;/p&gt;
  &lt;pre id=&quot;ggGo&quot; data-lang=&quot;kotlin&quot;&gt;private val promptTemplate = PromptTemplate(&amp;quot;&amp;quot;&amp;quot;
        Какое настроение у следующего текста:&amp;#x27;{text}&amp;#x27;? 
        Оно позитивное, негативное или нейтральное? 
        Укажи степень уверенности с помощью числа от 0.0 до 1.0.&amp;quot;
    &amp;quot;&amp;quot;&amp;quot;)
fun exchange(message: String): String {
        val prompt = promptTemplateRus.create(mapOf(&amp;quot;text&amp;quot; to message))
        val generation = client.sendMessage(prompt)
        return generation.output.content.toString()
    }&lt;/pre&gt;
  &lt;p id=&quot;OrfC&quot;&gt;В зависимости от модели, которую вы используете, шаблон можно корректировать исходя из получаемых результатов.&lt;br /&gt;Поскольку PromptTemplate возвращает объект типа Prompt, нам нужно изменить и метод sendMessage:&lt;/p&gt;
  &lt;pre id=&quot;N1FO&quot; data-lang=&quot;kotlin&quot;&gt;fun sendMessage(prompt: Prompt): Generation {
        val response = client.call(prompt)
        return response.result
    }&lt;/pre&gt;
  &lt;p id=&quot;kjnD&quot;&gt;Повторяем запрос и получаем такой результат:&lt;/p&gt;
  &lt;figure id=&quot;pquq&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/getpro/habr/upload_files/e99/778/ed1/e99778ed104ad52f0241667fc1a09eb2.PNG&quot; width=&quot;698&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;SoTa&quot;&gt;Результат неплохой, но его можно сделать лучше. Сейчас наше API отдаёт ответ в виде простого текста. Такой результат придётся парсить, плюс, модель может перефразировать сообщение в зависимости от запроса, что делает это ещё неудобнее для использования. Поэтому настроим форматирование выводимых данных. Модифицируем наш код следующим образом:&lt;/p&gt;
  &lt;p id=&quot;rxdm&quot;&gt;Создадим data класс для маппинга данных:&lt;/p&gt;
  &lt;pre id=&quot;azLp&quot; data-lang=&quot;kotlin&quot;&gt;data class SentimentAnalysisResult(
    val text: String = &amp;quot;&amp;quot;,
    val sentiment: String = &amp;quot;&amp;quot;,
    val confidence: String = &amp;quot;&amp;quot;
)&lt;/pre&gt;
  &lt;p id=&quot;Id1P&quot;&gt;Также добавим Output Parser чтобы сразу &amp;quot;упаковать&amp;quot; ответ в data класс:&lt;/p&gt;
  &lt;pre id=&quot;Kp8B&quot; data-lang=&quot;kotlin&quot;&gt;private val promptTemplate = PromptTemplate(&amp;quot;&amp;quot;&amp;quot;
        Какое настроение у следующего текста:&amp;#x27;{text}&amp;#x27;? 
        Оно позитивное, негативное или нейтральное? 
        Укажи степень уверенности с помощью числа от 0.0 до 1.0.{format}&amp;quot;
    &amp;quot;&amp;quot;&amp;quot;)
    private val outputParser = BeanOutputParser(SentimentAnalysisResult::class.java)

    fun exchange(message: String): SentimentAnalysisResult {
        val prompt = promptTemplate.create(mapOf(&amp;quot;text&amp;quot; to message, &amp;quot;format&amp;quot; to outputParser.format))
        val answer = client.sendMessage(prompt)
        return outputParser.parse(answer.output.content)
    }&lt;/pre&gt;
  &lt;p id=&quot;xJZC&quot;&gt;Теперь наш запрос будет иметь следующий вид:&lt;/p&gt;
  &lt;figure id=&quot;8dnI&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/getpro/habr/upload_files/a7c/c1a/01b/a7cc1a01b85b201d091e9e34bdb1f119.PNG&quot; width=&quot;706&quot; /&gt;
    &lt;figcaption&gt;Уверенность в ответе всё возрастает&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;aRSX&quot;&gt;Давайте проверим, что наша программа правильно определяет заданное настроение:&lt;/p&gt;
  &lt;figure id=&quot;zNIL&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/getpro/habr/upload_files/aa3/e17/6bb/aa3e176bb85bfe4f92ca08aca83c7905.PNG&quot; width=&quot;275&quot; /&gt;
    &lt;figcaption&gt;Нейтральный запрос&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;figure id=&quot;0GVq&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/getpro/habr/upload_files/9e4/3b2/9ce/9e43b29ce826be00e354caadb34139b4.PNG&quot; width=&quot;302&quot; /&gt;
    &lt;figcaption&gt;Позитивный запрос&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;figure id=&quot;FfOY&quot; class=&quot;m_custom&quot;&gt;
    &lt;img src=&quot;https://habrastorage.org/getpro/habr/upload_files/5ee/ae3/dd7/5eeae3dd77d1960bf9ba89b1a9a84507.PNG&quot; width=&quot;263&quot; /&gt;
    &lt;figcaption&gt;Негативный запрос&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;eGY6&quot;&gt;Результат соответствует ожиданиям! Далее, в зависимости от вашего проекта, можно настраивать модель, менять API сервиса или шаблоны для запросов.&lt;/p&gt;
  &lt;p id=&quot;2U1b&quot;&gt;В завершающей части хотел бы рассказать про тюнинг модели. OllamaChatClient позволяет настроить широкий спектр параметров модели, вот некоторые из них:&lt;/p&gt;
  &lt;ul id=&quot;dJjo&quot;&gt;
    &lt;li id=&quot;RS9v&quot;&gt;&lt;strong&gt;temperature&lt;/strong&gt;. Параметр креативности модели. Чем выше значение, тем ответы более необычные.&lt;/li&gt;
    &lt;li id=&quot;Brnj&quot;&gt;&lt;strong&gt;frequencyPenalty&lt;/strong&gt;. &amp;quot;Наказание&amp;quot; модели за повторения. Меньше значение — больше одинаковых словесных конструкций и выражений, меньше уникальных слов.&lt;/li&gt;
    &lt;li id=&quot;V1Bk&quot;&gt;&lt;strong&gt;presencePenalty&lt;/strong&gt;. &amp;quot;Наказание&amp;quot; модели за использование одних и тех же токенов в сгенерированном тексте. Чем меньше число, тем чаще могут попадаться одни и те же слова.&lt;/li&gt;
    &lt;li id=&quot;EaUY&quot;&gt;&lt;strong&gt;topK&lt;/strong&gt;. У модели есть определенное количество вариантов, как можно продолжить генерируемый текст. Данный параметр ограничивает количество доступных токенов для продолжения. Меньше число — меньше возможных опций.&lt;/li&gt;
    &lt;li id=&quot;ocT6&quot;&gt;&lt;strong&gt;topP&lt;/strong&gt;. Ограничение кумулятивной вероятности возможных токенов. Работает схожим образом с topK. Есть набор возможных токенов, которыми модель может продолжить текст. У каждого токена есть определённая вероятность, что он будет следующим. Токены добавляются в некий пул возможных токенов, где их вероятности складываются. Когда вероятность превысит P, больше токены не будут добавляться и модель будет выбирать токен из добавленных.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p id=&quot;FJgl&quot;&gt;Из-за того, что сообщение модели подстраивается под указанный формат, нам не сильно важна креативность модели, повторяет ли она одни и те же слова или нет, ведь мы этого не увидим. Единственный параметр, который сильно влияет на выводимые данных — это topP.&lt;br /&gt;Опытным путём было определено, что высокий topP приводит к тому, что модель постоянно меняет уровень уверенности в своём ответе. Для наших целей это нежелательно (иначе проще просто генерировать случайное число самим). Поэтому topP стоит сделать 0.25 или ниже. По остальным параметрам выбор не принципиален и не повлияет на оценку текста (по крайней мере для выбранной мной модели).&lt;/p&gt;
  &lt;p id=&quot;6vDY&quot;&gt;Внесём итоговые правки в код.&lt;br /&gt;application.yml&lt;/p&gt;
  &lt;pre id=&quot;nVCH&quot; data-lang=&quot;yaml&quot;&gt;ai:
  ollama:
    url: &amp;quot;http://localhost:11434&amp;quot;
    options:
      model: &amp;quot;openchat&amp;quot;
      temperature: 0.8f
      top_k: 100
      top_p: 0.25f
      frequency_penalty: 0f
      presence_penalty: 0f&lt;/pre&gt;
  &lt;p id=&quot;0vbR&quot;&gt;Класс конфигурации&lt;/p&gt;
  &lt;pre id=&quot;ZXUJ&quot; data-lang=&quot;kotlin&quot;&gt;@ConfigurationProperties(prefix = &amp;quot;ai.ollama&amp;quot;)
data class OllamaClientProperties(
    val url: String,
    val options: OllamaOptions
)

data class OllamaOptions(
    val model: String,
    val temperature: Float,
    val topK: Int,
    val topP: Float,
    val frequencyPenalty: Float,
    val presencePenalty: Float
)&lt;/pre&gt;
  &lt;p id=&quot;jbGw&quot;&gt;Конструктор для OllamaApiClient&lt;/p&gt;
  &lt;pre id=&quot;VU6Z&quot; data-lang=&quot;kotlin&quot;&gt;init {
        val options = OllamaOptions.create()
        options.model = properties.options.model
        options.temperature = properties.options.temperature
        options.topK = properties.options.topK
        options.topP = properties.options.topP
        options.frequencyPenalty = properties.options.frequencyPenalty
        options.presencePenalty = properties.options.presencePenalty
        client.withDefaultOptions(options)
    }&lt;/pre&gt;
  &lt;hr /&gt;
  &lt;p id=&quot;VeSY&quot;&gt;Вот и подошёл к концу гайд. В нём мы рассмотрели базовые компоненты Spring AI, связанные с интеграцией AI чат-бота в ваше приложение. Конечно, Spring AI не ограничивается представленными возможностями, это обширный проект, к которому регулярно выходят обновления. Получит ли он широкое распространение, неизвестно, но попробовать его в действии как минимум интересно, а, возможно, оно пригодиться не только для пет-проектов.&lt;/p&gt;
  &lt;p id=&quot;kMHL&quot;&gt;Исходники проекта вы можете посмотреть в &lt;a href=&quot;https://github.com/ullaes/sentiment-analysis&quot; target=&quot;_blank&quot;&gt;репозитории&lt;/a&gt;&lt;/p&gt;
  &lt;p id=&quot;Zf6V&quot;&gt;&lt;strong&gt;Источники&lt;/strong&gt;&lt;/p&gt;
  &lt;p id=&quot;A0Zh&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-ai/reference/index.html&quot; target=&quot;_blank&quot;&gt;Документация Spring AI&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://github.com/ollama/ollama&quot; target=&quot;_blank&quot;&gt;Ollama&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://medium.com/@daniel.puenteviejo/the-science-of-control-how-temperature-top-p-and-top-k-shape-large-language-models-853cb0480dae&quot; target=&quot;_blank&quot;&gt;Про настройку модели&lt;/a&gt;&lt;/p&gt;
  &lt;p id=&quot;nqau&quot;&gt;&lt;a href=&quot;https://habr.com/ru/articles/796855/&quot; target=&quot;_blank&quot;&gt;Источник&lt;/a&gt;&lt;/p&gt;

</content></entry></feed>