Команда Spring АйО перевела статью про новую версию AOT-репозиториев, которые позволяют генерировать реализацию методов запросов на этапе сборки. Это ускоряет запуск приложений, снижает потребление памяти и делает поведение репозиториев более прозрачным для разработчиков.


За последние несколько лет экосистема Java активно инвестирует в сокращение времени запуска приложений. Основное внимание уделяется оптимизациям, выполняемым Ahead-of-Time. Это может быть преобразование кода в нативный исполняемый файл с помощью GraalVM, использование уже оптимизированного байткода с помощью Coordinated Restore at Checkpoint (CRaC), технология Class Data Sharing (CDS) или её более современный аналог AOT cache — часть проекта Leyden. Несмотря на различия в пороге вхождения, все эти подходы переносят оптимизацию производительности с момента выполнения на более ранние стадии, такие как на этап сборки.

Spring Framework предоставляет поддержку в любом из этих направлений. Он помогает вам использовать выбранную стратегию, предлагая:

  • AOT Runtime Hints для создания нативных образов с помощью GraalVM

  • Генерацию AOT-кода для создания и autowiring-а бинов

  • Снапшоты контекста приложения с помощью тренировочных запусков premain из проекта Leyden

С выпуском Spring Data 4.0 (или версии 2025.1, если вы придерживаетесь календарного версионирования) мы анонсируем поддержку AOT репозиториев. Все операции подготовки репозиториев, которые раньше выполнялись при запуске приложения, теперь будут перенесены на этап сборки.

Как это работает и чего ожидать?

В двух словах: при установке свойства конфигурации spring.aot.repositories.enabled=true наш процесс AOT-преобразования превращает методы запросов в репозитории в полноценный исходный код, опираясь на специфику хранилища. Сгенерированные методы содержат тот же код, который вы бы написали вручную, не используя Spring Data для выполнения запросов. Этот исходный код затем компилируется вместе с остальным приложением и реализует интерфейс репозитория.

Представим себе репозиторий владельцев домашних животных, как в примере ниже.

Интерфейс репозитория сам по себе не наследует функциональность от базовых интерфейсов, таких как CrudRepository. Это сделано намеренно, чтобы минимизировать пример и сфокусироваться на АОТ репозиториях. Однако, обратите внимание, что метод save соответствует сигнатуре одного из предопределённых (речь про CrudRepository) методов, а два других метода не соответствуют каким-либо pre-pefined операциям — один реализован через имя метода (derived query), а другой с использованием @Query аннотации.

interface OwnerRepository extends Repository<Owner, Integer> {

    Owner save(Owner owner);

    List<OwnerSummary> findAllByLastName(String lastName);

    @Transactional(readOnly = true)
    @Query("SELECT DISTINCT owner FROM Owner owner left join owner.pets WHERE owner.lastName LIKE :lastName%")

    Page<Owner> findByLastName(@Param("lastName") String lastName, Pageable pageable);
    // ...
}

Во время этапа AOT-преобразования инфраструктура анализирует только те части, которые действительно требуют генерации кода. Возьмём, к примеру, ранее упомянутый метод save: поскольку в данном случае используется JPA, базовая реализация SimpleJpaRepository уже содержит дефолтную реализацию этого метода, поэтому генерация кода для него просто не требуется. То же самое касается и любых ваших собственных реализаций методов.

Комментарий от эксперта команды Spring АйО, Spring Data контрибьютора, Михаила Поливахи

Здесь Christorh не поясняет важную деталь. На самом деле, для Вашего OwnerRepository все равно будет выполняться проксирование, т.к. сгенерированная AOT реализация на этапе билда не содержит реализации для всех Ваших методов, и инжектив бин репозитория, вы все равно будете иметь дело с прокси.

Сгенерированная реализация может не содержать конкретного метода по разным причинам: 

- Как в примере метод, который вы определяете, может уже иметь совместимый аналог в дефолтной реализации репозитория, которую из коробки Вам предоставляет Spring Data. Например, в случае Spring Data JPA, дефолтная реализация JpaRepository репозитория это SimpleJpaRepository

- Также, в особо сложных случаях, Spring Data пока не умеет генерировать реализацию для методов. Это тема отдельной статьи. Такие методы будут исполняться по-старому.

- Есть еще угловые случаи, такие как Fragment Repositories и т.д, когда Spring Data вынуждена выступать как роутер запроса, чтобы понять, в какой репозиторий проксировать invocation.

Оставшиеся два метода из OwnerRepository, напротив, попадают под AOT-оптимизацию. Для них будет сгенерирован код, который окажется в классе OwnerRepositoryImpl__Aot, расположенном в том же пакете, что и исходный интерфейс OwnerRepository.

@Generated
public class OwnerRepositoryImpl__Aot extends AotRepositoryFragmentSupport {

  private final EntityManager entityManager;

  public OwnerRepositoryImpl__Aot(EntityManager entityManager,

    RepositoryFactoryBeanSupport.FragmentCreationContext context) {

    // ...

  }

  /**
   * AOT generated implementation of {@link OwnerRepository#findAllByLastName(String)}.
   */
  public List<OwnerSummary> findAllByLastName(String lastName) {

    String queryString = "SELECT o.firstName AS firstName, o.lastName AS lastName, o.city AS city FROM org.springframework.samples.petclinic.owner.Owner o WHERE o.lastName = :lastName";

    Query query = this.entityManager.createQuery(queryString, Tuple.class);
    query.setParameter("lastName", lastName);
    return (List<OwnerSummary>) convertMany(query.getResultList(), false, OwnerSummary.class);
  }

  /**
   * AOT generated implementation of {@link OwnerRepository#findByLastName(String,Pageable)}.
   */

  public Page<Owner> findByLastName(String lastName, Pageable pageable) {
    String queryString = "SELECT DISTINCT owner FROM Owner owner left join owner.pets WHERE owner.lastName LIKE :lastName";
    String countQueryString = "SELECT count(DISTINCT owner) FROM Owner owner left join owner.pets WHERE owner.lastName LIKE :lastName";

    if (pageable.getSort().isSorted()) {
      DeclaredQuery declaredQuery = DeclaredQuery.jpqlQuery(queryString);
      queryString = rewriteQuery(declaredQuery, pageable.getSort(), Owner.class);
    }

    Query query = this.entityManager.createQuery(queryString);
    query.setParameter("lastName", "%s%%".formatted(lastName));

    if (pageable.isPaged()) {
      query.setFirstResult(Long.valueOf(pageable.getOffset()).intValue());
      query.setMaxResults(pageable.getPageSize());
    }

    LongSupplier countAll = () -> {
      Query countQuery = this.entityManager.createQuery(countQueryString);
      countQuery.setParameter("lastName", "%s%%".formatted(lastName));
      return (Long) countQuery.getSingleResult();
    };

    return PageableExecutionUtils.getPage((List<Owner>) query.getResultList(), pageable, countAll);
  }
}

Как можно заметить, сгенерированный код может быть как довольно простым, так и достаточно сложным — всё зависит от самого запроса, привязки параметров, а также от запрашиваемых данных и их представления.

При запуске приложения классы, созданные на этапе AOT, подключаются к составу репозитория, лежащего в основе прокси, который реализует интерфейс репозитория. Это означает, что теперь вы впервые можете увидеть и перейти внутрь кода, который действительно выполняется при вызове метода интерфейса репозитория.

Помимо улучшенной отладки, предварительно сгенерированный код облегчает разбор запросов и позволяет лучше понять допущения, заложенные в реализации. Он также сокращает цепочку вызовов во время инициализации репозитория, что приводит к более быстрому запуску приложения в целом и снижению потребления памяти.

В зависимости от используемого хранилища данных эффект от таких оптимизаций может быть весьма ощутимым. Например, в случае с Spring Data JPA это даёт дополнительное ускорение запуска и снижение потребления памяти — в дополнение к уже существующим AOT-оптимизациям.

Репозитории с предварительной компиляцией (Ahead-of-Time) на данный момент доступны в preview. В первой итерации поддерживаются JPA (только через Hibernate) и MongoDB. В будущих релизах планируется расширение поддержки и для других модулей.


Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

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