毕业课题是做某种通信框架和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方法一致。运行后的输出也是一致的。