這是近期工作中遇到的一個問題,cxf在glassfish下timeout設置出現問題,進而引發的關于classloader, JAX-WS的一些小故事,很驚訝的發現cxf在這種情況下根本沒有辦法運行于glassfish平臺。
關鍵字:glassfish, cxf, classloader, JAX-WS, metro。
首先看問題的發生,我們有一個webservice的客戶端,使用cxf開發,原來運行于weblogic,目前準備移植到glassfish。異常測試中發現timeout設置不再有效,在glassfish平臺上timeout時間似乎是無限?測試中試過等待10分鐘也沒有timeout,socket一直連著,客戶端一直在等應答。
于是準備增加timeout的設置到cxf中,下面是cxf的timeout的典型設置:
Client client = ClientProxy.getClient(this.port);
HTTPConduit http = (HTTPConduit) client.getConduit();
HTTPClientPolicy httpClientPolicy = new HTTPClientPolicy();
httpClientPolicy.setConnectionTimeout(30 * 1000);
httpClientPolicy.setReceiveTimeout(60 * 1000);
http.setClient(httpClientPolicy);
這段代碼在weblogic中是正常工作的,但是在glassfish上就出現問題,“Client client = ClientProxy.getClient(this.port);” 這行會拋出一個異常ClassCastException:
javax.xml.ws.soap.SOAPFaultException: com.sun.xml.ws.client.sei.SEIStub cannot be cast to org.apache.cxf.frontend.ClientProxy
google了一下發現這個問題的出現已經在cxf的issue列表中, CXF-2237:
https://issues.apache.org/jira/browse/CXF-2237
從這個網址得到的信息:
1. That error would mean that the Sun JAX-WS implementation is being picked up instead of the CXF version.
2. Classpath issue picking up wrong JAX-WS implementation.
原因似乎很清晰了,但是沒有給出解決方法,繼續google發現類似的問題在jboss平臺也有發現,cxf有給出解決方法。
于是郁悶了,找了找資料,發現問題可能和JAX-WS有關:
1. JAX-WS 是用于web service的java api,定義在JSR 224.
2. sun提供了JAX-WS的一個實現Metro,包含在glassfish中。
3. sun的實現后來加入了jdk6(不過package和Metro不同)
4. apache cxf 是另外一個JAX-WS實現
從上面的異常看,"com.sun.xml.ws.client.sei.SEIStub cannot be cast to org.apache.cxf.frontend.ClientProxy", 像是從"sun impl" -> "cxf impl"的轉換出問題,也就是說runtime時實際運行的是sun的JAX-WS實現,而不是cxf的實現。這個和CXF-2237的描述是一致的,因此問題基本定位出來了:glassfish中cxf的實現沒有被裝載成功。
隨即查找了一下關于JAX-WS 實現裝載的資料,application是可以通過Provider SPI來選擇不同的JAX-WS實現的,在JSR 224: JAX-WS 2.x 中的chapter 6.2.1 有選擇Provider implementation的規則:
1. If a resource with the name of META-INF/services/javax.xml.ws.spi.Provider exists, then its first line, if present, is used as the UTF-8 encoded name of the implementation class.
意思是說查找名為META-INF/services/javax.xml.ws.spi.Provider的資源,如果存在,那么它的第一行,就是實現類的名字。
我們試著打開cxf的jar文件,發現的確有上述文件,內容只有一行:org.apache.cxf.jaxws.spi.ProviderImpl。然后查找了一下glassfish的jar文件,在glassfish安裝目錄下的lib\webservice-rt.jar中找到了JAX-WS的實現,同樣有META-INF/services/javax.xml.ws.spi.Provider文件,內容為: com.sun.xml.ws.spi.ProviderImpl.
2. If the ${java.home}/lib/jaxws.properties file exists and it is readable by the java.util.Properties.load(InputStream) method and it contains an entry whose key
is javax.xml.ws.spi.Provider, then the value of that entry is used as the name of the implementation class.
類似的,通過${java.home}/lib/jaxws.properties 文件來設置。
3. If a system property with the name javax.xml.ws.spi.Provider is defined, then its value is used as the name of the implementation class.
通過系統屬性javax.xml.ws.spi.Provider來設置。
4. Finally, a default implementation class name is used.
最后,默認實現,即使就是jdk中的sun的實現,猜測是Metro的某個版本
從上述規則來看,明顯當前是遵循了第一個規則,從資源META-INF/services/javax.xml.ws.spi.Provider中讀取實現類。而且雖然application中有cxf.jar的存在并且有名為META-INF/services/javax.xml.ws.spi.Provider的資源,但是因為application的classloader在裝載資源時,按照標準的classloader機制,會首先代理給parent classloader,因此最后實際是system classloader首先嘗試裝載資源,glassfish下lib\webservice-rt.jar是屬于system classpath,glashfish的system classloader會查找到并裝載webservice-rt.jar中的META-INF/services/javax.xml.ws.spi.Provider。這樣application中的classloader就沒有機會裝載cxf的META-INF/services/javax.xml.ws.spi.Provider,而是直接使用glassfish system classloader裝載好的javax.xml.ws.spi.Provider,自然就是Metro的實現。
現在關鍵的問題在于這個規則是第一順序位,后面的2,3根本沒有機會。因此想裝載cxf的JAX-WS實現,就只有想辦法改變classloader的裝載機制,想辦法讓application中的classloader有機會裝載到cxf的JAX-WS實現。
類似的class 裝載的問題在cxf上就比較常見了,之前在weblogic上也經常遇到類似的,解決的思路就是改變classloader的默認裝載機制,讓application中的classloader自己去裝載而不是代理給system classloader。
weblogic中對此有專門的設置prefer-application-packages,在weblogic-ejb-jar.xml或者weblogic-application.xml中加入:
<prefer-application-packages>
<package-name>javax.jws.*</package-name>
</prefer-application-packages>
就可以指示weblogic,對于上述的package,優先實現application的classloader而不是使用system classloader,從而解決這個問題。
因為問題的解決思路就很明顯了: 在glassfish中找到類似于prefer-application-packages的方法。
首先找到的是glassfish中的delegate設置,看delegate的介紹:
(optional) If true, the web module follows the standard class loader delegation model and delegates to its parent class loader first before looking in the local class loader. You must set this to true for a web application that accesses EJB components or that acts as a web service client or endpoint.
粗看正是我們想要的,再一看發現不行,delegate的使用是有限制的:
1. delegate只在sun-web.xml有,只能用于web app,不能用于普通的application
2. delegate是有限制的,對于"java.*"和"javax.*"的包不能生效的
隨后的查找發現,非常遺憾,glassfish沒有提供其他的類似機制,因此試圖改變classloader的想法陷入絕境。
只好先考慮其他的方法,一個思路就是讓glassfish的system classloader直接load到cxf,方法如下:
1.將cxf的lib包(或者只是META-INF/services/javax.xml.ws.spi.Provider文件)放到glassfish的system classpath下并在lib\webservice-rt.jar之前
2.刪除lib\webservice-rt.jar
這個試過了,glassfish直接起不來
但這些都不是足夠妥當,最后考慮到既然大家都是JAX-WS實現,而且sun的Metro也算做的不錯,因此將錯就錯,考慮直接使用Metro好了。就當前遇到的timeout設置的問題,在Metro中是非常容易解決的:
Map<String, Object> requestContext = ((BindingProvider) service).getRequestContext();
requestContext.put("com.sun.xml.ws.connect.timeout", 30 * 1000);
requestContext.put("com.sun.xml.ws.request.timeout", 60 * 1000);
至此這次timeout的設置問題總算解決了,但是,依然沒有辦法解決glassfish的classloader問題,以后如果遇到類似的需要application優先裝載特定類的情況,還是會遭遇同樣的困境。這里比較奇怪的是,為什么glassfish會沒有類似的設置,按說既然glassfish已經給出了delegate這個設置,說明glassfish已經意思到有這個需要并且也給出了部分解決方法,但是為什么對應于application確不給出任何解決方案呢?百思不得其解。
而從上面的描述也可以看出,如果不使用特殊的方法,正常情況下,applicatio是沒有辦法裝載到cxf的JAX-WS實現的,實際在runtime時跑的是metor的實現。