草捏以前写过一篇《Spring源码-循环依赖(附25张调试截图)》,也算是对循环依赖研究了一番。但是今天仍是在循环依赖上踩坑了,真是被安排的明明白白。下面我讲述下此次踩坑的过程,主要涉及的知识点有三个:模板方法、Bean加载顺序和循环依赖。spring
此次踩坑的原由要从模板方法提及,最近写的一个需求,在Manager中须要对A、B、C三类数据进行处理,处理过程相似且较多,而只是数据类型和细节上有些差别。为了复用,天然想到了用模板方法重写,这也是我第一次尝试在Spring中使用模板方法,而后就踩坑了T T。缓存
下面我大概重现下场景,在Manager中有一个fun方法会根据传入的type使用相应的工具类处理数据,工具类是经过属性注入的UtilA、UtilB和UtilC。Manager中还有一个preHandle方法作一些数据预处理,后续会用到,但不是如今。app
@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实现。ide
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> 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); } }
我最后采用的是方法一,考虑的是只须要修改一处便可,第二种方法须要修改三个子类,改动处较多。你们若是遇到这种问题,仍是根据本身的实际状况来解决。
最后总结下,本身此次踩坑的缘由有两点: