So i found a way to do this implementing the BeanDefinitionRegistryPostProcessor using the org.reflections lib to find all the Key annotations on my Property object.
I was then able create a custom bean definitions for each key which i can then register using the Key as a qualifier to allow spring to inject all my Property objects.
so first thing was adding the Qualifer annotation to my Key annotation.
@Qualifier
@interface Key {
String name();
String fallback() default "";
}
next was to create an implementation of the BeanDefinitionRegistryPostProcessor interface, this registers a bean definition with the concrete implementation of the Property interface to inject at runtime, the constructor parameters and the qualifier from the key annotation found by using the reflections to scan packages
(this was the key to replacing the use of reflections from setting the objects in my bean to just using it to dynamically lookup the key/property and making it available for injection)
@Component
public class PropertyBeanPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
Reflections reflections = new Reflections(ClasspathHelper.forPackage("com.package.to.scan"),
new FieldAnnotationsScanner(), new MethodParameterScanner());
registerBeansForConstructors(registry, reflections.getConstructorsWithAnyParamAnnotated(Key.class));
registerBeansForMethods(registry, reflections.getMethodsWithAnyParamAnnotated(Key.class));
registerBeansForFields(registry, reflections.getFieldsAnnotatedWith(Key.class));
}
private void registerBeansForFields(BeanDefinitionRegistry registry, Set<Field> fields) {
for (Field field : fields) {
Class<?> parameterType = field.getType();
Annotation[] annotations = field.getDeclaredAnnotations();
Type genericType = field.getGenericType();
registerBeanIfPropertyType(registry, parameterType, genericType, annotations);
}
}
private void registerBeansForMethods(BeanDefinitionRegistry registry, Set<Method> methods) {
for (Method method : methods) {
Class<?>[] parameterTypes = method.getParameterTypes();
Annotation[][] annotations = method.getParameterAnnotations();
Type[] genericTypes = method.getGenericParameterTypes();
registerBeansForParameters(registry, parameterTypes, annotations, genericTypes);
}
}
private void registerBeansForConstructors(BeanDefinitionRegistry registry, Set<Constructor> constructors) {
for (Constructor constructor : constructors) {
Class<?>[] parameterTypes = constructor.getParameterTypes();
Annotation[][] annotations = constructor.getParameterAnnotations();
Type[] genericTypes = constructor.getGenericParameterTypes();
registerBeansForParameters(registry, parameterTypes, annotations, genericTypes);
}
}
private void registerBeansForParameters(BeanDefinitionRegistry registry, Class<?>[] parameterTypes, Annotation[][] annotations, Type[] genericTypes) {
for (int i = 0; i < parameterTypes.length; i++) {
Class<?> parameterType = parameterTypes[i];
Annotation[] parameterAnnotations = annotations[i];
Type genericType = genericTypes[i];
registerBeanIfPropertyType(registry, parameterType, genericType, parameterAnnotations);
}
}
private void registerBeanIfPropertyType(BeanDefinitionRegistry registry, Class<?> parameterType, Type genericType, Annotation[] parameterAnnotations) {
if (!Property.class.isAssignableFrom(parameterType)) {
return;
}
Arrays.stream(parameterAnnotations)
.filter(annotation -> Key.class.isAssignableFrom(annotation.annotationType()))
.map(Key.class::cast)
.findFirst()
.ifPresent(key -> register(registry, key, genericType));
}
private void register(BeanDefinitionRegistry registry, Key key, Type type) {
registry.registerBeanDefinition(key.name(), createDefinition(key, type));
log.info("registered property: {}", key);
}
public static BeanDefinition createDefinition(Key key, Type type) {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(PropertyImpl.class);
beanDefinition.setConstructorArgumentValues(createConstructorArgumentValues(key, type));
beanDefinition.addQualifier(createAutowireCandidateQualifier(key));
return beanDefinition;
}
private static AutowireCandidateQualifier createAutowireCandidateQualifier(Key key) {
AutowireCandidateQualifier autowireCandidateQualifier = new AutowireCandidateQualifier(Key.class);
autowireCandidateQualifier.setAttribute("name", key.name());
autowireCandidateQualifier.setAttribute("fallback", key.fallback());
return autowireCandidateQualifier;
}
private static ConstructorArgumentValues createConstructorArgumentValues(Key key, Type type) {
ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues();
constructorArgumentValues.addIndexedArgumentValue(1, key);
constructorArgumentValues.addIndexedArgumentValue(2, getPropertyType(type));
return constructorArgumentValues;
}
private static Class<?> getPropertyType(Type type) {
if (!(type instanceof ParameterizedType)) {
throw new RuntimeException("field " + type.getTypeName() + " is not parameterised");
}
ParameterizedType parameterizedType = (ParameterizedType) type;
Type[] actualGenericTypeArguments = parameterizedType.getActualTypeArguments();
if (actualGenericTypeArguments.length != 1) {
throw new RuntimeException("invalid number of generics: " + Arrays.stream(actualGenericTypeArguments).map(Type::getTypeName).collect(Collectors.toList()));
}
return TypeToken.of(actualGenericTypeArguments[0]).getRawType();
}
in the end i found this works for me but there may be a better way to create a factory that spring can trigger rather than using the reflections lib as this isn't the fastest