Открыть список
Как стать автором
Обновить

Как расширить Spring своим типом Repository на примере Infinispan

Java
Из песочницы

Зачем об этом писать?

Это моя первая статья, в ней я попытаюсь описать полученный мною практический опыт работы со Spring Repository под капотом фреймворка. Готовых статей про эту тему я в интернете не нашёл ни на русском, ни на английском, были только несколько репозиториев исходников на github, ну и исходники самого Spring. Поэтому и решил, почему бы не написать, вдруг тема написания своих типов репозиториев для Spring для кого-то ещё актуальна.

Программирование для Infinispan я не буду рассматривать подробно, детали реализации всегда можно посмотреть в исходниках, указанных в конце статьи. Основной упор сделан именно на сопряжение механизма Spring Boot Repository и нового типа репозитория.

С чего всё начиналось

В ходе работы на одном из проектов у одного из архитектора возникла идея, что можно написать свои типы репозиториев по аналогии, как это сделано в разных модулях Spring (например, JPARepository, KeyValueRepository, CassandraRepository и т.п.). В качестве пробной реализации решили выбрать работу с данными через Infinispan.

Естественно, что архитекторы - люди занятые, поэтому реализовывать идею поручили Java разработчику, т.е. мне.

Когда я начал прорабатывать тему в интернете, то Google упорно выдавал почти одни статьи про то, как замечательно использовать JPARepository во всех видах на тривиальных примерах. По KeyValueRepository информации было ещё меньше. На StackOverFlow печальные никем не отвеченные вопросы по подобной теме. Делать нечего, пришлось лезть в исходники Spring.

Infinispan

Если говорить кратко про Infinispan, то это всего лишь распределённое хранилище данных в виде ключ-значение, и всё это постоянно висит закэшированное в памяти. Перегружаем Infinispan, данные все обнуляются.

Было решено, что наиболее подходящий кандидат для исследования - KeyValueRepository, как самый близкий к данной области, уже реализованный в Spring. Вся разница только в том, что вместо Infinispan (на его месте мог быть и Hazelcast, например), как хранилища данных, в KeyValueRepository обычный ConcurrentHashMap.

Реализация

Чтобы в Spring проекте подключить возможность пользоваться репозиторием для хранилища ключ-значение пользуются аннотацией EnableMapRepositories.

@SpringBootApplication
@EnableMapRepositories("my.person.package.for.entities")
public class Application {

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }

}

Можем практически полностью скопировать содержимое кода данной аннотации и создать свою EnableInfinispanRepositories.

Чтобы каждый раз это не писать, скажу, что слово map мы всегда заменяем на infinispan, в аналогичных реализациях, скрытых спойлерами.

Код аннотации EnableInfinispanRepositories
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(InfinispanRepositoriesRegistrar.class)
public @interface EnableInfinispanRepositories {

  String[] value() default {};

  String[] basePackages() default {};

  Class<?>[] basePackageClasses() default {};

  ComponentScan.Filter[] excludeFilters() default {};

  ComponentScan.Filter[] includeFilters() default {};

  String repositoryImplementationPostfix() default "Impl";

  String namedQueriesLocation() default "";

  QueryLookupStrategy.Key queryLookupStrategy() default 
    QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND;

  Class<?> repositoryFactoryBeanClass() default 
    KeyValueRepositoryFactoryBean.class;

  Class<?> repositoryBaseClass() default 
    DefaultRepositoryBaseClass.class;

  String keyValueTemplateRef() default "infinispanKeyValueTemplate";

  boolean considerNestedRepositories() default false;

}

Если посмотреть что происходит в коде аннотации EnableMapRepositories, то увидим, что там импортируется класс, который и делает всю магию по активации данного типа репозитория.

@Import(MapRepositoriesRegistrar.class)
public @interface EnableMapRepositories {
}

Ниже код MapRepositoriesRegistar.

public class MapRepositoriesRegistrar extends 
  RepositoryBeanDefinitionRegistrarSupport {

  @Override
  protected Class<? extends Annotation> getAnnotation() {
    return EnableMapRepositories.class;
  }
  
  @Override
  protected RepositoryConfigurationExtension getExtension() {
    return new MapRepositoryConfigurationExtension();
  }
}

В коде перегружаются два метода. В одном связывается Registar со своей активирующей аннотацией, чтобы потом из неё получить заполненные атрибуты конфигурации. В другом находится реализация хранилища данных, специфичных для данного типа репозитория.

Сделаем по аналогии свой InfinispaRepositoriesRegistar.
@NoArgsConstructor
public class InfinispanRepositoriesRegistrar extends 
  RepositoryBeanDefinitionRegistrarSupport {

  @Override
  protected Class<? extends Annotation> getAnnotation() {
    return EnableInfinispanRepositories.class;
  }

  @Override
  protected RepositoryConfigurationExtension getExtension() {
    return new InfinispanRepositoryConfigurationExtension();
  }
}

Теперь посмотрим, как же выглядит сама реализация хранилища.

public class MapRepositoryConfigurationExtension extends 
  KeyValueRepositoryConfigurationExtension {

  @Override
  public String getModuleName() {
    return "Map";
  }

  @Override
  protected String getModulePrefix() {
    return "map";
  }

  @Override
  protected String getDefaultKeyValueTemplateRef() {
    return "mapKeyValueTemplate";
  }

  @Override
  protected AbstractBeanDefinition getDefaultKeyValueTemplateBeanDefinition(RepositoryConfigurationSource configurationSource) {
    BeanDefinitionBuilder adapterBuilder = BeanDefinitionBuilder
      .rootBeanDefinition(MapKeyValueAdapter.class);
    adapterBuilder.addConstructorArgValue(
      getMapTypeToUse(configurationSource));
    BeanDefinitionBuilder builder = BeanDefinitionBuilder
      .rootBeanDefinition(KeyValueTemplate.class);
    ...
  }
  ...
}

В MapKeyValueAdapter будет реализована самая специфическая часть, характерная именно для локального хранения кэша в HashMap. А вот KeyValueTemplate оборачивает методы адаптера довольно общим кодом.

Поэтому чтобы выполнить задачу и заменить хранение данных с локального кэша на распределённое хранилище Infinispan, нужно сделать аналогичный ConfigurationExtension, но заменить на специфичный адаптер, где и будет реализована вся логика чтения/записи/поиска данных, характерная для Infinispan.

Реализация InfinispanRepositoriesConfigurationExtension
@NoArgsConstructor
public class InfinispanRepositoryConfigurationExtension 
  extends KeyValueRepositoryConfigurationExtension {

  @Override
  public String getModuleName() {
    return "Infinispan";
  }

  @Override
  protected String getModulePrefix() {
    return "infinispan";
  }

  @Override
  protected String getDefaultKeyValueTemplateRef() {
    return "infinispanKeyValueTemplate";
  }

  @Override
  protected Collection<Class<?>> getIdentifyingTypes() {
    return Collections.singleton(InfinispanRepository.class);
  }

  @Override
  protected AbstractBeanDefinition getDefaultKeyValueTemplateBeanDefinition(RepositoryConfigurationSource configurationSource) {
    RootBeanDefinition infinispanKeyValueAdapterDefinition = 
      new RootBeanDefinition(InfinispanKeyValueAdapter.class);
    RootBeanDefinition keyValueTemplateDefinition = 
      new RootBeanDefinition(KeyValueTemplate.class);
    ConstructorArgumentValues constructorArgumentValuesForKeyValueTemplate = new ConstructorArgumentValues();
    constructorArgumentValuesForKeyValueTemplate
      .addGenericArgumentValue(infinispanKeyValueAdapterDefinition);
    keyValueTemplateDefinition.setConstructorArgumentValues(
      constructorArgumentValuesForKeyValueTemplate);
    return keyValueTemplateDefinition;
  }
}

Нужно обязательно в нашем ConfigurationExtension ещё перегрузить метод getIdentifyingTypes(), чтобы в нём сослаться на наш новый тип репозитория (см. реализацию выше).

@NoRepositoryBean
public interface InfinispanRepository <T, ID> extends 
  PagingAndSortingRepository<T, ID> {
}

Чтобы окончательно всё заработало, нужно сконфигурировать KeyValueTemplate, подсунув ему наш адаптер.

@Configuration
public class InfinispanConfiguration extends CachingConfigurerSupport {

  @Autowired
  private ApplicationContext applicationContext;

  @Bean
  public InfinispanKeyValueAdapter getInfinispanAdapter() {
    return new InfinispanKeyValueAdapter(
      applicationContext.getBean(CacheManager.class)
    );
  }

  @Bean("infinispanKeyValueTemplate")
  public KeyValueTemplate getInfinispanKeyValueTemplate() {
    return new KeyValueTemplate(getInfinispanAdapter());
  }
}

На этом всё.

Можно, конечно, копать глубже и не пользоваться готовыми Spring-овыми реализациями для репозиториев, а наследоваться исключительно от их абстрактных классов и интерфейсов, но объём работ будет намного больше, чем в этой статье.

Резюме

Написав всего 6 своих классов, мы получили новый тип репозитория, который может работать с Infinispan в качестве хранилища данных. И работает этот новый тип репозитория очень похоже на стандартные Spring репозитории.

Полный комплект исходников можно найти на моём github.

Исходники Spring Data KeyValue можно увидеть также на github.

Если у вас есть конструктивные замечания к данной реализации, то пишите в комментариях, либо можете сделать pull request в исходном проекте.

Теги:javainfinispanspring frameworkspring bootspringrepositoryadapterbackend
Хабы: Java
Всего голосов 7: ↑7 и ↓0 +7
Просмотры1.7K

Похожие публикации

Java/Kotlin Backend Developer
от 140 000 до 250 000 ₽EVEN LabМоскваМожно удаленно
Java-разработчик
от 80 000 до 130 000 ₽fabriqueМосква
Java разработчик
от 150 000 до 190 000 ₽KotelovСанкт-Петербург
Разработчик Java
от 150 000 ₽ITFB GroupМоскваМожно удаленно

Лучшие публикации за сутки