mysql循环依赖_记一次循环依赖踩坑 - hzjjames的个人空间 - OSCHINA - 中文开源技术交流社区...

0787d26511187c7b69d459e94d7a98fc.png

下面我讲述下这次踩坑的过程,主要涉及的知识点有三个:模板方法、Bean加载顺序和循环依赖。

这次踩坑的起因要从模板方法说起,最近写的一个需求,在Manager中需要对A、B、C三类数据进行处理,处理过程类似且较多,而只是数据类型和细节上有些差异。为了复用,自然想到了用模板方法重写,这也是我第一次尝试在Spring中使用模板方法,然后就踩坑了T T。

下面我大概重现下场景,在Manager中有一个fun方法会根据传入的type使用相应的工具类处理数据,工具类是通过属性注入的UtilA、UtilB和UtilC。Manager中还有一个preHandle方法做一些数据预处理,后续会用到,但不是现在。

@Component

public class Manager{

@Autowired

private UtilA utilA;

@Autowired

private UtilB utilB;

@Autowired

private UtilC utilC;

public void fun(String type, String data){

switch (type) {

case "A" :

utilA.process(data);

break;

case "B" :

utilB.process(data);

break;

case "C":

utilC.process(data);

break;

default:

utilA.doProcess(data);

}

}

public String preHandle(String data){

// 我是一个假预处理...我什么都没做,嘿嘿

return data;

}

}

UtilA、UtilB和UtilC都继承了一个模板类Template。process方法是一个模板方法用于处理数据,同时调用了doProcess抽象方法,其具体逻辑将由UtilA、UtilB和UtilC实现。

public abstract class Template{

public void process(String data){

// 我是一个模板方法...我可以做很多工作,省得儿子们都写一遍

// 而特殊的工作交给doProcess由儿子们来具体实现

doProcess(data);

}

protected abstract void doProcess(String data);

}

以UtilA为例,如下:

@Component

public class UtilA extends Template{

@Override

protected void doProcess(String data){

System.out.println("我是A,处理数据:" + data);

}

}

模板方法我们都写出来了,没什么问题。但现在我还有这样一个需求,我要在process方法中调用Manager的preHandle方法(别问我为啥不直接复制过来,实际情况更复杂些,在preHandle中还用到了很多其他方法和依赖,所以最好是复用),因此需要在Template中获得Manager的实例,可是Template是一个抽象类,都没法实例化成Bean,更别提依赖注入了。这里我的解决办法是,引入了一个SpringContextHolder,这是一个ApplicationContext的包装类,通过它来获得Manager实例,其定义如下:

@Component

public class SpringContextHolder implements ApplicationContextAware{

private static ApplicationContext applicationContext;

@Override

public void setApplicationContext(ApplicationContext context) throws BeansException{

applicationContext = context;

}

public static  T getBean(String name){

return (T) applicationContext.getBean(name);

}

}

然后是改写Template类,在构造函数中获得Manager实例,然后在process方法就可以顺利调用preHandle方法了。

public abstract class Template{

private Manager manager;

public Template(){

manager = SpringContextHolder.getBean("manager");

}

public void process(String data){

manager.preHandle(data);

doProcess(data);

}

protected abstract void doProcess(String data);

}

下面是主函数,开始运行了:

public class Main{

public static void main(String[] args){

ApplicationContext context = new ClassPathXmlApplicationContext("spring-context.xml");

Manager manager = (Manager) context.getBean("manager");

manager.fun("A", "123");

}

}

调用manager的fun方法,由于我们传入的参数是"A",所以将会使用utilA处理数据。一切看起来都很好,但这时候就遇到第一个问题了,启动容器时,会加载UtilA,将调用构造器进行实例化,而在构造器中我们指定通过SpringContextHolder的getBean方法来获得manager,这时由于SpringContextHolder还未被加载,所以applicationContext是null,因此会报出空指针问题,所以我们需要保证在加载UtilA之前先加载SpringContextHolder,也就是控制Bean的加载顺序。我们可以借助@DependsOn注解,加在UtilA上,并传入参数“springContextHolder”,当加载UtilA时就会先完成SpringContextHolder的加载。

@Component

@DependsOn("springContextHolder")

public class UtilA extends Template{

@Override

protected void doProcess(String data){

System.out.println("我是A,处理数据:" + data);

}

}

这下搞定了,能跑了。当我把代码上传到测试环境,应用无法启动了。一看日志,是发生了循环依赖,Spring容器起不来。仔细一看,确实发生了循环依赖。Manager中通过属性注入UtilA,而UtilA的父类Template在构造函数中通过getBean获得Manger。可是问题来了,为什么我在本地能运行,而测试环境却报错了?说细点就是,为什么本地不会发生循环依赖,而测试环境会发生循环依赖。如果你之前看过《Spring源码-循环依赖(附25张调试截图)》或者对循环依赖有所了解,想必已经知道如果X和Y都是属性注入的循环依赖,Spring能通过三级缓存解决,不会报错,而对于X和Y都是构造器注入的循环依赖,Spring是无法解决的,会报错。现在的情况是,我一处用了属性注入,而另一处用了构造器注入。所以猜想,在本地是先加载的Manager,先做的属性注入,所以不报错,而测试环境是先加载的UtilA,先做的构造器注入,所以产生循环依赖错误。为什么两个环境的加载顺序不同呢?查了些资料,Spring自动扫描的加载顺序和hashCode有关,而hashCode和操作系统有关,所以两个环境的操作系统不同可能会导致加载顺序不同。这也就是本地环境和测试环境运行结果不同的原因了。

下面说下怎么解决这个问题,大概的思路有两种:

去除构造器依赖;

控制加载顺序。

第一种方法,就是不要在构造器中获取依赖了,我们可以在process方法中获取:

public abstract class Template{

private Manager manager;

public Template(){

}

public void process(String data){

manager = SpringContextHolder.getBean("manager");

manager.preHandle(data);

doProcess(data);

}

protected abstract void doProcess(String data);

}

第二种方法,就是控制Manager始终在UtilA之前加载,利用@DependsOn注解:

@Component

@DependsOn({"springContextHolder", "manager"})

public class UtilA extends Template{

@Override

protected void doProcess(String data){

System.out.println("我是A,处理数据:" + data);

}

}

我最后采用的是方法一,考虑的是只需要修改一处即可,第二种方法需要修改三个子类,改动处较多。大家如果遇到这种问题,还是根据自己的实际情况来解决。

最后总结下,自己这次踩坑的原因有两点:

在学习循环依赖时,只考虑到了X和Y都用属性注入或构造器注入,没思考过X使用属性注入、Y使用构造器注入是否会发生循环依赖问题。

对Bean的加载顺序缺乏关注。为了保证程序的正确运行,Bean的加载顺序需要保证正确。

4bd8437ea900f6207cad07ee66e27cb4.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值