Разбираемся, как работает 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) на основе наших интерфейсов.

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

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

Let’s block ads! (Why?)

Read More

Recent Posts

VK купила 40% билетной платформы Intickets.ru

VK объявляет о приобретении 40% компании Intickets.ru (Интикетс). Это облачный сервис для контроля и управления продажей билетов на мероприятия. Сумма…

2 дня ago

OpenAI готовится запустить поисковую систему на базе ChatGPT

OpenAI готовится запустить собственную поисковую систему на базе ChatGPT. Информацию об этом публикуют западные издания. Ожидается, что новый поисковик может…

2 дня ago

Роскомнадзор рекомендовал хостинг-провайдерам ограничить сбор данных с сайтов для иностранных ботов

Центр управления связью общего пользования (ЦМУ ССОП) Роскомнадзора рекомендовал компаниям из реестра провайдеров ограничить доступ поисковых ботов к информации на российских сайтах.…

3 дня ago

Apple возобновила переговоры с OpenAI и Google для интеграции ИИ в iPhone

Apple возобновила переговоры с OpenAI о возможности внедрения ИИ-технологий в iOS 18, на основе данной операционной системы будут работать новые…

1 неделя ago

Российская «дочка» Google подготовила 23 иска к крупнейшим игрокам рекламного рынка

Конкурсный управляющий российской «дочки» Google подготовил 23 иска к участникам рекламного рынка. Общая сумма исков составляет 16 млрд рублей –…

1 неделя ago

Google завершил обновление основного алгоритма March 2024 Core Update

Google завершил обновление основного алгоритма March 2024 Core Update. Раскатка обновлений была завершена 19 апреля, но сообщил об этом поисковик…

1 неделя ago