運用Spring DM和CXF來實現WebService的動態發布

火星人 @ 2014-03-10 , reply:0


  在JAXWS2.0發布以前,用JAVA發布一個WebService是一件相當複雜的工作,令很多開發高手都望而卻步;但隨著JAXWS2.0、2.1版本的發布,通過大量使用JAVA annotation特性,以及運用JAXB20規範來統一數據展示,從而大大簡化和規範了開發過程,一些新的開源框架也隨之誕生.Apache CXF就是其中的佼佼者.它實現了JCP與Web Service2.1中一些重要標準.CXF簡化了構造,集成,面向服務架構(SOA)業務組件與技術的靈活復用.在CXF中,Service使用WSDL標準定義並能夠使用各種不同的消息格式(或binding)和網路協議(transports)包括SOAP、XML(通過HTTP或JMS)進行訪問.

  而OSGI技術更是JAVA社區近來的熱點,它將面向contract,插件化,組件化的設計思想上升到一個理論的高度,推出了一系列的規範和參考實現;Spring DM更是在OSGI基礎上將Spring的DI能力擴充到OSGI層面,並對OSGI規範中缺少的部分加以補充和完善,使之成為更具有實踐應用價值的框架.

  這篇文件將介紹如何運用這兩個開源項目來實現WebService的組件化發布.

  環境:

  Eclipse3.4

  JDK1.605

  CXF2.1.1

  Spring DM1.0.3

  第一步:在Eclipse下創建新的工作區;

  第二步:導入Spring DM的Bundle,主要導入以下幾個Bundle:

  (1)org.springframework.bundle.osgi.core

  (2)org.springframework.bundle.osgi.extender

  (3)org.springframework.bundle.osgi.io

  (4)org.springframework.bundle.spring.aop

  (5)org.springframework.bundle.spring.beans

  (6)org.springframework.bundle.spring.context

  (7)org.springframework.bundle.spring.core

  (8)org.springframework.osgi.aopalliance.osgi

  第三步:導入其他Eclipse下的Bundle

  (1)org.apache.commons.logging

  (2)org.eclipse.equinox.http.jetty

  (3)org.eclipse.equinox.http.servlet

  (4)org.eclipse.osgi.services

  (5)org.mortbay.jetty

  設計的原則:

  (1) CXF將被獨立封裝成一個Bundle;

  (2) CXF Bundle對外提供OSGI服務,其他Bundle可以利用這個服務來發布Web Service;

  (3) CXF內部預設使用了Spring的庫,而Spring DM環境也帶了Spring 的庫,所以在實現CXF Bundle的時候要使CXF運行於None Spring模式;

  (4) 充分考慮系統的封裝型和可擴展能力;

  (5) Web Service是動態發布;

  實現:

  OSGI服務側的應用場景有兩個,一個是將Web容器內嵌到OSGI環境中,另一種是將OSGI環境以WAR的方式發布到獨立的Web容器中.這裡使用的是第一種方式.所以在前面的Bundle列表中可以看到Jetty的Bundle.

  如果使用方案一就需要通過一個OSGI的HTTP服務將外部的HTTP請求轉發給CXF內部的Servlet,所以這裡要使用OSGI的服務Bundle.

  CXF2.1.1版本中包含很多第三方的庫,這裡需要清理一下,不是所有庫都需要最終封裝到CXF Bundle中,清理的庫包括:

  (1) CXF tools需要的庫;包括jaxb-xjc-2.1.6.jar

  (2) Jms庫,這裡不需要使用Jms;

  (3) Spring的所有庫;

  (4) JAXB2.1的庫,這兩個庫可以放到JDK6的libendorse目錄下,做為公共庫,替代JVM預設的JAXB2.0的庫;

  (5) XMLBean的庫,這裡不使用XMLBean的編碼方式;

  (6) Log庫,我們使用OSGI中的logging Bundle來實現log功能;

  (7) Javax.ws的庫,使用的環境是JDK6,這個庫是給JDK5準備的;

  (8) Jetty的庫;

  (9) Js和Json的庫;

  (10) Velocity庫;

  這裡保留了REST方式需要的庫.的保留的庫列表:

  abdera-core-0.4.0-incubating.jar

  abdera-extensions-html-0.4.0-incubating.jar

  abdera-extensions-json-0.4.0-incubating.jar

  abdera-extensions-main-0.4.0-incubating.jar

  abdera-i18n-0.4.0-incubating.jar

  abdera-parser-0.4.0-incubating.jar

  abdera-server-0.4.0-incubating.jar

  commons-codec-1.3.jar

  commons-httpclient-3.1.jar

  commons-lang-2.4.jar

  cxf-2.1.1.jar

  cxf-manifest.jar

  FastInfoset-1.2.2.jar

  geronimo-activation_1.1_spec-1.0.2.jar

  geronimo-annotation_1.0_spec-1.1.1.jar

  geronimo-javamail_1.4_spec-1.3.jar

  geronimo-jaxws_2.1_spec-1.0.jar

  geronimo-stax-api_1.0_spec-1.0.1.jar

  htmlparser-1.0.5.jar

  jaxen-1.1.jar

  jdom-1.0.jar

  neethi-2.0.4.jar

  opensaml-1.1.jar

  saaj-api-1.3.jar

  saaj-impl-1.3.jar

  slf4j-api-1.3.1.jar

  slf4j-jdk14-1.3.1.jar

  stax-utils-20060502.jar

  wsdl4j-1.6.1.jar

  wss4j-1.5.4.jar

  wstx-asl-3.2.4.jar

  xml-resolver-1.2.jar

  XmlSchema-1.4.2.jar

  xmlsec-1.4.0.jar

  創建CXFBundle:

  創建時候一定要選擇OSGI標準選項:

  按照CXF的要求,如果使用非Spring的場景,需要使用CXFNonSpringServlet來接受HTTP請求,所以需要將這個Servlet註冊給OSGI的Http服務:

  public class CXFWrapperServlets extends CXFNonSpringServlet{

  private static final long serialVersionUID = -6994879522066465447L;

  private static Map BusMap = new HashMap();

  public void init(ServletConfig servletConfig) throws ServletException {

  super.init(servletConfig);

  BusMap.put("servletBus", getBus());

  Bus bus = CXFWrapperServlets.getMyBus("servletBus");

  BusFactory.setDefaultBus(bus);

  }

  ...

  註冊類:

  public class HttpServiceTracker extends ServiceTracker{

  public HttpServiceTracker(BundleContext context) {

  super(context, HttpService.class.getName(), null);

  }

  public Object addingService(ServiceReference reference) {

  HttpService httpService = (HttpService) context.getService(reference);

  try {

  Servlet ss = (Servlet)new CXFWrapperServlets();

  httpService.registerServlet("/services",ss , null, null);

  } catch (Exception e) {

  e.printStackTrace();

  }

  return httpService;

  }

  注意這裡的註冊路徑」 /services」,就是說訪問http://localhost/services/*的所有HTTP請求都將發送給CXFWrapperServlets來處理.

  定義Web Service註冊介面:

  public interface WSRegister {

  public void Regiser(Object impl,String address);

  }

  這裡的參數是Web Service的實現類和訪問這個Web Service 的URL地址.介面實現類:

  public class WSRegisterImpl implements WSRegister{

  public void Regiser(Object impl,String address){

  EndpointImpl endpoint = new EndpointImpl(impl);

  endpoint.setAddress(address);

  endpoint.publish();

  ...

  下面是通過Spring DM提供的能力來發布這個OSGI服務.分別定義兩個XML文件用於發布服務,一個文件是這樣的:

  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  xsi:schemaLocation="http://www.springframework.org/schema/beans

  http://www.springframework.org/schema/beans/spring-beans.xsd">

  context-class-loader="service-provider"/>

  另一個文件是:

  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  xmlns:osgi="http://www.springframework.org/schema/osgi"

  xsi:schemaLocation="http://www.springframework.org/schema/beans

  http://www.springframework.org/schema/beans/spring-beans.xsd

  http://www.springframework.org/schema/osgi

  http://www.springframework.org/schema/osgi/spring-osgi.xsd">

  interface="CXFWrapper.interfaces.WSRegister" context-class-loader="service-provider">

  這兩個文件都要放到META-INF/spring目錄下,這樣Spring DM的運行時才會動態的實現OSGI的註冊.

  在CXFBundle的BundleActivator類中實現HTTP服務的啟動:

  public void start(BundleContext context){

  httpServiceTracker = new HttpServiceTracker(context);

  httpServiceTracker.open();;

  }

  然後是整理MANIFEST.MF文件:

  Dependencies TAB頁面在導入包選擇中,導入如下包:

  Import-Package:

  javax.servlet;version="2.4.0",

  javax.servlet.http;version="2.4.0",

  org.apache.commons.logging;version="1.0.4",

  org.osgi.framework;version="1.4.0",

  org.osgi.service.http;version="1.2.0",

  org.osgi.util.tracker;version="1.3.3"

  Runtime TAB頁面在導出包中要導出註冊介面:

  Export-Package: CXFWrapper.interfaces

  在Classpath輸入框中,選擇上面列出的所有保留的庫.

驗證當前的工作正確 

 到目前為止,CXFBundle的基本工作已經完成,現在需要寫一個Web Service的應用來驗證當前的工作是正確的;

  在設計Web Service應用Bundle的時候需要考慮以下幾個事情:

  1、 每個WebService應用的實現應該做為一個獨立的Bundle運行;

  2、 按照WebService的推薦設計方式,一般都是先定義好WSDL文件,然後根據這個文件定義的Contract來實現服務端和客戶端;CXF提供了相關的工具將WSDl編譯成樁代碼.這個樁代碼包括Web Service介面的JAVA定義,以及由WSDL引用的XML Schema生成的關鍵數據對象.這些生成代碼應該做為一個獨立的DummyBundle來運行;

  3、 這個DummyBundle要做為CXFBundle 的Required Bundle;

  這裡的WSDL文件使用的是CXF下載包中帶的例子中的一個,讀者可以在CXF的Sample目錄中找到這個hello_world.wsdl文件;

  在Eclipse環境下建立一個新的Bundle,給名字為WsDummyBundle,這個Bundle不需要提供Active類!使用CXF提供的Wsdl2Java工具對這個WSDL進行編譯,將編譯生成的代碼全部做為這個Bundle的內部實現:

  然後在MANIFEST.MF文件中的Runtime面板中導出這兩個包:

  Export-Package: org.apache.hello_world_soap_http,

  org.apache.hello_world_soap_http.types

  至此,構建WsDummyBundle的工作就算完成;

  接下來創建WebService應用的實現Bundle:這個Bundle將使用Spring DM的能力來獲取CXFBundle發布的Web Service註冊介面服務(WSRegister)來發布,所以要定義一個HelloWorldBean類,用於完成Web Service的註冊:

  public class HelloWorldBean {

  private WSRegister register;

  public void setRegister(WSRegister register){

  this.register = register;

  }

  public WSRegister getRegister(){

  return this.register;

  }

  public void start(){

  GreeterImpl impl = new GreeterImpl();

  register.Regiser(Greeter.class, impl, "/Greeter");

  }

  }

  注意SpringDM將把WSRegister(OSGI服務)實例注射給register變數,在start方法中,實現最終的註冊過程.

  這裡注意一下調用Regiser函數時的Address參數,這裡只需要給出一個相對路徑就可以了,最終WebService將被發布在http://localhost/services/Greeter這個URL上.

  這裡使用了WSRegister這個介面定義,所以在MANIFEST.MF文件中要導入CXFWrapper.interfaces;

  定義類MyGreeterImp來實現WSDL定義的介面:

  @javax.jws.WebService(name = "Greeter",

  serviceName = "SOAPService",

  portName = "SoapPort",

  targetNamespace = "http://apache.org/hello_world_soap_http",

  wsdlLocation = "http://localhost/internal/hello_world.wsdl",

  endpointInterface = "org.apache.hello_world_soap_http.Greeter")

  public class MyGreeterImpl extends Greeter{

  ...

  }

  這裡使用了Greeter介面和其他WSDL樁代碼定義的類,所以在MANIFEST.MF文件中要導入

  org.apache.hello_world_soap_http,

  org.apache.hello_world_soap_http.types

  這兩個包;

  這裡還需要注意,如果wsdlLocation給出,處理要稍微複雜些:hello_world.wsdl文件能夠通過這個wsdlLocation指定的URL:http://localhost/internal/hello_world.wsdl訪問到.這個URL可以根據具體情況來配置!

  為了使WSDL能夠被正確的訪問到,需要再次使用OSGI的Http服務,這次暴露出來的不是Servlet而是WSDL文件:

  private class WSDLServiceTracker extends ServiceTracker{

  public Object addingService(ServiceReference reference) {

  HttpService httpService = (HttpService) context.getService(reference);

  try {

  httpService.registerResources("/internal/hello_world.wsdl", "/hello_world.wsdl", null);

  } catch (Exception e) {

  ...

  然後要在HelloWorldBean中啟動這個ServiceTracker:

  public void start(){

  httpServiceTracker = new WSDLServiceTracker(context);

  httpServiceTracker.open();

  GreeterImpl impl = new GreeterImpl();

  register.Regiser(Greeter.class, impl, "/Greeter");

  }

  如果wsdlLocation沒有給出,CXF將通過反射機制來獲取JAXWS2.1運行時所的信息.

  要定義SpringDM要求的配置文件,同樣有兩個,都要放置在META-INF/spring目錄下,一個是標準的Spring Bean定義文件:

  

  init-method="start" >
  另一個是SpringDM的OSGI服務配置文件:
 注意這裡的osgi:reference的id和上面property的ref屬性值應該相同.
  至此這個Bundle的實現完成.

  要對CXFBundle做一些收尾的工作,Bundle和Bundle之間使用了不同的類載入器,所以使用導入導出的方式讓不同的Bundle實現類的共享.CXF的運行時需要能夠看到任何被註冊服務所的樁類信息,所以CXFBundle也要導入WsDummyBundle導出的所有包.但是從工程的角度來說WsDummyBundle導出什麼庫不是事先確定的,需要根據具體的WebSevice來確定,因此每次發布新的WebService的時候,都需要修改CXFBundle得到MANIFEST.MF文件,這是非常麻煩的一件事情.

  如果將WsDummyBundle做為CXFBundle的Required Bundle,那麼WsDummyBundle將被和CXFBundle相同的類載入器載入,WsDummyBundle包含的所有類對CXF都預設可見,這樣的話發布新的服務就非常的簡單:

  1、 將樁代碼封裝到WsDummyBundle中,導出必要的包;

  2、 實現你的WebService Bundle;

  3、 運行就可以了,不需要對CXFBundle做任何改動!

  基於上述的討論,因此要在CXFBundle的MANIFEST.MF的Dependencies頁面加入

  Require-Bundle: WSDummyBundle

  運行整個系統,使用ss命令查看Bundle的狀態:

  ss

  Framework is launched.

  id State Bundle

  0 ACTIVE org.eclipse.osgi_3.4.0.v20080605-1900

  10 ACTIVE org.apache.commons.logging_1.0.4.v20080605-1930

  19 ACTIVE org.eclipse.osgi.services_3.1.200.v20071203

  29 ACTIVE org.springframework.bundle.spring.core_2.5.1

  30 ACTIVE org.springframework.bundle.osgi.io_1.0.3

  31 ACTIVE org.springframework.bundle.osgi.extender_1.0.3

  32 ACTIVE org.eclipse.equinox.http.servlet_1.0.100.v20080427-0830

  33 ACTIVE org.eclipse.equinox.http.jetty_1.1.0.v20080425

  34 ACTIVE org.springframework.bundle.osgi.core_1.0.3

  35 ACTIVE org.springframework.bundle.spring.context_2.5.1

  36 ACTIVE org.springframework.bundle.spring.beans_2.5.1

  37 ACTIVE javax.servlet_2.4.0.v200806031604

  42 ACTIVE org.mortbay.jetty_5.1.14.v200806031611

  43 ACTIVE org.springframework.osgi.aopalliance.osgi_1.0.0.SNAPSHOT

  46 ACTIVE WSDummyBundle_1.0.0

  47 ACTIVE org.springframework.bundle.spring.aop_2.5.1

  49 ACTIVE CXFWrapper_1.0.0

  50 ACTIVE HelloWorldBundle_1.0.0

  通過IE瀏覽器訪問地址http://localhost/services,將展示如下信息,這個信息是CXFNonSpringServlet提供的:

  點擊WSDL連接,就可以看到完整的關於這個WebService的WSDL信息;

  討論:

  1、 關於CXF的Bus:

  CXF中的Bus是CXF中的核心部件,並不是我們傳統上提到的通訊層面的匯流排,而更像一個公共服務和組件的存放倉庫.這有點類似Corba中的命名服務或是JEE環境下的JDNI註冊樹.它使用JAVA類的meta類做為查找索引,預設情況下,註冊了如下幾個核心類:

  a) CXFBusLifeCycleManager類,實現observer模式,用於發送Bus的起停事件;

  b) DestinationFactoryManager類,傳輸層的發送埠抽象類,用於創建SOAP消息發送通道;

  c) ResourceManager類,資源讀取工具,主要讀取配置文件,WSDL文件等;

  d) ConduitInitiatorManager類,傳輸層的接收埠抽象類,用於創建SOAP消息的接收通道;

  e) BindingFactoryManager類,SOAP綁定工廠類,實現JAXWS2.1規範要求;

  2、關於OSGI環境下類的可見性問題

  OSGI中的每個Bundle都是使用獨立的類裝載器;類裝載器本身就為其裝載的類創建了一個無形的名字空間,因此即使是相同的類,被不同的裝載器裝載后,彼此也變成的了不同的類.所以在開發OSGI應用的時候經常要主要此類的問題的發生,特別是當出現ClassCastException和classDefinitionNotFound的時候要特別警惕是否是類裝載器引入的問題.這裡舉一個稍微複雜的例子:

  在前面清理CXF的第三方庫中,我曾經單獨提到Javax.ws的庫在JDK6的環境下要從CXF的lib目錄下移除.現在看看如果不移掉,會有什麼問題:

  JDK6中已經定義了Javax.ws,在JVM啟動后,它就被預設的類裝載器裝載.那麼WebService Bundle預設情況下也將使用JVM的Javax.ws庫.而CXF Bundle則不同,它自己的類裝載器裝載了另外一套Javax.ws庫(Bundle中的類搜索優先順序別是先Bundle自己的classpath,然後才是外部的).所以當WebService Bundle通過CXF Bundle暴露的OSGI服務註冊WebService時,介面類上的annotation @WebService(注意這個annotation使用的是JVM的Javax.ws)對於CXF Bundle就是不可見的,於是從CXF Bundle的角度來看,客戶端提供了一個純的,沒有任何annotation修飾的類,同時這個類也沒有提供任何WSDL文件的信息,最終導致註冊失敗.




[火星人 via ] 運用Spring DM和CXF來實現WebService的動態發布已經有131次圍觀

http://www.coctec.com/docs/java/show-post-61867.html