Разбираемся, как работает Spring Data Repository, и создаем свою библиотеку по аналогии

от автора

На 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) на основе наших интерфейсов.

Исходный код можно посмотреть здесь.

Полезные ссылки

ссылка на оригинал статьи https://habr.com/ru/post/538860/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *