Разбираемся, как работает 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

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

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