Spring当中注入原型Bean的方式汇总

1. 了解单例和原型Bean

1.1 什么是单例Bean?什么是原型Bean?

  • 1.单例Bean,相信各位朋友都不陌生,Spring当中的Bean默认就是单例的,也就是无论从什么地方去使用@Autowired或者@Resource等方式去进行注入,拿到的都是同一个对象,这个对象维护在Spring容器当中,每次使用都是直接从Spring容器当中直接进行获取。
  • 2.原型Bean,也就是说你每次使用到该Bean,都是Spring框架它去重新帮你去进行创建的,也就是说你任意的两次获取该Bean,永远不可能获取到相同的对象。

1.2 如何去定义一个单例Bean?如何去定义一个原型Bean?

使用@Component@Bean@Configuration等注解往容器中注册的Bean,都是单例Bean,要想实现原型Bean,可以通过@Scope注解等方式去配置为Bean的作用域为prototype

下面是使用@Component注解去定义普通的单例Bean的一种方式

@Component
public class User {

    private int id;

    public void setId(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public void test() {
        System.out.println(this.hashCode());
    }
}

下面是去定义一个原型Bean的方式:

@Component
@Scope(value = BeanDefinition.SCOPE_PROTOTYPE)
public class User {

    private int id;

    public void setId(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public void test() {
        System.out.println(this.hashCode());
    }
}

1.3 了解Autowired注入原型Bean存在的问题

我们已经使用@Scope注解去配置该Bean为原型的,按道理,我们使用如下的代码注入的就是一个原型Bean

@Component
public class App implements BeanFactoryAware {
    @Autowired
    User user;
}

按道理,如果我们使用如下的代码对user对象进行输出时,每次都是得到不同的对象,也就是说它们的hash值应该是不同的,但是实际上呢?

    for (int i = 0; i < 5; i++) {
            System.out.println(user);
        }

得到如下的结果,你也许会好奇,不是说是原型Bean吗,怎么获取到的还是同一个对象呢?

com.wanna.User@49fdbe2b
com.wanna.User@49fdbe2b
com.wanna.User@49fdbe2b
com.wanna.User@49fdbe2b
com.wanna.User@49fdbe2b

实际上是因为,在SpringIOC容器的启动过程中,已经将App这个Bean完成了初始化操作,在初始化过程中,已经对App当中的属性值完成了设置,因此App对象当中的user对象已经被固定死了,无论我们怎么去获取,都会获取到同一个Bean

既然Autowired注入的是同一个User,那么,我们有办法去注入一个原型的Bean吗?肯定是有的!

2. 解决Autowired无法注入原型Bean的解决方法

2.1 使用@Scope注解的proxyMode属性去解决

proxyMode可以有两种配置方式,TARGET_CLASSINTERFACES,顾名思义,第一种方式代表去代理目标类,也就是使用CGLIB去完成动态代理;第二种方式代表去代理目标类的所有接口,也就是使用JDK动态代理去完成代理。

@Scope(value = BeanDefinition.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)

再去执行原来的代码,得到如下的运行结果,我们可以发现,得到的结果已经完全不同了!

com.wanna.User@35cec305
com.wanna.User@237add
com.wanna.User@491cafec
com.wanna.User@cbd40c1
com.wanna.User@4fa86cb8

如何进行实现的?我们都知道不管是CGLIB还是JDK的动态代理,都是拦截目标方法的,为什么我们直接输出就能拿到不同的对象?因为输出流进行输出时调用了toString方法,调用了这个方法,自然可以触发代理方法的执行,可以打印出来原型的效果。

2.2 使用@Lookup注解去进行解决

使用@Lookup注解需要配置在返回类型为具体类型的方法上(一般是配置在getter上,但是并不是一定的,也可以配置在其它方法上)。

@Lookup注解如何使用?

  • 1.如果不配置@Lookup注解的value属性,那么默认是按照方法的返回类型去返回对象。
  • 2.如果配置了@Lookup注解的value属性,那么将会按照beanName去返回对象。
  • 3.因为这个方法并不会被调用到,调用方法的目标逻辑是在拦截目标方法的切面方法当中去进行执行的,因此就算return null也不影响对象的获取。
  • 4.@Lookup注解并不一定要标注在实例方法上,可以标注在抽象方法(包括接口的方法)当中。
    @Lookup
    public User getUser() {
        return this.user;
    }

要获取Bean,就得使用标注了@Lookup的方法去进行获取对象。

