1. 程式人生 > >一個applicationContext 載入錯誤導致的阻塞解決小結

一個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 之前,即可實現正常情況下的載入!

  驗證結果,果然如此!