• 一个applicationContext 加载错误导致的阻塞解决小结


      问题为对接一个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>
      

      另外再加几个必要的配置文件扫描!对接完成!不费事!
      然后,我坑哧坑哧把代码copy过来,准备 commit 搞定收工!


      结果,不出所料,server 起不来了。也不完全是启不来了,就只是启起来之后,啥也没有了。

      sso 中也没啥东西,就是拦截下 header 中的值,判定如果没有登录就的话,就直接返回到 sso 的登录页去了。

      那么,到底是哪里的问题呢?思而不得后,自然就开启了飞行模式了!

    下面,开启debug模式!

      本想直接 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;
        }

      由于这么多无效的异常,导致我反复换了n个姿势,总算到达正确的位置。
      然而当跟踪到具体的一行时,还是发生了错误。

    既然用单步调试无法找到错误,那么是不是在我没有单步的地方,出了问题?

    对咯,就是 静态方法块!这个地方,是在首次调用该类的任意方法时,进行初始化的!也许这是我们的方向。

    最后,跟踪到了一个静态块中,发现这里被中断了!

        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 的鲁棒代码,以供参考(坐标: org.apache.catalina.core.StandardContext):

        /**
         * 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);
    
        }
    View Code
  • 相关阅读:
    SpringCloud06Config远程配置
    SpringCloud05Zuul网关
    SpringCloud02Ribbon负载均衡
    Springboot06Dubbo+Zookeeper
    SpringCloud03Feign负载均衡
    @SpringbootApllication主启动类注解分析
    SpringCloud04Hystrix熔断机制
    SpringCloud01Eureka注册中心
    Redis笔记
    C++while循环特殊用法
  • 原文地址:https://www.cnblogs.com/yougewe/p/9948909.html
Copyright © 2020-2023  润新知