How to make a singleton by generic type with class argument in Spring?

Viewed 380

Let's suppose I have a Wrapper with generic type:

@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
public class Wrapper<T> {
    private final Class<T> wrappedClass;
    
    public Wrapper(Class<T> wrappedClass) {
       this.wrappedClass = wrappedClass;
    }
}

And I want to use this Wrapper with many classes (for example > 100). Is it possible to make Spring create singleton of wrapper for each generic type and pass generic class as parameter to constructor? For example, Spring must always inject the same instance of Wrapper<Foo>. If it is possible, please give example with java code configuration, but not with xml.

2 Answers

If I understood correctly you want to add beans of wrapper dynamically based on some criteria that some beans (like Foo / Bar) adhere to and some don't.

This is a kind of advanced stuff in spring, but in a nutshell you will have to implement a Bean Factory Post Processor that will be called automatically by spring during the startup.

This is a point where you could analyze the beans by iterating over all the "accessible" beans (like Foo / Bar and others) and for beans that should be wrapped you will create a bean definition of the wrapper, despite the fact that the wrapper itself is not a bean.

I've created a simple example to illustrate this. In my sample project I've put everything under package "wrappers":

@Wrappable
public class Foo {
}
@Wrappable
public class Bar {
}

public class ShouldNotBeWrapped {
}

Note that I've put an annotation @Wrappable - a custom annotation that will serve as a "differentiator" of what should be wrapped and what not. The processing of the annotation will be done in Bean Factory Post Processor.

The annotation is nothing special really, it should be acessible in runtime (spring is a runtime framework and be put on classes):

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Wrappable {
}

The java config will add Foo, Bar, ShouldNotBeWrapped as beans and also Bean Factory Post Processor that I'll describe below:

@Configuration
public class WrappersJavaConfig {
    @Bean
    public Foo foo () {
        return new Foo();
    }

    @Bean
    public Bar bar () {
        return new Bar();
    }

    @Bean
    public ShouldNotBeWrapped shouldNotBeWrapped () {
        return new ShouldNotBeWrapped();
    }

    @Bean
    public WrappersEnrichmentBFPP wrappersEnrichmentBFPP () {
        return new WrappersEnrichmentBFPP();
    }
}

The Wrapper class itself for the sake of example has toString but it doesn't differ much from your wrapper presented in the question:

public class Wrapper<T> {
    private T wrapped;

    public Wrapper(T wrapped) {
        this.wrapped = wrapped;
    }

    @Override
    public String toString() {
        return "Wrapper for" + wrapped;
    }
}

And the Main class will list all the loaded beans and get their classes + call toString so that we could see that the wrappers are defined correctly:

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(WrappersJavaConfig.class);

        String[] names = ctx.getBeanDefinitionNames();
        for(String name : names) {
            Object bean = ctx.getBean(name);
            if(bean.getClass().getPackage().getName().startsWith("wrappers")) { 
                System.out.println(ctx.getBean(name).getClass() + " ==> " + ctx.getBean(name));
            }
        }
    }
}

Sidenote, the "if" condition in the main method is because I don't want to print the beans that spring loads by itself (infra stuff, etc) - only my beans which all reside in package "wrappers" as I've mentioned above.

Now the BeanFactoryPostProcessor - is a regular bean in a sense that it gets registered in the java config and it looks like this (your implementation might be different but the idea is the same):

public class WrappersEnrichmentBFPP implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        String[] bddNames = beanFactory.getBeanDefinitionNames();
        for(String bddName : bddNames) {
            Object bean = beanFactory.getBean(bddName);
            if(bean.getClass().isAnnotationPresent(Wrappable.class)) {
                BeanDefinition wrappedBdd = BeanDefinitionBuilder.genericBeanDefinition(Wrapper.class)
                        .addConstructorArgReference(bddName)
                        .getBeanDefinition();
                ((BeanDefinitionRegistry)beanFactory).registerBeanDefinition("wrapperFor" + bddName, wrappedBdd);
            }
        }
    }
}

So I'm getting all the beans one by one in for-each loop, then I'm asking whether the bean has an annotation "wrappable" on it in the if condition. If it has - it must be wrapped. In this case I create an "artificial" bean definition for Wrapper and add a constuctor that will reference my bean that should be wrapped. Then I register the bean definition by adding it to the application context.

Run the code above and you'll see the output similar to mine:

class wrappers.WrappersJavaConfig$$EnhancerBySpringCGLIB$$f88f147d ==> wrappers.WrappersJavaConfig$$EnhancerBySpringCGLIB$$f88f147d@1283bb96
class wrappers.Foo ==> wrappers.Foo@74f0ea28
class wrappers.Bar ==> wrappers.Bar@f6efaab
class wrappers.ShouldNotBeWrapped ==> wrappers.ShouldNotBeWrapped@3c19aaa5
class wrappers.WrappersEnrichmentBFPP ==> wrappers.WrappersEnrichmentBFPP@3349e9bb
class wrappers.Wrapper ==> Wrapper forwrappers.Foo@74f0ea28
class wrappers.Wrapper ==> Wrapper forwrappers.Bar@f6efaab

As you see, two last lines are lines that correspond to the wrapper beans created for the same instances of Foo and Bar but nothing was created for the ShouldNotBeWrapped bean

The APIs used are somewhat obscure and look outdated, but again its pretty advanced stuff and works at the level of spring container infra itself. Having said that, there are a lot of tutorials about BeanFactoryPostProcessor-s.

Since Using BFPPs is not a usual task, and although I've provided the solution, I don't see any real usage of it, wrappers can't be used "instead" of Foo or Bar classes, do not have their APIs, etc. Maybe you could explain why do you need wrappers over some beans. Usually people use Aspects/BeanPostProcessors (not BFPP but BPP) to wrap the class into dynamic proxy (cglib / java.lang.Proxy) and add an additional behavior, stuff like @Transactional, cache handling and so forth is implemented in spring with BeanPostProcessors, so consider checking this direction as well.

It is possible and in fact a feature in spring.

Spring can inject your dependency with the correct generic type.The following example is from spring documentation.

Suppose you have an interface

public interface Store<T>{...}

and two beans. One implements Store,one implemenets Store.

@Configuration
public class MyConfiguration {

    @Bean
    public StringStore stringStore() {
        return new StringStore();
    }

    @Bean
    public IntegerStore integerStore() {
        return new IntegerStore();
    }
}

You can declare the type with the correct type parameter and spring will inject the right bean for you.

@Autowired
private Store<String> s1; // <String> qualifier, injects the stringStore bean

@Autowired
private Store<Integer> s2; // <Integer> qualifier, injects the integerStore bean
Related