一个applicationContext 加载错误导致的阻塞问题及解决方法
问题为对接一个sso的验证模块,正确的对接姿势为,接入一个filter,然后接入一个SsoListener。
然而在接入之后,却导致了应用无法正常启动,或者说看起来很奇怪,来看下都遇到什么样的问题,以及是如何处理的?
还是web.xml,原本是这样的:(很简洁!)
xx-test encodingFilter org.springframework.web.filter.CharacterEncodingFilter encoding UTF-8 forceEncoding true encodingFilter /* spring org.springframework.web.servlet.DispatcherServlet contextConfigLocation classpath:spring/spring-servlet.xml 1 spring /
而需要添加的filter如下:
SessionFilter com.xxx.session.RedisSessionFilter SessionFilter /* com.xx.session.SSOHttpSessionListener SSOFilter com.xxx.auth.SSOFilter SSOFilter /* configFileLocation abc
另外再加几个必要的配置文件扫描!对接完成!不费事!
然后,我坑哧坑哧把代码copy过来,准备commit搞定收工!
结果,不出所料,server起不来了。也不完全是启不来了,就只是启起来之后,啥也没有了。
sso中也没啥东西,就是拦截下header中的值,判定如果没有登录就的话,就直接返回到sso的登录页去了。
那么,到底是哪里的问题呢?思而不得后,自然就开启了飞行模式了!
下面,开启debug模式!
本想直接debugspring的,结果,很明显,失败了。压根就没有进入spring的ClassPathXmlApplicationContext中,得出一个结论,spring没有被正确的打开!
好吧,那让我们退回一步,既然servlet启不来,那么,可能就是filter有问题了。
不过,请稍等,filter不是在有请求进来的时候,才会起作用吗?没道理在初始化的时候就把应用给搞死了啊!(不过其实这是有可能的)
那么,到底问题出在了哪里?
简单扫略下代码,不多,还有一个listener没有被引起注意,去看看吧。
先了解下,web.xml中的listener作用:
listener即监听器,其实也是tomcat的一个加载节点。加载顺序与它们在web.xml文件中的先后顺序无关。即不会因为filter写在listener的前面而会先加载filter。
其加载顺序为:listener->filter->servlet
接下来,就知道,listener先加载,既然没有到servlet,也排除了filter,那就debuglistener呗!
果然,debug进入无误!单步后,发现应用在某此被中断,线程找不到了,有点懵。(其实只是因为线程中被调用了线程切换而已)
我想着,可能是某处发生了异常,而此处又没有被try-catch,所以也是很伤心。要是能临时打try-catch就好了。
其实idea中是可以对没有捕获的异常进行收集的,即开启当发生异常时就捕获的功能就可以了。
然而,这大部分情况下捕获的异常,仅仅正常的loadClass()异常,这在类加载模型中,是正常抛出的异常。
//如:java.net.URLClassLoader.findClass()抛出的异常 protectedClass>findClass(finalStringname) throwsClassNotFoundException { finalClass>result; try{ result=AccessController.doPrivileged( newPrivilegedExceptionAction>(){ publicClass>run()throwsClassNotFoundException{ Stringpath=name.replace('.','/').concat(".class"); Resourceres=ucp.getResource(path,false); if(res!=null){ try{ returndefineClass(name,res); }catch(IOExceptione){ thrownewClassNotFoundException(name,e); } }else{ returnnull; } } },acc); }catch(java.security.PrivilegedActionExceptionpae){ throw(ClassNotFoundException)pae.getException(); } if(result==null){ //此处抛出的异常可以被idea捕获 thrownewClassNotFoundException(name); } returnresult; }
由于这么多无效的异常,导致我反复换了n个姿势,总算到达正确的位置。
然而当跟踪到具体的一行时,还是发生了错误。
既然用单步调试无法找到错误,那么是不是在我没有单步的地方,出了问题?
对咯,就是静态方法块!这个地方,是在首次调用该类的任意方法时,进行初始化的!也许这是我们的方向。
最后,跟踪到了一个静态块中,发现这里被中断了!
static{ //原罪在这里 CAS_EDIS_CLIENT_TEMPLATE=CasSpringContextUtils.getBean("casRedisClientTemplate",CasRedisClientTemplate.class); }
这一句看起来是向spring的bean工厂请求一个实例,为什么能被卡死呢?
只有再深入一点,才能了解其情况:
publicstaticTgetBean(Stringname,Class beanType){ returngetApplicationContext().getBean(name,beanType); }
这句看起来更像是spring的bean获取,不应该有问题啊!不过接下来一句会让我们明白一切:
publicstaticApplicationContextgetApplicationContext(){ synchronized(CasSpringContextUtils.class){ while(applicationContext==null){ try{ //没错,就是这里了,这里设置了死锁,线程交出,等待1分钟超时,继续循环 CasSpringContextUtils.class.wait(60000); }catch(InterruptedExceptionex){ } } returnapplicationContext; } }
很明显,这里已经导致了某种意义上的死锁。因为web.xml在加载到此处时,使用的是一个main线程,而加载到此处时,却被该处判断阻断。
那么我们可能想,applicationContext是一个sping管理的类,那么只要他被加载后,不可以了吗?就像下面一样:
没错,spring在加载到此类时,会调用一个setApplicationContext,此时applicationContext就不会null了。然后想像还是太美,原因如上:
publicvoidsetApplicationContext(ApplicationContextapplicationContext)throwsBeansException{ synchronized(CasSpringContextUtils.class){ CasSpringContextUtils.applicationContext=applicationContext; //梦想总是很美好,当加载完成后,通知wait() CasSpringContextUtils.class.notifyAll(); } }
ok,截止这里,我们已经找到了问题的根源。是一个被引入的jar的优雅方式阻止了你的前进。冬天已现,春天不会远!
如何解决?
很明显,你是不可能去改动这段代码的,那么你要做的,就是想办法绕过它。
即:在执行getApplicationContext()之前,把applicationContext处理好!
如何优先加载spring上下文?配置一个context-param,再加一个ContextLoaderListener,即可:
contextConfigLocation classpath:spring/applicationContext.xml org.springframework.web.context.ContextLoaderListener
在ContextLoaderListener中,会优先加载contextInitialized();从而初始化整个spring的生命周期!
/** *Initializetherootwebapplicationcontext. */ @Override publicvoidcontextInitialized(ServletContextEventevent){ initWebApplicationContext(event.getServletContext()); }
也就是说,只要把这个配置放到新增的filter之前,即可实现正常情况下的加载!
验证结果,果然如此!
最后,附上一段tomcat加载context的鲁棒代码,以供参考:
/** *Configurethesetofinstantiatedapplicationeventlisteners *forthisContext. *@returntrue
ifalllistenerswre *initializedsuccessfully,orfalse
otherwise. */ publicbooleanlistenerStart(){ if(log.isDebugEnabled()) log.debug("Configuringapplicationeventlisteners"); //Instantiatetherequiredlisteners Stringlisteners[]=findApplicationListeners(); Objectresults[]=newObject[listeners.length]; booleanok=true; for(inti=0;ieventListeners=newArrayList<>(); ArrayList