На 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) на основе наших интерфейсов.
Исходный код можно посмотреть здесь.