applicationContext 加载顺序错误导致服务启动失败及解决方法

本文详细描述了在Spring和SpringBoot环境中集成SSO时遇到的死锁问题,通过分析listener、filter和Spring加载顺序,最终定位到静态块的死锁。解决方案是调整context-param和ContextLoaderListener的使用,以确保Spring上下文的优先加载。

一、Spring解决方式

描述:对接一个sso的验证模块,正确的对接姿势为,接入一个 filter, 然后接入一个 SsoListener 。然而在接入之后,却导致了应用无法正常启动

 web.xml

<?xml version="1.0" encoding="UTF-8" ?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
     version="3.0">
 <display-name>xx-test</display-name>
 <filter>
  <filter-name>encodingFilter</filter-name>
  <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
  <init-param>
   <param-name>encoding</param-name>
   <param-value>UTF-8</param-value>
  </init-param>
  <init-param>
   <param-name>forceEncoding</param-name>
   <param-value>true</param-value>
  </init-param>
 </filter>
 <filter-mapping>
  <filter-name>encodingFilter</filter-name>
  <url-pattern>/*</url-pattern>
 </filter-mapping>
 <servlet>
  <servlet-name>spring</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <init-param>
   <param-name>contextConfigLocation</param-name>
   <param-value>classpath:spring/spring-servlet.xml</param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
 </servlet>
 <servlet-mapping>
  <servlet-name>spring</servlet-name>
  <url-pattern>/</url-pattern>
 </servlet-mapping>
</web-app>

 filter

<filter>
 <filter-name>SessionFilter</filter-name>
 <filter-class>com.xxx.session.RedisSessionFilter</filter-class>
</filter>
<filter-mapping>
 <filter-name>SessionFilter</filter-name>
 <url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
 <listener-class>com.xx.session.SSOHttpSessionListener</listener-class>
</listener>
<filter>
 <filter-name>SSOFilter</filter-name>
 <filter-class>com.xxx.auth.SSOFilter</filter-class>
</filter>
<filter-mapping>
 <filter-name>SSOFilter</filter-name>
 <url-pattern>/*</url-pattern>
</filter-mapping>
<context-param>
 <param-name>configFileLocation</param-name>
 <param-value>abc</param-value>
</context-param>

 分析本想直接 debug spring 的,结果,很明显,失败了。压根就没有进入 spring 的 ClassPathXmlApplicationContext 中,得出一个结论,spring 没有被正确的打开!好吧,那让我们退回一步,既然 servlet 启不来,那么,可能就是 filter 有问题了。不过,请稍等,filter 不是在有请求进来的时候,才会起作用吗?没道理在初始化的时候就把应用给搞死了啊!(不过其实这是有可能的)那么,到底问题出在了哪里?简单扫略下代码,还有一个 listener 没有被引起注意,去看看吧。先了解下,web.xml 中的 listener 作用:listener 即 监听器,其实也是 tomcat 的一个加载节点。加载顺序与它们在 web.xml 文件中的先后顺序无关。即不会因为 filter 写在 listener 的前面而会先加载 filter。其加载顺序为: listener -> filter -> servlet,接下来,就知道, listener 先加载,既然没有到 servlet, 也排除了 filter, 那就 debug listener 呗!果然,debug进入无误!单步后,发现应用在某此被中断,线程找不到了,有点懵。(其实只是因为线程中被调用了线程切换而已)我想着,可能是某处发生了异常,而此处又没有被 try-catch, 所以也是很伤心。要是能临时打 try-catch 就好了。其实 idea 中 是可以对没有捕获的异常进行收集的,即开启当发生异常时就捕获的功能就可以了。然而,这大部分情况下捕获的异常,仅仅正常的 loadClass() 异常,这在类加载模型中,是正常抛出的异常。

// 如: java.net.URLClassLoader.findClass() 抛出的异常
 protected Class<?> findClass(final String name)
   throws ClassNotFoundException
 {
   final Class<?> result;
   try {
     result = AccessController.doPrivileged(
       new PrivilegedExceptionAction<Class<?>>() {
         public Class<?> run() throws ClassNotFoundException {
           String path = name.replace('.', '/').concat(".class");
           Resource res = ucp.getResource(path, false);
           if (res != null) {
             try {
               return defineClass(name, res);
             } catch (IOException e) {
               throw new ClassNotFoundException(name, e);
             }
           } else {
             return null;
           }
         }
       }, acc);
   } catch (java.security.PrivilegedActionException pae) {
     throw (ClassNotFoundException) pae.getException();
   }
   if (result == null) {
     // 此处抛出的异常可以被 idea 捕获
     throw new ClassNotFoundException(name);
   }
   return result;
 }

 

既然用单步调试无法找到错误,那么是不是在我没有单步的地方,出了问题?对咯,就是 静态方法块!这个地方,是在首次调用该类的任意方法时,进行初始化的!也许这是我们的方向。最后,跟踪到了一个静态块中,发现这里被中断了!

static {
  // 原罪在这里
  CAS_EDIS_CLIENT_TEMPLATE = CasSpringContextUtils.getBean("casRedisClientTemplate", CasRedisClientTemplate.class);
}

这一句看起来是向 spring 的 bean工厂请求一个实例,为什么能被卡死呢?只有再深入一点,才能了解其情况:

public static <T> T getBean(String name, Class<T> beanType) {
  return getApplicationContext().getBean(name, beanType);
}

这句看起来更像是 spring 的bean获取,不应该有问题啊!不过接下来一句会让我们明白一切:

public static ApplicationContext getApplicationContext() {
  synchronized (CasSpringContextUtils.class) {
    while (applicationContext == null) {
      try {
        // 没错,就是这里了, 这里设置了死锁,线程交出,等待1分钟超时,继续循环
        CasSpringContextUtils.class.wait(60000);
      } catch (InterruptedException ex) {
      }
    }
    return applicationContext;
  }
}

很明显,这里已经导致了某种意义上的死锁。因为 web.xml 在加载到此处时,使用的是一个 main 线程,而加载到此处时,却被该处判断阻断。那么我们可能想, applicationContext 是一个 sping 管理的类,那么只要他被加载后,不就可以了吗?就像下面一样:没错,spring 在加载到此类时,会调用一个 setApplicationContext, 此时 applicationContext 就不会null了。然后想像还是太美,原因如下

public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
  synchronized (CasSpringContextUtils.class) {
    CasSpringContextUtils.applicationContext = applicationContext;
    // 梦想总是很美好,当加载完成后,通知 wait()
    CasSpringContextUtils.class.notifyAll();
  }
}

ok, 截止这里,我们已经找到了问题的根源。是一个被引入的jar的优雅方式阻止了你的前进。

解决方式:很明显,你是不可能去改动这段代码的,那么你要做的,就是想办法绕过它。即:在执行 getApplicationContext() 之前,把 applicationContext 处理好!如何优先加载 spring 上下文?配置一个 context-param, 再加一个 ContextLoaderListener, 即可:

<!-- 提前加载spring -->
<context-param>
 <param-name>contextConfigLocation</param-name>
 <param-value>classpath:spring/applicationContext.xml</param-value>
</context-param>
<listener>
 <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

在 ContextLoaderListener 中,会优先加载 contextInitialized(); 从而初始化整个 spring 的生命周期!

/**
 * Initialize the root web application context.
 */
