2013年8月12日 星期一

Apache CXF 筆記

為啥要玩Apache CXF呢?簡單的說就是要從它跟Jersey之間挑一個來用,目標是用它作為Web Application的後端跟GWT 開發的前端(透過 Resty GWT)溝通使用。

至於為什麼不直接用GWT RPC呢?因為公司的Architects覺得GWT有點朝不保夕的感覺,他們希望如果哪天GWT死掉了,前端可以有機會一點一點的換成Javascript native implementation...我是覺得這有點異想天開啦,但是看他們好像有人有基於這種架構開發過還很舒服的樣子,我就不多說了,玩下去就知道。

Jersey 公司已經一堆人會了,也有人用過,於是我就自動請纓來Survey CXF了...雖然我覺得這玩意八成不會被接受(開玩笑,一堆人會就代表開發成本低、有默契,這玩意很多時候比架構重要)。

CXF的基本介紹

CXF 是 Apache Foundation 底下開發SOAP、JAX-WS、JAX-RS應用的一個較為現代化(?)的應用框架。 它支援了幾個重要的Feature:

JAX-WS Support

基於JSR 224的實現,不多廢話,這玩意很老,overhead 很重,但跟異質系統做資料交換且要確保資料型態的正確性還是得靠這個。

Spring Integration

CXF的核心在2.4版以前跟Spring緊密的接合在一起,2.4版後將Spring作為configuration的一種選項(但這是一個強力建議的選項,不用Spring...你得搞懂它的核心是由哪些部件組成然後自己組一個才行)。

Aegis & JAXB Data Binding

CXF 提供在"Databinding"層提供多種選擇,它所謂的Databinding指的是Object to Structured Text(XML, Json, etc...)之間的轉換。

JAX-RS(RESTful Services) Support

基於JSR 339的實現,這篇文章主要想要玩想要討論的東西。JSR 339還蠻新的(final release: 24 May, 2013),CXF還沒有很好的實現它,想單純玩JAX-RS2.0的建議用Jersey,畢竟Jersey是reference implementation,沒做好Oracle就該打屁股了。

Apache 2.0 Licensed

Apache 出品,當然是Apache 2.0 License。


開始一個新的CXF JAX-RS專案


學一個東西,當然先從它的文件與Sample Code開始(...這是指好的情況,有時候人品爆發,就會從看Code 開始)。CXF如果完全照它最簡單的設定走,其實還蠻簡單的,首先,讓我們用CXF所提供的maven archetype來generate 一個Eclipse Project。

如果用的是Eclipse + M2E來開發的,可以很簡單的用m2e的new maven project 來開始新專案,當中在選擇archetype時,建議用 catalog: http://repo1.maven.org/maven2/archetype-catalog.xml 下的cxf-jaxrs-service,來生成project,generate 出來的結果最有趣的部份在build區塊內,讓我們分段的來看一下它的內容:
   <plugin><!--get a random port from system -->
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>build-helper-maven-plugin</artifactId>
    <version>1.5</version>
    <executions>
     <execution>
      <id>reserve-network-port</id>
      <goals>
       <goal>reserve-network-port</goal>
      </goals>
      <phase>process-test-resources</phase>
      <configuration>
       <portNames>
        <portName>test.server.port</portName>
       </portNames>
      </configuration>
     </execution>
    </executions>
   </plugin>
   <plugin>
    <groupId>org.apache.tomcat.maven</groupId>
    <artifactId>tomcat7-maven-plugin</artifactId>
    <executions>
     <execution>
      <id>start-tomcat</id>
      <goals>
       <goal>run-war</goal>
      </goals>
      <phase>pre-integration-test</phase>
      <configuration>
       <port>${test.server.port}</port>
       <path>/jaxrs-service</path>
       <fork>true</fork>
       <useSeparateTomcatClassLoader>true</useSeparateTomcatClassLoader>
      </configuration>
     </execution>
     <execution>
      <id>stop-tomcat</id>
      <goals>
       <goal>shutdown</goal>
      </goals>
      <phase>post-integration-test</phase>
      <configuration>
       <path>/jaxrs-service</path>
      </configuration>
     </execution>
    </executions>
   </plugin>

可以看見這段是先利用build-helper-maven-plugin 先去取得一個OS上沒有用到的IP,然後存到變數test.server.port上,這樣之後tomcat7-maven-plugin要啟動tomcat7時,就有了一個適當的IP可以用了。

而Tomcat7-maven-plugin會在pre-integration-test phase做start up,然後在post-integration-test phase做shut down,這樣在integration-test phase 期間,unit test case執行的時候就有正確的環境可以執行,底下是maven跑integration-test 的設定:
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>2.8.1</version>
    <executions>
     <execution>
      <id>integration-test</id>
      <goals>
       <goal>integration-test</goal>
      </goals>
      <configuration>
       <systemPropertyVariables>
        <service.url>http://localhost:${test.server.port}/cxf_demo2</service.url>
       </systemPropertyVariables>
      </configuration>
     </execution>
     <execution>
      <id>verify</id>
      <goals>
       <goal>verify</goal>
      </goals>
     </execution>
    </executions>
   </plugin>

像這樣的對continuous integration的友善支持,對以現代化開發流程為前提要選用framework來說,是很重要的。
像我一樣,跟Maven不很熟的朋友,建議看一下 Maven - Introduction to the Build Life-cycle,了解一下Maven會跑哪些步驟,你可以做些什麼。



CXF在Container裡面的設定

CXF 在Application configuration上,在2.4版以前是與Spring Framework耦合的,也就是說,你得給一份Spring的xml configuration。
在web.xml上,你有兩個方式可以apply這個設定:
宣告一個Spring的ContextListener

 <context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>WEB-INF/beans.xml</param-value>
 </context-param>

 <listener>
  <listener-class>
   org.springframework.web.context.ContextLoaderListener
  </listener-class>
 </listener>

或者,在CXFServlet 的宣告裡,透過init param 指定
<servlet>
 <servlet-name>CXFServlet1</servlet-name>
 <servlet-class>
  org.apache.cxf.transport.servlet.CXFServlet
 </servlet-class>
 <init-param>
  <param-name>config-location</param-name>
  <param-value>/WEB-INF/beans1.xml</param-value>
 </init-param>
 <load-on-startup>1</load-on-startup>
</servlet>

我並不是很喜歡採用一個web service方案的時候,還要被強迫在專案指定特定的DI framework,但我可以理解基於CXF的龐大與複雜度,一個適當的DI framework可以幫忙做很多骯髒活。
不過在Configuration DI 的Solution這個方面的選擇,個人認為Jersey做了一個比較漂亮的決定 - HK2 (HK2是一個相容於CDI、非常的輕量、一旦把作為一個組裝器的事情給做完,可以把後續移交給其他DI Container的DI framework)。
在2.4版之後,CXF開始將Spring從核心切割出去,想要開始一個不基於Spring做組裝的CXF核心,你可以在web.xml裡下這樣宣告:
<servlet>
 <servlet-name>NonSpringServlet</servlet-name>
 <servlet-class>
  org.apache.cxf.transport.servlet.CXFNonSpringServlet
 </servlet-class>
 <load-on-startup>1</load-on-startup>
</servlet>
簡單的說,不想要Spring就宣告個CXFNonSpringServlet(...這真的是很醜),這個Servlet 提供了一缸子的<init-param>給使用者客製化,大部分的值接受以換行符號或逗點分隔來給予多個參數(...這真的有夠醜,誰想要自己的web.xml被搞成這樣啊?)不喜歡這個作法的朋友,請create一個java class 去繼承CXFNonSpringServlet,事實上,目前的CXFServlet就是透過繼承它來實作的(也就是有地方可以給你挖Code、抄Code啦)。
於是,為了實現Google Guice + CXF,我自己實作了一個MyGuiceCXFJaxrsServlet.java:
public class MyGuiceCXFJaxrsServlet extends CXFNonSpringServlet {
 private static final long serialVersionUID = 4798627144134171969L;

 @Override
 public void init(ServletConfig servletConfig) throws ServletException {
  super.init(servletConfig);
  
  Bus bus = this.getBus();
  BusFactory.setDefaultBus(bus);

  JAXRSServerFactoryBean jaxrsFacBean = new JAXRSServerFactoryBean();
  jaxrsFacBean.setBus(getBus());

  
  // config OUT interceptors...
  configInInterceptors( jaxrsFacBean);
  
  // config IN interceptors...
  configOutInterceptors(jaxrsFacBean);
  
  // determine target jax-rs services
  configResources(jaxrsFacBean);
  
  jaxrsFacBean.setProvider(new JacksonJsonProvider());
  jaxrsFacBean.create();
 }
 /**
  * 
  * @param jaxrsFacBean
  */
 protected void configInInterceptors(JAXRSServerFactoryBean jaxrsFacBean) {
  List<Interceptor<? extends Message>> interceptors = 
    new ArrayList<Interceptor<? extends Message>>();
  
  interceptors.add(new JsonpInInterceptor());
  jaxrsFacBean.setInInterceptors(interceptors);
 }
 /**
  * 
  * @param jaxrsFacBean
  */
 protected void configOutInterceptors(JAXRSServerFactoryBean jaxrsFacBean) {
  List<Interceptor<? extends Message>> interceptors = 
    new ArrayList<Interceptor<? extends Message>>();
  interceptors.add(new JsonpPreStreamInterceptor());
  interceptors.add(new JsonpPostStreamInterceptor());
  
  jaxrsFacBean.setOutInterceptors(interceptors);
  
//  interceptors = new ArrayList<Interceptor<? extends Message>>();
//  interceptors.add(new FaultOutInterceptor());
//  jaxrsFacBean.setOutFaultInterceptors(interceptors);
 }
 /**
  * 
  * @param jaxrsFacBean
  */
 protected void configResources(JAXRSServerFactoryBean jaxrsFacBean){
  
  
  //TODO make this configurable...
  
  Injector injector = Guice.createInjector(new AbstractModule() {
   @Override
   protected void configure() {
    bind(User.class).toInstance(new User("Zanyking"));
    bind(CustomerService.class);    
   }
  });
  
  jaxrsFacBean.setResourceClasses(CustomerService.class);
  jaxrsFacBean.setResourceProvider(CustomerService.class, 
    new SingletonResourceProvider(
      injector.getInstance(CustomerService.class)));
 }
 
}
最主要將Guice與CXF接合起來的地方是在 ConfigResource(JAXRSServerFactoryBean) 這個method 的實作裡,我用Guice生成了一個基於jax-rs的CustomerService物件,給CXF去用。
對於想要直接custom一個CXF系統來用的情境,這個method的傳入參數:JAXRSServerFactoryBean的API就是關鍵,今天如果有人想要搞一個GuiceCXF還是WeldCXF,那需要的就是從這顆bean開始去挖CXF的架構來設計。
PS:其實,我還另外挖到一支 org.apache.cxf.jaxrs.servlet.CXFNonSpringJaxrsServlet ,它的介紹竟然只有這樣...這讓我感到很悲傷啊。

結論

我對於CXF感到不滿意的地方:
  1. JAX-RS 2.0的支援並不完全。應該說它還沒做完,在目前version 2.7.6,CXF並不支援javax.ws.rs.client.Client(我下了3.0.0-SNAPSHOT來看,看來未來它會把API都接好,但目前這個snapshot玩下去功能整個不會動是爛的...悲劇),你得用它的專屬WebClient API來撰寫測試。
  2. Documentation關於NonSpring這段的使用不完整且不友善,沒doc也就算了,沒sample code就很糟糕了,大部分的使用案例得要開發者看著Spring bean.xml去自己腦內翻譯成none spring version。

我對於CXF感到滿意的地方:
  1. 如果你喜歡XML configuration、Spring就是你的菜,那CXF太棒了。
  2. 出了很久,所以會發生的問題、範例、效能調整之類的東西,該解的都解了,如果用出問題,比較高的機會是開發者自己的問題。
  3. Maven有給archetype,這不是什麼了不起的玩意,但它有做。
雖然我想在目前跑的專案上,因為這玩意還不完全支援JAX-RS2.0,而且XML configuration對於專案成員來說也不是特別偏愛(都在用Guice了,你可以想見有很高的機率有人是XML hater)所以大概要出局,但若要我給這個產品一個分數,我想她還蠻OK的,基礎架構設計非常的java classic,當初開架構的沒有為了跟風亂搞什麼太fancy的東西結果四不像,一旦搞懂它的模型,設定上就四平八穩。
可見的未來它可能得遭遇一段陣痛期,我認為它得做好的工作有:
  • JAX-RS2.0 Compliant:沒啥好說,越快支援越好,還不支援的每天它都正在流失客群。
  • 去把跟其他DI的部份整好:不是誰都想用Spring的,而照顧其他DI framework使用者的需求不是一句:『自己去看Code』就結束的,做solution survey會真的挖下去看code的開發者可沒想像中那麼多。
  • 網站本身Doc的水準有待加強:我知道Apache是Open Source Java界的丐幫(專案武林最多、組織最大、什麼千奇百怪的需求都照顧),但一個向上爬、積極爭取眼球的專案最重要的就是doc與sample code,有很多外人幫你試出好的用法不代表你就不用去整理。

不曉得有多少人在用CXF?有在用的、或有什麼其他關於CXF的問題的朋友,歡迎回應。