Русский
Русский
English
Статистика
Реклама

Разбираемся, как работает 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 FeignClientsRegistrarimplements 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(); //Создаем описание для FactoryBeanDefinitionBuilder 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-методы.

Чтобы найти требуемые нам интерфейсы, используется следующий код:

//создаем scannerClassPathScanningCandidateComponentProvider scanner = getScanner();scanner.setResourceLoader(this.resourceLoader);//добавляем необходимые фильтры //AnnotationTypeFilter - для аннотаций//AssignableTypeFilter - для наследованияscanner.addIncludeFilter(new AnnotationTypeFilter(Congratulate.class));scanner.addIncludeFilter(new AssignableTypeFilter(Congratulator.class));//указываем пакет, где будем искать//importingClassMetadata.getClassName() - возвращает имя класса,//где стоит аннотация @EnableCongratulationString 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 - создается из наших CongratulatorString 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 по имени, например, так

@Autowiredprivate 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>{...@Overridepublic abstract Class<?> getObjectType();protected abstract T createInstance() throws Exception;}

Первый метод getObjectType требует вернуть класс возвращаемого объекта - это просто, его мы передали в конструктор.

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

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

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

Источник: habr.com
К списку статей
Опубликовано: 28.01.2021 18:09:16
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Java

Spring

Spring-boot

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru