Spring Custom @Value Annotation

Viewed 382

I'm looking to create a custom interface to inject properties like so...

interface Property<T> { T get(); }

I would like to then set the return value of the get() call using a custom annotation like...

@interface Key { String name(); String fallback() default ""; }

Then uses this throughout my application like...

@key(name = "my.string.property", fallback = "some default value")
Property<String> myStringProperty;

@key(name = "my.number.property", fallback = "1")
Property<Integer> myNumberProperty;

The reason we want to do this rather than using the @Value annotation is to hook these objects into our pre-existing system events with a new PropertyChanged event which can update the return value of the get() method (we will also persist these updates as we're running a distributed system which can create new nodes at anytime) and will also expose these properties in our UIs system admin page.

I've managed to get this working for fields annotated with my custom annotation using ReflectionUtils#doWithFields from my own implementation of BeanPostProcessor#postProcessBeforeInitialization. This is more of a hack as spring does all it's injection and then we're updating the field via reflection so this doesn't work when you annotate the constructor param. I used this guide for that, https://www.baeldung.com/spring-annotation-bean-pre-processor.

so my question is, is there a way to implement a factory object for spring where i can write code to read the annotation and inject an implementation based on that so i don't need to use reflection and it will work no matter where i inject as it'll be a part of the springs normal injection life cycle?

1 Answers

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

Related