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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

Это моя первая статья, в ней я попытаюсь описать полученный мною практический опыт работы со 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 в исходном проекте.

Источник: https://habr.com/ru/post/535218/


Интересные статьи

Интересные статьи

Я давно знаком с Битрикс24, ещё дольше с 1С-Битрикс и, конечно же, неоднократно имел дела с интернет-магазинами которые работают на нём. Да, конечно это дорого, долго, местами неуклюже...
(Изображение носит иллюстративный характер) Первая победа (PFG vs WG) (5:0) Wargaming в течении двух лет атакует нас и нашу компанию. До вчерашнего дня у нас было 7 судебных исков в...
Сразу оговоримся, что совсем дешево делать не будем, т.к. не хочется убивать нервные клетки, делая доморощенные энкодеры для моторчиков + хочется упростить создание 3D модели, которая...
Здравствуйте, меня зовут Виктор и я разработчик в компании Gems Development. Я хочу рассказать, как мы реализовывали создание и заполнение производственного календаря в Postgresql. ...
Привет, Хабр! Я тружусь в команде Антиспама, и, как и у большинства бэкенд-разработчиков Badoo, большая часть времени у меня уходит на работу с PHP-кодом. С этой работой связано много специфи...