技术开发 频道

运用Spring DM和CXF来实现WebService 的动态发布

  【IT168技术文档】

  在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的lib\endorse目录下,做为公共库,替代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文件的信息,最终导致注册失败。

0
相关文章