基于Camel实现SOAP协议到自定义协议的转换

背景

毕业课题是做某种通信框架和ESB总线的集成,其中ESB的选型是ServiceMix,它的路由机制是借助Camel实现的。Camel提供了RouteBuilder抽象类,只要在其configure方法中,以from、to等方法描述路由,然后通过CamelContext的addRoute方法,就能将路由插入到Camel中。

现在想实现,对外发布一个WebService,用户调用该服务后,总线内部将SOAP消息,转换为自定义协议的报文,然后发送出去。

简单实现

Camel路由中,有一个process方法,可传入一个处理器,对消息进行处理,由于是做SOAP消息的转换,所以在Camel-cxf的基础上试验,路由代码类似:

CxfEndpoint from = new CxfEndpoint();
from.setDataFormat(DataFormat.RAW);
from.setCamelContext(getContext());
from.setAddress(address);
from.setServiceName(new QName(ns,service));
from.setPortName(new QName(ns,port));
from.setWsdlURL(wsdlUrl);

from(from).convertBodyTo(String.class).process(new MyProtocolProcessor());

MyProtocolProcessor负责SOAP消息的处理,这里以将消息打印到标准输出为例:

public class MyProtocolProcessor implements Processor {
    @Override
    public void process(Exchange exchange) throws Exception {
        System.out.println(exchange.getIn().getBody().toString());
    }
}

也可以在构造Processor时初始化自定义协议的客户端,然后在process方法调用客户端完成消息发送。代码运行后控制台输出如下:


   
   
      // 省略
   

 

缺点

在之前的实现里,是通过Processor来做SOAP消息的解析以及后续消息发送的,但是这样显然不够优雅。一来需要编码人员注意到Processor的存在,不利于内部逻辑的隐藏;二来没办法做自定义协议客户端关闭时的资源清理。

Camel中,to方法可以传入字符串形式的URI,或者一个Endpoint,来实现消息的流出。例如 to("http://localhost:8080/test") 就是将消息发送到 http://localhost:8080/test 这个地址,现在也想做成这种形式,但是对Camel了解有限,需要先摸索自定义协议的实现机制。

 

探索过程

因为对Camel不了解,所以采用了面向异常编程的办法,也就是先假设系统功能全部实现,则给定预期输入,通过Debug和编码,不断消除异常和错误输出,直到最终系统结果符合预期。

于是将路由配置成:

from(from).convertBodyTo(String.class).to("myprotocol://variable1/variable2");

其中,myprotocol://variable1/variable2 就是自定义协议的URI格式,myprotocol代表协议名,URI中还有两个变量 variable1 和 variable2 。

运行后,报出如下异常堆栈:

Exception in thread "main" org.apache.camel.FailedToCreateRouteException: Failed to create route route1 at: >>> To[myprotocol://variable1/variable2] <<< in route: Route(route1)[[From[http://localhost:8081/test]] -> [Convert... because of Failed to resolve endpoint: myprotocol://variable1/variable2 due to: No component found with scheme: myprotocol
	at org.apache.camel.model.RouteDefinition.addRoutes(RouteDefinition.java:1352)
	at org.apache.camel.model.RouteDefinition.addRoutes(RouteDefinition.java:212)
	at org.apache.camel.impl.DefaultCamelContext.startRoute(DefaultCamelContext.java:1140)
	at org.apache.camel.impl.DefaultCamelContext.startRouteDefinitions(DefaultCamelContext.java:3735)
	at org.apache.camel.impl.DefaultCamelContext.doStartCamel(DefaultCamelContext.java:3440)
	at org.apache.camel.impl.DefaultCamelContext$4.call(DefaultCamelContext.java:3248)
	at org.apache.camel.impl.DefaultCamelContext$4.call(DefaultCamelContext.java:3244)
	at org.apache.camel.impl.DefaultCamelContext.doWithDefinedClassLoader(DefaultCamelContext.java:3267)
	at org.apache.camel.impl.DefaultCamelContext.doStart(DefaultCamelContext.java:3244)
	at org.apache.camel.support.ServiceSupport.start(ServiceSupport.java:72)
	at org.apache.camel.impl.DefaultCamelContext.start(DefaultCamelContext.java:3160)
	at hnu.yhc.bus.Main.main(Main.java:15)
Caused by: org.apache.camel.ResolveEndpointFailedException: Failed to resolve endpoint: myprotocol://variable1/variable2 due to: No component found with scheme: myprotocol
	at org.apache.camel.impl.DefaultCamelContext.getEndpoint(DefaultCamelContext.java:759)
	at org.apache.camel.util.CamelContextHelper.getMandatoryEndpoint(CamelContextHelper.java:80)
	at org.apache.camel.model.RouteDefinition.resolveEndpoint(RouteDefinition.java:227)
	at org.apache.camel.impl.DefaultRouteContext.resolveEndpoint(DefaultRouteContext.java:116)
	at org.apache.camel.impl.DefaultRouteContext.resolveEndpoint(DefaultRouteContext.java:122)
	at org.apache.camel.model.SendDefinition.resolveEndpoint(SendDefinition.java:62)
	at org.apache.camel.model.SendDefinition.createProcessor(SendDefinition.java:56)
	at org.apache.camel.model.ProcessorDefinition.makeProcessorImpl(ProcessorDefinition.java:569)
	at org.apache.camel.model.ProcessorDefinition.makeProcessor(ProcessorDefinition.java:530)
	at org.apache.camel.model.ProcessorDefinition.addRoutes(ProcessorDefinition.java:240)
	at org.apache.camel.model.RouteDefinition.addRoutes(RouteDefinition.java:1349)
	... 11 more

可以看到,无法创建路由的根本原因是无法找到myprotocol协议对应的Component,于是到DefaultCamelContext的异常位置查看源码,可以看到原因大概是answer为null,再向上,可以看到answer变量的引用来源于createEndpoint方法或Component的createEndpoint方法:

answer = createEndpoint(uri);
...
if (component != null) {
    answer = component.createEndpoint(uri);
}
...
if (answer == null && scheme != null) {
    throw new ResolveEndpointFailedException(uri, "No component found with scheme: " + scheme);
}

于是从Component着手,根据URI获取Component的方法为org.apache.camel.impl.DefaultCamelContext#getComponent(java.lang.String, boolean, boolean),在其initComponent方法中,我们看到如下语句:

component = getComponentResolver().resolveComponent(name, this);

使用Ctrl + Alt + B 快捷键,不难发现ComponentResolver的实现类是DefaultComponentResolver,阅读源码后了解到,Camel是通过SPI机制加载Component接口实现类的。于是我们在Java工程的resources目录下,创建 META-INF/services/org/apache/camel/component/myprotocol文件,内容为:

class=test.MyProtocolComponent

test.MyProtocolComponent为MyProtocolComponent类的全限定名。然后在test包下,创建MyProtocolComponent类,由于我们需要基于SOAP做转换,为了减少编码量,这里直接继承了CxfComponent类。然后重写其createEndpoint方法:

public class MyProtocolComponent extends CxfComponent {
    @Override
    protected Endpoint createEndpoint(String uri, String remaining, Map parameters) throws Exception {
        return new MyProtocolEndpoint(remaining, this);
    }
}

remaining就是 variable1/variable2 。然后继续实现MyProtocolEndpoint:

public class MyProtocolEndpoint extends CxfEndpoint {
    public MyProtocolEndpoint(String remaining, CxfComponent cxfComponent) {
        super(remaining, cxfComponent);
    }
}

再次运行后,报错如下:

Exception in thread "main" java.lang.IllegalArgumentException: serviceClass must be specified

堆栈显示,该错误来源于CxfProducer的doStart()方法,于是我们继承CxfProducer实现MyProtocolProducer,重写doStart方法。并重写MyProtocolEndpoint的createProducer方法,使之返回MyProtocolProducer类型的对象。

由于CxfProducer在doStart方法中做了Client的初始化,所以我们也要保持Producer的这个作用。重写后的doStart方法如下:

@Override
protected void doStart() throws Exception {
    // 在此初始化自定义协议的客户端
}

可以顺便实现doStop方法,做客户端的关闭和资源清理。当我们阅读源码时,可以看到CxfProducer有两个process方法,且在这里调用Client完成了数据发送,所以我们也重写这两个方法,实现自定义协议的数据发送,具体代码和上面MyProtocolProcessor的process方法一致。运行后的输出也是一致的。

你可能感兴趣的:(ServiceMix)