@Override
public void contextInitialized(ServletContextEvent event) {
  initWebApplicationContext(event.getServletContext());
}

也就是说,只要把这个配置放到新增的 filter 之前,即可实现正常情况下的加载!验证结果,果然如此!最后,附上一段 tomcat 加载 context 的鲁棒代码,以供参考:

/**
   * Configure the set of instantiated application event listeners
   * for this Context.
   * @return <code>true</code> if all listeners wre
   * initialized successfully, or <code>false</code> otherwise.
   */
  public boolean listenerStart() {
    if (log.isDebugEnabled())
      log.debug("Configuring application event listeners");
    // Instantiate the required listeners
    String listeners[] = findApplicationListeners();
    Object results[] = new Object[listeners.length];
    boolean ok = true;
    for (int i = 0; i < results.length; i++) {
      if (getLogger().isDebugEnabled())
        getLogger().debug(" Configuring event listener class '" +
          listeners[i] + "'");
      try {
        String listener = listeners[i];
        results[i] = getInstanceManager().newInstance(listener);
      } catch (Throwable t) {
        t = ExceptionUtils.unwrapInvocationTargetException(t);
        ExceptionUtils.handleThrowable(t);
        getLogger().error(sm.getString(
            "standardContext.applicationListener", listeners[i]), t);
        ok = false;
      }
    }
    if (!ok) {
      getLogger().error(sm.getString("standardContext.applicationSkipped"));
      return false;
    }
    // Sort listeners in two arrays
    ArrayList<Object> eventListeners = new ArrayList<>();
    ArrayList<Object> lifecycleListeners = new ArrayList<>();
    for (int i = 0; i < results.length; i++) {
      if ((results[i] instanceof ServletContextAttributeListener)
        || (results[i] instanceof ServletRequestAttributeListener)
        || (results[i] instanceof ServletRequestListener)
        || (results[i] instanceof HttpSessionIdListener)
        || (results[i] instanceof HttpSessionAttributeListener)) {
        eventListeners.add(results[i]);
      }
      if ((results[i] instanceof ServletContextListener)
        || (results[i] instanceof HttpSessionListener)) {
        lifecycleListeners.add(results[i]);
      }
    }
    // Listener instances may have been added directly to this Context by
    // ServletContextInitializers and other code via the pluggability APIs.
    // Put them these listeners after the ones defined in web.xml and/or
    // annotations then overwrite the list of instances with the new, full
    // list.
    for (Object eventListener: getApplicationEventListeners()) {
      eventListeners.add(eventListener);
    }
    setApplicationEventListeners(eventListeners.toArray());
    for (Object lifecycleListener: getApplicationLifecycleListeners()) {
      lifecycleListeners.add(lifecycleListener);
      if (lifecycleListener instanceof ServletContextListener) {
        noPluggabilityListeners.add(lifecycleListener);
      }
    }
    setApplicationLifecycleListeners(lifecycleListeners.toArray());
    // Send application start events
    if (getLogger().isDebugEnabled())
      getLogger().debug("Sending application start events");
    // Ensure context is not null
    getServletContext();
    context.setNewServletContextListenerAllowed(false);
    Object instances[] = getApplicationLifecycleListeners();
    if (instances == null || instances.length == 0) {
      return ok;
    }
    ServletContextEvent event = new ServletContextEvent(getServletContext());
    ServletContextEvent tldEvent = null;
    if (noPluggabilityListeners.size() > 0) {
      noPluggabilityServletContext = new NoPluggabilityServletContext(getServletContext());
      tldEvent = new ServletContextEvent(noPluggabilityServletContext);
    }
    for (int i = 0; i < instances.length; i++) {
      if (!(instances[i] instanceof ServletContextListener))
        continue;
      ServletContextListener listener =
        (ServletContextListener) instances[i]
      try {
        fireContainerEvent("beforeContextInitialized", listener);
        // 调用 listener.contextInitialized() 触发 listener
        if (noPluggabilityListeners.contains(listener)) {
          listener.contextInitialized(tldEvent);
        } else {
          listener.contextInitialized(event);
        }
        fireContainerEvent("afterContextInitialized", listener);
      } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        fireContainerEvent("afterContextInitialized", listener);
        getLogger().error
          (sm.getString("standardContext.listenerStart",
                 instances[i].getClass().getName()), t);
        ok = false;
      }
    }
    return (ok);
  }

 二、SpringBoot解决方式

通过上面的描述基本上我们已经知道了问题的现象,以及分析问题的思路,还有就是解决问题的思路,其实对于SpringBoot来说,也就是解决方式的不通罢了,思路都是一样的,spring大部分是基于配置文件的,springboot能够更加的简洁,更多的是基于注解,既当A依赖于B,那就需要先加载B,然后才能加载A,所以我们可以显示的提醒它,那么@DependsOn就能完美的解决这个问题。

参考:https://www.jb51.net/article/150692.htm

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值