基於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方法調用客戶端完成消息發送。代碼運行後控制檯輸出如下:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:hnu="省略">
   <soapenv:Header/>
   <soapenv:Body>
      // 省略
   </soapenv:Body>
</soapenv:Envelope>

 

缺點

在之前的實現裏,是通過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<String, Object> 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方法一致。運行後的輸出也是一致的。

發佈了84 篇原創文章 · 獲贊 12 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章