Spring中单例对象引用原型对象

本文探讨了在Spring中,当单例对象需要引用原型对象时遇到的问题及解决方案。通过实现`ApplicationContextAware`接口手动从IOC容器获取原型对象,或者使用依赖查找(DL)方式,利用CGLIB动态生成子类来实现不同对象的引用。这两种方法各有优缺点,前者增加代码对框架的耦合,后者则避免了手动创建对象。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Spring中单例对象引用原型对象

在spring中,基本上所有的业务对象都是托管到了IOC中进行管理。
日常开发中我们会接触到的对象都大多数是单例的。当然,Spring中也提供了@Scope注解让使用者可以对类的作用域进行声明。

但若某时候,若想要使用一个业务对象A引用一个业务对象B,而A是单例对象,但是B是应该是原型的,那么此时直接在原型类B上直接利用@Scope("prototype")设置原型作用域后注入到A中是行不通的。

类A:

@Component
public class RefPrototype {

    @Autowired
    private ProDao proDao;


    public void refMethod () {
        proDao.daoMethod();
        System.out.println("ref method");
    }
}

类B:

@Repository
@Scope("prototype")
public class ProDao {
    public void daoMethod() {
        System.out.println(this.hashCode());
    }
}
AnnotationConfigApplicationContext ctx = 
         new AnnotationConfigApplicationContext(Config.class);
RefPrototype bean = ctx.getBean(RefPrototype.class);
bean.refMethod();
RefPrototype bean1 = ctx.getBean(RefPrototype.class);
bean1.refMethod();
RefPrototype bean2 = ctx.getBean(RefPrototype.class);
bean2.refMethod();

打印结果:

1397616978
ref method
1397616978
ref method
1397616978
ref method

这样打印出来的类B的hashcode其实都是一样的,也就是说,实际上注入了相同的类B。

其实原因还是挺简单的,因为你的单例类A在IOC中只实例化了一次,那么它对应的属性,在你没有手动去干预的情况下,它也是不会变动的。

想要实现这种效果,Spring官方文档里提供的第一种解决方案是实现ApplicationContextAware 接口,然后拿到IOC容器,对于这个多例类不采用DI的方式获取,而是每次通过IOC容器去手动获取。这就验证了我们前面所说的,手动干预。

ApplicationContextAware 接口

public interface ApplicationContextAware 
           extends Aware {
    void setApplicationContext(ApplicationContext var1) 
                                     throws BeansException;
}

修改后的类A:

@Component
public class RefPrototype implements ApplicationContextAware {

//    @Autowired
    private ProDao proDao;
    private ApplicationContext ctx;

    public void refMethod () {
        this.createProDao().daoMethod();
        System.out.println("ref method");
    }

    public void setApplicationContext(ApplicationContext 
               applicationContext) throws BeansException {
        this.ctx = applicationContext;
    }

    public ProDao createProDao () {
        return this.ctx.getBean(ProDao.class);
    }

}

setApplicationContext() 会在 ApplicationContextAwareProcessorinvokeAwareInterfaces() 中调用并且将IOC设置进去。

这种方式是可以实现我们想要的那种效果的,放弃部分IOC。但是它的弊端就是业务代码直接耦合了框架,对框架的侵入性较高,这样是不利于代码的整洁和后期维护。

通过DL方式实现

除了上述方式外,spring文档也介绍了另外 一种方式,利用依赖查找的方式来实现单例类引用多例的问题。

此种方式要求对于需要多例出现的属性需要对应的有一个用于生产该类对象的抽象方法,方法需要用@LookUp("bean名字")进行声明。但注意此类型的方法需要出现在容器负责实例化对象的代码位置上。而不能出现在例如用@Bean修饰的方法上,因为此时是我们开发者在手动处理new的工作。

以下摘自原文档

The Spring Framework implements this method injection by using
bytecode generation from the CGLIB library to dynamically generate a
subclass that overrides the method.

此种方法的实质是利用cglib对我们声明的抽象查找方法进行了实现,进而我们才能使用它来获取我们需要的对象。

改造后的类A:

@Component
public abstract class RefPrototypeByLookUp {

    public void refMethod () {
        this.createProDao().daoMethod();
        System.out.println("ref method");
    }

    @Lookup(value = "proDao")
    public abstract ProDao createProDao();

}

测试代码:

AnnotationConfigApplicationContext ctx = 
    new AnnotationConfigApplicationContext(Config.class);
    
RefPrototypeByLookUp bean = 
                 =ctx.getBean(RefPrototypeByLookUp.class);
bean.refMethod();

RefPrototypeByLookUp bean1 
                = ctx.getBean(RefPrototypeByLookUp.class);
bean1.refMethod();

RefPrototypeByLookUp bean2 = 
                  ctx.getBean(RefPrototypeByLookUp.class);
bean2.refMethod();

测试结果:

1173643169
ref method
1282287470
ref method
1397616978
ref method

测试可以看出,每个类B对象的hashcode都是不一样的,说明我们此时引用的并不是同一个对象,也达到了依赖原型类的目的。

这里的抽象方法也可以为实现方法,但是你可能没法拿到你想返回的对象,如果直接new的话是非常不推崇的,所以这里可以考虑直接返回一个null。spring会对该查找方法进行重写。

最后附上一段官方文档说明。

If the method is abstract, the dynamically-generated subclass
implements the method. Otherwise, the dynamically-generated subclass
overrides the concrete method defined in the original class.

### Java 中单例模式与多例模式的概念 #### 单例模式 单例模式确保某一个类只有一个实例,并提供全局访问点。这种模式通过私有化构造函数并创建静态方法来获取唯一的实例对象[^1]。 ```java public class Singleton { private static final Singleton INSTANCE = new Singleton(); // 私有化构造器 private Singleton() {} // 提供全局访问点 public static Singleton getInstance() { return INSTANCE; } } ``` 枚举方式被认为是实现单例模式的最佳实践之一,因为这种方式不仅能够防止反序列化重新创建新的实例,而且天生具备线程安全性[^2]。 ```java public enum SingletonEnum { INSTANCE; public void someMethod() {} } ``` #### 多例模式 多例模式指的是每次请求都会创建一个新的对象实例。这适用于那些状态会频繁变化的对象,或者是需要独立生命周期管理的情况[^4]。 ```java // 原型模式下的Bean定义(Spring框架) @Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public MyPrototypeBean myPrototypeBean() { return new MyPrototypeBean(); } ``` ### 单例模式与多例模式的区别 - **实例数量**:单例模式在整个应用程序运行期间只会存在一个实例;而多例模式会在每一次调用时都生成新实例。 - **资源消耗**:由于单例模式只初始化一次,因此可以节省内存空间和其他有限资源;相比之下,多例模式可能会占用更多系统资源,尤其是在高并发场景下不断创建销毁对象的情况下。 - **线程安全**:对于单例来说,如果要保证其在线程间的正确行为,则需考虑同步问题或采用更优的方式如枚举形式;而对于多例而言,默认情况下不存在共享数据竞争条件的问题,所以一般不需要额外处理线程安全方面的事宜[^3]。 - **适用范围**:单例适合用于工具类、配置中心等不希望被重复加载的服务组件;多例更适合于业务逻辑实体或其他具有动态属性的对象,这些对象可能随环境改变而有所不同。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值