如何在Spring中使用类参数通过通用类型创建单例?

huangapple go评论85阅读模式
英文:

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

问题

假设我有一个带有泛型的包装类:

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

我想要将这个 Wrapper 与许多类一起使用(例如 > 100 个)。是否可以让 Spring 为每个泛型类型创建一个包装类的单例,并将泛型类作为参数传递给构造函数?例如,Spring 必须始终注入相同的 Wrapper<Foo> 实例。如果可能的话,请给出使用 Java 代码配置的示例,而不是 XML。

英文:

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

@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
public class Wrapper&lt;T&gt; {
    private final Class&lt;T&gt; wrappedClass;
    
    public Wrapper(Class&lt;T&gt; 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&lt;Foo&gt;. If it is possible, please give example with java code configuration, but not with xml.

答案1

得分: 2

如果我理解正确,您想根据一些标准动态添加包装器动态的豆子,其中一些豆子(如Foo / Bar)遵循一些标准,而其他豆子则不遵循。

这是Spring中的高级内容,但简而言之,您需要实现一个Bean工厂后处理器,在启动期间Spring会自动调用它。

这是一个可以在其中通过迭代所有“可访问”的豆子(如Foo / Bar等)来分析豆子的地方,对于应该被包装的豆子,您将创建一个包装器的豆子定义,尽管包装器本身不是一个豆子。

我创建了一个简单的示例来说明这一点。在我的示例项目中,我将所有内容都放在“wrappers”包下:

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

public class ShouldNotBeWrapped {
}

请注意,我添加了一个注解@Wrappable - 这是一个自定义注解,将用作区分应该包装什么和什么不应该包装的“区分器”。注解的处理将在Bean工厂后处理器中完成。

该注解实际上没有什么特别之处,它应该在运行时可访问(Spring是一个运行时框架,并且可以放在类上):

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

Java配置将添加FooBarShouldNotBeWrapped作为豆子,并添加Bean工厂后处理器,我将在下面描述:

@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();
    }
}

出于示例目的,Wrapper类本身具有toString方法,但与您在问题中提出的包装器并没有太大区别:

public class Wrapper<T> {
    private T wrapped;

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

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

主类将列出所有加载的豆子并获取它们的类+调用toString,以便我们可以看到包装器是否被正确定义:

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));
            }
        }
    }
}

顺便说一下,在主方法中的“if”条件是因为我不想打印Spring自行加载的豆子(基础架构等) - 只打印我的豆子,它们都位于“wrappers”包中,正如我上面提到的。

现在BeanFactoryPostProcessor - 从某种意义上说是一个常规的bean,因为它在Java配置中注册,它看起来像这样(您的实现可能不同,但思想是一样的):

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);
            }
        }
    }
}

因此,我在for-each循环中逐个获取所有豆子,然后在“if条件”中询问豆子是否具有注解“wrappable”。如果有 - 它必须被包装。
在这种情况下,我为Wrapper创建了一个“人为的”bean定义,并添加了一个引用应该被包装的我的bean的构造函数。
然后,通过将其添加到应用程序上下文中,我注册了bean定义。

运行上面的代码,您将看到类似于我的输出:

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 for wrappers.Foo@74f0ea28
class wrappers.Wrapper ==> Wrapper for wrappers.Bar@f6efaab

如您所见,最后两行是对与FooBar的相同实例创建的包装器豆子的描述,但没有为ShouldNotBeWrapped豆子创建任何内容。

所使用的API有些晦涩,并且看起来过时,但再次,这是相当高级的内容,它在Spring容器基础架构的级别工作。话虽如此,关于BeanFactoryPostProcessor的教程有很多。

由于使用BFPPs不是一项常规任务,尽管我提供了解决方案,但我没有看到它的实际用途,包装器

英文:

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&lt;T&gt; {
    private T wrapped;

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

    @Override
    public String toString() {
        return &quot;Wrapper for&quot; + 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(&quot;wrappers&quot;)) { 
                System.out.println(ctx.getBean(name).getClass() + &quot; ==&gt; &quot; + 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(&quot;wrapperFor&quot; + 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 ==&gt; wrappers.WrappersJavaConfig$$EnhancerBySpringCGLIB$$f88f147d@1283bb96
class wrappers.Foo ==&gt; wrappers.Foo@74f0ea28
class wrappers.Bar ==&gt; wrappers.Bar@f6efaab
class wrappers.ShouldNotBeWrapped ==&gt; wrappers.ShouldNotBeWrapped@3c19aaa5
class wrappers.WrappersEnrichmentBFPP ==&gt; wrappers.WrappersEnrichmentBFPP@3349e9bb
class wrappers.Wrapper ==&gt; Wrapper forwrappers.Foo@74f0ea28
class wrappers.Wrapper ==&gt; 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.

答案2

得分: 0

这在 Spring 中是可能的,实际上也是一个特性。

Spring 可以使用正确的泛型类型来注入你的依赖关系。下面的示例来自于 Spring 文档。

假设你有一个接口:

public interface Store<T> {...}

还有两个 Bean。一个实现了 Store<String>,另一个实现了 Store<Integer>

@Configuration
public class MyConfiguration {

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

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

你可以声明正确类型参数的类型,Spring 将会为你注入正确的 Bean。

@Autowired
private Store<String> s1; // 使用 <String> 限定符,注入 stringStore Bean

@Autowired
private Store<Integer> s2; // 使用 <Integer> 限定符,注入 integerStore Bean
英文:

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&lt;T&gt;{...}

and two beans. One implements Store<String>,one implemenets Store<Integer>.

@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&lt;String&gt; s1; // &lt;String&gt; qualifier, injects the stringStore bean

@Autowired
private Store&lt;Integer&gt; s2; // &lt;Integer&gt; qualifier, injects the integerStore bean

</details>



huangapple
  • 本文由 发表于 2020年10月12日 16:48:55
  • 转载请务必保留本文链接:https://go.coder-hub.com/64314517.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定