[Java] Разбираемся, как работает Spring Data Repository, и создаем свою библиотеку по аналогии
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
На habr уже была статья о том, как создать библиотеку в стиле Spring Data Repository (рекомендую к чтению), но способы создания объектов довольно сильно отличаются от "классического" подхода, используемого в Spring Boot. В этой статье я постарался быть максимально близким к этому подходу. Такой способ создания бинов (beans) применяется не только в Spring Data, но и, например, в Spring Cloud OpenFeign.Содержание
ТЗНачнем с краткого технического задания: что мы в итоге хотим получить. Весь бизнес-слой является полностью выдумкой и не является примером качественного программирования, основная его цель - показать, как можно взаимодействовать со Spring. Итак, у нас прошли праздники, но мы хотим иметь возможность создавать на лету бины (beans), которые позволили бы нам поздравлять всех, кого мы в них перечислим.Пример:
public interface FamilyCongratulator extends Congratulator {
void сongratulateМамаAndПапа();
}
При вызове метода мы хотим получать:
Мама,Папа! Поздравляю с Новым годом! Всегда ваш
Или вот так
@Congratulate("С уважением, Пупкин")
public interface ColleagueCongratulator {
@CongratulateTo("Коллега")
void сongratulate();
}
и получать
Коллега! Поздравляю с Новым годом! С уважением, Пупкин
Т.е. мы должны найти все интерфейсы, которые расширяют интерфейс Congratulator или имеют аннотацию @CongratulateВ этих интерфейсах мы должны найти все методы, начинающиеся с congratulate , и сгенерировать для них метод, выводящий в лог соответствующее сообщение. @EnableКак и любая взрослая библиотека у нас будет аннотация, которая включает наш механизм (как @EnableFeignClients и @EnableJpaRepositories).
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
...}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(JpaRepositoriesRegistrar.class)
public @interface EnableJpaRepositories {
...}
Если посмотреть внимательно, то можно заметить, что обе этиx аннотации содержат @Import, где есть ссылка на класс, расширяющий интерфейс ImportBeanDefinitionRegistrar
public interface ImportBeanDefinitionRegistrar {
default void registerBeanDefinitions(
AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry,
BeanNameGenerator importBeanNameGenerator) {
registerBeanDefinitions(importingClassMetadata, registry);
}
default void registerBeanDefinitions(
AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
}
}
Напишем свою аннотацию
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import(CongratulatorsRegistrar.class)
public @interface EnableCongratulation {
}
Не забудем прописать @Retention(RetentionPolicy.RUNTIME), чтобы аннотация была видна во время выполнения.ImportBeanDefinitionRegistrarПосмотрим, что происходит в ImportBeanDefinitionRegistrar у Spring Cloud Feign:
class FeignClientsRegistrar
implements ImportBeanDefinitionRegistrar,
ResourceLoaderAware,
EnvironmentAware {
...
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
//создаются beans для конфигураций по умолчанию
registerDefaultConfiguration(metadata, registry);
//создаются beans для создания клиентов
registerFeignClients(metadata, registry);
}
...
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
...
//выполняется поиск кандидатов на создание
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
Set<String> basePackages = getBasePackages(metadata);
for (String basePackage : basePackages) {
candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
}
...
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
...
//заполняем контекст
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
//Создаем описание для Factory
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
...
//Регистрируем это описание
BeanDefinitionHolder holder = new BeanDefinitionHolder(
beanDefinition, className, new String[] { alias });
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
...
}
В Spring Cloud OpenFeign сначала создаются бины конфигурации, затем выполняется поиск кандидатов и для каждого кандидата создается Factory.В Spring Data подход аналогичный, но так как Spring Data состоит из множества модулей, то основные моменты разнесены по разным классам (см. например org.springframework.data.repository.config.RepositoryBeanDefinitionBuilder#build)Можно заметить, что сначала создаются Factory, а не сами bean. Это происходит потому, что мы не можем в BeanDefinitionHolder описать, как должен работать наш bean.Сделаем по аналогии наш класс (полный код класса можно посмотреть здесь)
public class CongratulatorsRegistrar implements
ImportBeanDefinitionRegistrar,
ResourceLoaderAware, //используется для получения ResourceLoader
EnvironmentAware { //используется для получения Environment
private ResourceLoader resourceLoader;
private Environment environment;
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
...
ResourceLoaderAware и EnvironmentAware используется для получения объектов класса ResourceLoader и Environment соответственно. При создании экземпляра CongratulatorsRegistrar Spring вызовет соответствующие set-методы.Чтобы найти требуемые нам интерфейсы, используется следующий код:
//создаем scanner
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
//добавляем необходимые фильтры
//AnnotationTypeFilter - для аннотаций
//AssignableTypeFilter - для наследования
scanner.addIncludeFilter(new AnnotationTypeFilter(Congratulate.class));
scanner.addIncludeFilter(new AssignableTypeFilter(Congratulator.class));
//указываем пакет, где будем искать
//importingClassMetadata.getClassName() - возвращает имя класса,
//где стоит аннотация @EnableCongratulation
String basePackage = ClassUtils.getPackageName(
importingClassMetadata.getClassName());
//собственно сам поиск
LinkedHashSet<BeanDefinition> candidateComponents =
new LinkedHashSet<>(scanner.findCandidateComponents(basePackage));
...
private ClassPathScanningCandidateComponentProvider getScanner() {
return new ClassPathScanningCandidateComponentProvider(false,
this.environment) {
@Override
protected boolean isCandidateComponent(
AnnotatedBeanDefinition beanDefinition) {
//требуется, чтобы исключить родительский класс - Congratulator
return !Congratulator.class.getCanonicalName()
.equals(beanDefinition.getMetadata().getClassName());
}
};
}
Регистрация Factory:
String className = annotationMetadata.getClassName();
// Используем класс CongratulationFactoryBean как наш Factory,
// реализуем в дальнейшем
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(CongratulationFactoryBean.class);
// описываем, какие параметры и как передаем,
// здесь выбран - через конструктор
definition.addConstructorArgValue(className);
definition.addConstructorArgValue(configName);
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
// aliasName - создается из наших Congratulator
String aliasName = AnnotationBeanNameGenerator.INSTANCE.generateBeanName(
candidateComponent, registry);
String name = BeanDefinitionReaderUtils.generateBeanName(
beanDefinition, registry);
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition,
name, new String[]{aliasName});
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
Попробовав разные способы, я советую остановиться на передаче параметров через конструктор, этот способ работает наиболее стабильно. Если вы захотите передать параметры не через конструктор, а через поля, то в параметры (beanDefinition.setAttribute) обязательно надо положить переменную FactoryBean.OBJECT_TYPE_ATTRIBUTE и соответствующий класс (именно класс, а не строку). Без этого наш Factory создаваться не будет. И Sping Data и Spring Feign передают строку: скорее всего это действует как соглашение, так как найти место, где эта строка используется, я не смог (если кто подскажет - дополню).Что, если мы хотим иметь возможность получать наши beans по имени, например, так
@Autowired
private Congratulator familyCongratulator;
это тоже возможно, так как во время создания Factory в качестве alias было передано имя bean (AnnotationBeanNameGenerator.INSTANCE.generateBeanName(candidateComponent, registry))FactoryBeanТеперь займемся Factory.Стандартный интерфейс FactoryBean имеет 2 метода, которые нужно имплементировать
public interface FactoryBean<T> {
Class<?> getObjectType();
T getObject() throws Exception;
default boolean isSingleton() {
return true;
}
}
Заметим, что есть возможность указать, является ли объект, который будет создаваться, Singleton или нет.Есть абстрактный класс (AbstractFactoryBean), который расширяет интерфейс дополнительной логикой (например, поддержка destroy-методов). Он так же имеет 2 абстрактных метода
public abstract class AbstractFactoryBean<T> implements FactoryBean<T>{
...
@Override
public abstract Class<?> getObjectType();
protected abstract T createInstance() throws Exception;
}
Первый метод getObjectType требует вернуть класс возвращаемого объекта - это просто, его мы передали в конструктор.
@Override
public Class<?> getObjectType() {
return type;
}
Второй метод требует вернуть уже сам объект, а для этого нужно его создать. Для этого есть много способов. Здесь представлен один из них. Сначала создадим обработчик для каждого метода:
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<>();
for (Method method : type.getMethods()) {
if (!AopUtils.isEqualsMethod(method) &&
!AopUtils.isToStringMethod(method) &&
!AopUtils.isHashCodeMethod(method) &&
!method.getName().startsWith(СONGRATULATE)
) {
throw new UnsupportedOperationException(
"Method " + method.getName() + " is unsupported");
}
String methodName = method.getName();
if (methodName.startsWith(СONGRATULATE)) {
if (!"void".equals(method.getReturnType().getCanonicalName())) {
throw new UnsupportedOperationException(
"Congratulate method must return void");
}
List<String> members = new ArrayList<>();
CongratulateTo annotation = method.getAnnotation(
CongratulateTo.class);
if (annotation != null) {
members.add(annotation.value());
}
members.addAll(Arrays.asList(methodName.replace(СONGRATULATE, "").split(AND)));
MethodHandler handler = new MethodHandler(sign, members);
methodToHandler.put(method, handler);
}
}
Здесь MethodHandler - простой класс, который мы создаем сами.Теперь нам нужно создать объект. Можно, конечно, напрямую вызвать Proxy.newInstance, но лучше воспользоваться классами Spring, которые, например, дополнительно создадут для нас методы hashCode и equals.
//Класс Spring для создания proxy-объектов
ProxyFactory pf = new ProxyFactory();
//указываем список интерфейсов, которые этот bean должен реализовывать
pf.setInterfaces(type);
//добавляем advice, который будет вызываться при вызове любого метода proxy-объекта
pf.addAdvice((MethodInterceptor) invocation -> {
Method method = invocation.getMethod();
//добавляем какой-нибудь toString метод
if (AopUtils.isToStringMethod(method)) {
return "proxyCongratulation, target:" + type.getCanonicalName();
}
//находим и вызываем наш созданный ранее MethodHandler
MethodHandler methodHandler = methodToHandler.get(method);
if (methodHandler != null) {
methodHandler.congratulate();
return null;
}
return null;
});
target = pf.getProxy();
Объект готов.Теперь при старте контекста Spring создает бины (beans) на основе наших интерфейсов. Исходный код можно посмотреть здесь.Полезные ссылки
- Создание библиотеки в стиле Spring Data Repository своими руками при помощи Dynamic Proxy и Spring IoC
- Spring изнутри
- How to use FactoryBean
===========
Источник:
habr.com
===========
Похожие новости:
- [Java, Разработка мобильных приложений, Разработка под Android] Android Bluetooth Low Energy (BLE) – готовим правильно, часть #4 (bonding) (перевод)
- [PHP, PostgreSQL, SQL] Установка Redmine за 15 минут (RVM + RoR + Unicorn + Nginx)
- [Информационная безопасность, Разработка веб-сайтов, JavaScript] Опасная уязвимость в популярной библиотеке Sequelize
- [Open source, Java, API, Apache, Natural Language Processing] Поиск по синонимам — контролируем процесс или доверяемся нейросетям
- [Программирование, Java, API, Хакатоны] Тривиальная и неправильная «облачная» компиляция
- [Java, Git, DevOps] Пишем Telegram Bota для оповещения о коммите в git репозитарий на базе Gitea и разворачиваем его в Google Cloud Platform
- [Разработка веб-сайтов, JavaScript, Программирование] Углублённое руководство по JavaScript: генераторы. Часть 2, простой пример использования (перевод)
- [JavaScript, Программирование, Тестирование веб-сервисов] Тестирование с использованием Puppeteer
- [Программирование, Java, Микросервисы] Spring Cloud и Spring Boot. Часть 1: использование Eureka Server (перевод)
- [Тестирование веб-сервисов] Тестирование нескольких экземпляров одного и того же мок компонента (перевод)
Теги для поиска: #_java, #_java, #_spring, #_springboot, #_java
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 22:47
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
На habr уже была статья о том, как создать библиотеку в стиле Spring Data Repository (рекомендую к чтению), но способы создания объектов довольно сильно отличаются от "классического" подхода, используемого в Spring Boot. В этой статье я постарался быть максимально близким к этому подходу. Такой способ создания бинов (beans) применяется не только в Spring Data, но и, например, в Spring Cloud OpenFeign.Содержание ТЗНачнем с краткого технического задания: что мы в итоге хотим получить. Весь бизнес-слой является полностью выдумкой и не является примером качественного программирования, основная его цель - показать, как можно взаимодействовать со Spring. Итак, у нас прошли праздники, но мы хотим иметь возможность создавать на лету бины (beans), которые позволили бы нам поздравлять всех, кого мы в них перечислим.Пример: public interface FamilyCongratulator extends Congratulator {
void сongratulateМамаAndПапа(); } Мама,Папа! Поздравляю с Новым годом! Всегда ваш
@Congratulate("С уважением, Пупкин")
public interface ColleagueCongratulator { @CongratulateTo("Коллега") void сongratulate(); } Коллега! Поздравляю с Новым годом! С уважением, Пупкин
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE) @Documented @Import(FeignClientsRegistrar.class) public @interface EnableFeignClients { ...} @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Import(JpaRepositoriesRegistrar.class) public @interface EnableJpaRepositories { ...} public interface ImportBeanDefinitionRegistrar {
default void registerBeanDefinitions( AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) { registerBeanDefinitions(importingClassMetadata, registry); } default void registerBeanDefinitions( AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { } } @Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME) @Import(CongratulatorsRegistrar.class) public @interface EnableCongratulation { } class FeignClientsRegistrar
implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware { ... @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { //создаются beans для конфигураций по умолчанию registerDefaultConfiguration(metadata, registry); //создаются beans для создания клиентов registerFeignClients(metadata, registry); } ... public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>(); ... //выполняется поиск кандидатов на создание ClassPathScanningCandidateComponentProvider scanner = getScanner(); scanner.setResourceLoader(this.resourceLoader); scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class)); Set<String> basePackages = getBasePackages(metadata); for (String basePackage : basePackages) { candidateComponents.addAll(scanner.findCandidateComponents(basePackage)); } ... for (BeanDefinition candidateComponent : candidateComponents) { if (candidateComponent instanceof AnnotatedBeanDefinition) { ... //заполняем контекст registerFeignClient(registry, annotationMetadata, attributes); } } } private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) { String className = annotationMetadata.getClassName(); //Создаем описание для Factory BeanDefinitionBuilder definition = BeanDefinitionBuilder .genericBeanDefinition(FeignClientFactoryBean.class); ... //Регистрируем это описание BeanDefinitionHolder holder = new BeanDefinitionHolder( beanDefinition, className, new String[] { alias }); BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); } ... } public class CongratulatorsRegistrar implements
ImportBeanDefinitionRegistrar, ResourceLoaderAware, //используется для получения ResourceLoader EnvironmentAware { //используется для получения Environment private ResourceLoader resourceLoader; private Environment environment; @Override public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } @Override public void setEnvironment(Environment environment) { this.environment = environment; } ... //создаем scanner
ClassPathScanningCandidateComponentProvider scanner = getScanner(); scanner.setResourceLoader(this.resourceLoader); //добавляем необходимые фильтры //AnnotationTypeFilter - для аннотаций //AssignableTypeFilter - для наследования scanner.addIncludeFilter(new AnnotationTypeFilter(Congratulate.class)); scanner.addIncludeFilter(new AssignableTypeFilter(Congratulator.class)); //указываем пакет, где будем искать //importingClassMetadata.getClassName() - возвращает имя класса, //где стоит аннотация @EnableCongratulation String basePackage = ClassUtils.getPackageName( importingClassMetadata.getClassName()); //собственно сам поиск LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>(scanner.findCandidateComponents(basePackage)); ... private ClassPathScanningCandidateComponentProvider getScanner() { return new ClassPathScanningCandidateComponentProvider(false, this.environment) { @Override protected boolean isCandidateComponent( AnnotatedBeanDefinition beanDefinition) { //требуется, чтобы исключить родительский класс - Congratulator return !Congratulator.class.getCanonicalName() .equals(beanDefinition.getMetadata().getClassName()); } }; } String className = annotationMetadata.getClassName();
// Используем класс CongratulationFactoryBean как наш Factory, // реализуем в дальнейшем BeanDefinitionBuilder definition = BeanDefinitionBuilder .genericBeanDefinition(CongratulationFactoryBean.class); // описываем, какие параметры и как передаем, // здесь выбран - через конструктор definition.addConstructorArgValue(className); definition.addConstructorArgValue(configName); AbstractBeanDefinition beanDefinition = definition.getBeanDefinition(); beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className); // aliasName - создается из наших Congratulator String aliasName = AnnotationBeanNameGenerator.INSTANCE.generateBeanName( candidateComponent, registry); String name = BeanDefinitionReaderUtils.generateBeanName( beanDefinition, registry); BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, name, new String[]{aliasName}); BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); @Autowired
private Congratulator familyCongratulator; public interface FactoryBean<T> {
Class<?> getObjectType(); T getObject() throws Exception; default boolean isSingleton() { return true; } } public abstract class AbstractFactoryBean<T> implements FactoryBean<T>{
... @Override public abstract Class<?> getObjectType(); protected abstract T createInstance() throws Exception; } @Override
public Class<?> getObjectType() { return type; } Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<>();
for (Method method : type.getMethods()) { if (!AopUtils.isEqualsMethod(method) && !AopUtils.isToStringMethod(method) && !AopUtils.isHashCodeMethod(method) && !method.getName().startsWith(СONGRATULATE) ) { throw new UnsupportedOperationException( "Method " + method.getName() + " is unsupported"); } String methodName = method.getName(); if (methodName.startsWith(СONGRATULATE)) { if (!"void".equals(method.getReturnType().getCanonicalName())) { throw new UnsupportedOperationException( "Congratulate method must return void"); } List<String> members = new ArrayList<>(); CongratulateTo annotation = method.getAnnotation( CongratulateTo.class); if (annotation != null) { members.add(annotation.value()); } members.addAll(Arrays.asList(methodName.replace(СONGRATULATE, "").split(AND))); MethodHandler handler = new MethodHandler(sign, members); methodToHandler.put(method, handler); } } //Класс Spring для создания proxy-объектов
ProxyFactory pf = new ProxyFactory(); //указываем список интерфейсов, которые этот bean должен реализовывать pf.setInterfaces(type); //добавляем advice, который будет вызываться при вызове любого метода proxy-объекта pf.addAdvice((MethodInterceptor) invocation -> { Method method = invocation.getMethod(); //добавляем какой-нибудь toString метод if (AopUtils.isToStringMethod(method)) { return "proxyCongratulation, target:" + type.getCanonicalName(); } //находим и вызываем наш созданный ранее MethodHandler MethodHandler methodHandler = methodToHandler.get(method); if (methodHandler != null) { methodHandler.congratulate(); return null; } return null; }); target = pf.getProxy();
=========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 22:47
Часовой пояс: UTC + 5