昨天馬上要出版本了,我做的驗證碼在windows上可以正常顯示,HP Server上裝的Redhat 9上無法顯示,愁死我了,搞了一天都沒有搞定,公司又不能上網.今天搜到了一個,先轉一把.要是測試部發現問題,又是一個致命問題,系統不能登陸!

關鍵字: Tomcat       

最近公司項目開發中遇到的一個問題,整理一下,和大家分享。

驗證碼無法顯示的問題,驗證碼的代碼就是google上查找到的最常見的代碼,服務器采用resin部署于linux或unix。不是常見的out.clear()問題,這次的問題發現在一個我壓根就沒有想到的地方,profile DISPLAY 環境變量。

1) 問題描述:
登錄頁面等有驗證瑪顯示的頁面,通常可以正確顯示驗證碼圖片,但是在某些情況下發現驗證碼圖片無法顯示,并且目前只發生在linux/unix平臺,windows下正常.而且和resin/jdk版本無關.

bug的直接表現是表現為ie下是紅叉,firefox下無實現.將驗證碼圖片的地址在ie輸入框中輸入,則頁面報錯:

代碼
  1. 500 Servlet Exception   
  2. java.lang.NoClassDefFoundError   
  3.     at java.lang.Class.forName0(Native Method)   
  4.     at java.lang.Class.forName(Class.java:164)   
  5.     at java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment(GraphicsEnvironment.java:68)   
  6.     at java.awt.image.BufferedImage.createGraphics(BufferedImage.java:1141)   
  7.         at com.asiainfo.aimc.wmail.action.CreateImageServlet.doGet(CreateImageServlet.java:104)  

 

這里的java.lang.NoClassDefFoundError 極其誤導人,一直以為是CLASSPATH或者jar包的問題,所以反復檢查resin和jdk版本。
始終無法找到問題,只好嘗試追查jdk源碼,看到底發生了什么。

2) jdk源碼追查

調用的servlet:
BufferedImage bi = new BufferedImage(...)
Graphics2D g = bi.createGraphics();

查jdk: BufferedImage.createGraphics():
GraphicsEnvironment env =
GraphicsEnvironment.getLocalGraphicsEnvironment();

再查GraphicsEnvironment.getLocalGraphicsEnvironment:
String nm = (String) java.security.AccessController.doPrivileged
(new sun.security.action.GetPropertyAction
("java.awt.graphicsenv", null));
......
localEnv = GraphicsEnvironment) Class.forName(nm).newInstance();
......
問題應該和nm有關,這里明顯是一個類似工廠模式的設計,"java.awt.graphicsenv"到nm 然后Class.forName() 生成GraphicsEnvironment對象。
由于代碼在jdk中,不方便修改,因此單獨將這些代碼提出來到簡單的測試類 Test.java:

3) 測試代碼分析

代碼
  1. public class Test {   
  2.     public static void main(String[] args) {   
  3.         String nm = (String) java.security.AccessController.doPrivileged   
  4.         (new sun.security.action.GetPropertyAction   
  5.          ("java.awt.graphicsenv"null));   
  6.            
  7.         System.out.println(nm);   
  8.            
  9.         try {   
  10.             Class.forName(nm).newInstance();   
  11.         } catch (Throwable e) {   
  12.             System.out.println("error=" + e.getClass().getName());   
  13.                
  14.             e.printStackTrace();   
  15.         }    
  16.     }   
  17. }  

在windows平臺下運行,結果正常,打印:
sun.awt.Win32GraphicsEnvironment

 

將代碼放到出問題的resin安裝所在的linux平臺,手工編譯運行:
javac Test.java
java -cp . Test

報錯,打印為:

 

代碼
  1. sun.awt.X11GraphicsEnvironment   
  2. Throwable=java.lang.InternalError   
  3. java.lang.InternalError: Can't connect to X11 window server using '10.3.18.16' as the value of the DISPLAY variable.   
  4.         at sun.awt.X11GraphicsEnvironment.initDisplay(Native Method)   
  5.         at sun.awt.X11GraphicsEnvironment.access$000(X11GraphicsEnvironment.java:53)   
  6.         at sun.awt.X11GraphicsEnvironment$1.run(X11GraphicsEnvironment.java:142)   
  7.         at java.security.AccessController.doPrivileged(Native Method)   
  8.         at sun.awt.X11GraphicsEnvironment.<clinit>(X11GraphicsEnvironment.java:131)   
  9.         at java.lang.Class.forName0(Native Method)   
  10.         at java.lang.Class.forName(Class.java:164)   
  11.         at Test.main(Test.java:13)  

 

從錯誤信息" Can't connect to X11 window server using '10.3.18.16' as the value of the DISPLAY variable."來看,和DISPLAY環境變量有關
執行unset再運行可以發現問題消失:
$> unset DISPLAY
$> java -cp . Test
sun.awt.X11GraphicsEnvironment
$>

在此情況下(unset DISPLAY )下重新啟動resin,發現驗證碼可以正常顯示。

4) 解決的方法:
必須保證resin運行時DISPLAY 環境變量沒有設置,如果resin運行的環境有其他要求必須使用DISPLAY,則可以在運行resin前使用unset清除. 建議的簡單而有效的方法是直接修改resin/bin/httpd.sh文件,在第二行(具體行數無所謂,但必須在最后一行前)插入:
#! /bin/sh
unset DISPLAY
#....

5)疑惑
1. Can't connect to X11 window server using '10.3.18.16' as the value of the DISPLAY variable
為什么要去連X11 window server ?不懂

2. 從Test.java運行看拋出的是Error : java.lang.InternalError,但是頁面上顯示的是java.lang.NoClassDefFoundError,看了看源代碼也沒有發現先catch 后throws的錯誤處理,不清楚這里的具體處理,不方便繼續追查,作罷。