    for (int i = 0; i < 5; i++) {
            System.out.println(getUser());
        }

最终得到如下的内容:

com.wanna.User@2eda2062
com.wanna.User@1a9ec80e
com.wanna.User@7fd4e815
com.wanna.User@5f6b53ed
com.wanna.User@20cdb152

2.3 通过配置TargetSource去进行配置

TargetSource,从这个名字当中我们可以知道,它是一个用来获取TargetSource,也就是一个用来获取"目标对象"的"源"。对于如何获取TargetSource,这当然是可以自定义的,这也是Spring留给我们的扩展点。

使用TargetSource需要配置Spring容器中默认的执行SpringAOPAbstract***Creator组件的相关内容,我们如何去进行配置?我们可以手写一个BeanPostProcessor去拦截下来Abstract***Creator的执行,然后去进行配置。

需要注意的是:

  • 我们既然想对Abstract***Creator进行拦截,那么我们肯定得优先级比它还高从而去保证比这个组件在创建时,Abstract***Creator这个组件还没完成创建!因此我们这里采用了实现PriorityOrdered接口,去保证优先级比较高。

我们类似编写如下的一个配置类:

@Configuration
public class Config implements BeanPostProcessor, PriorityOrdered, BeanFactoryAware {

    BeanFactory beanFactory;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        AbstractBeanFactoryBasedTargetSource targetSource = new AbstractBeanFactoryBasedTargetSource() {
            @Override
            public Object getTarget() throws Exception {
                return getBeanFactory().getBean(getTargetBeanName());
            }
        };

        if (bean instanceof Abstract***Creator) {
            AbstractBeanFactoryBasedTargetSourceCreator creator = new AbstractBeanFactoryBasedTargetSourceCreator() {
                @Override
                protected AbstractBeanFactoryBasedTargetSource createBeanFactoryBasedTargetSource(Class<?> beanClass, String beanName) {
                    if (User.class.isAssignableFrom(beanClass)) {
                        return targetSource;
                    }
                    return null;
                }
            };
            creator.setBeanFactory(beanFactory);
            ((Abstract***Creator) bean).setCustomTargetSourceCreators(creator);
        }

        return bean;
    }
}

我们主要通过setCustomTargetSourceCreators方法往Abstract***Creator组件当中添加一个自定义的TargetSourceCreator

这个TargetSourceCreator组件的作用是根据自定义相关的逻辑,最终返回一个AbstractBeanFactoryBasedTargetSource类型的TargetSource对象,它是一个典型的工厂模式!

对于我们想要实现的AbstractBeanFactoryBasedTargetSource组件,我们只需要实现它的getTarget方法即可,这个getTarget方法主要体现之前所讲述的那个词,"目标对象"的"源",“源”是从什么地方体现的呢?就是在这里,我们可以自定义逻辑获取Bean

但是这里的getBeanFactory().getBean(getTargetBeanName()),其实比较有意思,这个getBeanFactory获取到的并不是我们SpringIOC容器,而是一个克隆出来的新的BeanFactory

我们使用如下的代码去进行测试

    for (int i = 0; i < 5; i++) {
            System.out.println(user);
        }

得到如下的结果:

com.wanna.User@1f536481
com.wanna.User@5234b61a
com.wanna.User@22a260ff
com.wanna.User@54c425b1
com.wanna.User@50b734c4

其实这个TargetSource很有意思,我们来看看Spring当中对于它的实现就知道了!获取目标对象的来源可以是ThreadLocal,可以是Prototype,也可以是对象池(XXXPool),更多的实现我们还可以进行自定义。

image.png

也就是说基于TargetSource可以实现很多好玩的功能,并非是只能被我们去用来获取原型Bean,获取原型Bean只是它的其中一个功能,它还有很多功能(比如ThreadLocal)是值得我们去探索的!

还有个值得注意的问题:

  • 就算我们的Bean是单例的,如果我们给它配置了AbstractBeanFactoryBasedTargetSource,那么获取到这个Bean仍旧是会变成原型的,这是这个组件内部实现的。

2.4 手动getBean去实现获取原型Bean

既然我们都可以标注了@Lookup注解了,那么我们当然也可以实现类似的功能对吧!

    public User getUser() {
        return beanFactory.getBean(User.class);
    }

我们直接实现BeanFactoryAware或者是ApplicationContextAware接口去注入BeanFactory,手动在getter当中去进行getBean不就行了?自己手动通过getBean去获取到的肯定是Spring帮我们去进行新创建的对象,而且Spring底层肯定最终也要调用getBean去从容器中调用。

#Java学习##Spring##学习路径#
全部评论
感谢大佬分享!!!!😂
点赞 回复 分享
发布于 2022-01-15 15:08

相关推荐

jack_miller:我给我们导员说我不在这里转正,可能没三方签了。导员说没事学校催的时候帮我想办法应付一下
点赞 评论 收藏
分享
点赞 11 评论
分享
牛客网
牛客企业服务