這是一個被直接集成在camel-core中的組件,其地位由此可見一斑。而且實現組成也是相當簡單,加起來也才三個類
ControlBusComponent
,ControlBusEndpoint
,ControlBusProducer
。
1. 概述
Apache Camel在對EIP實現中,最終選擇在其2.11版本完成了對於"Control Bus"模式的支持——內置了對於所集成系統的監控和管理。通俗點說就是你可以針對每一個Route進行諸如start,stop,suspend,resume,status,stats等等操作,這極大增加了程序運行時的靈活性。本文接下來的部分將對Apache Camel實現進行分析,做到知其所以然。
在Apache Camel中,你已經可以通過JMX,CamelContext提供的Java API,亦或是Event Notifier來實現管理和監控功能,但是ControlBus提供的是一種更加友好的開箱即食機制。
2. 源碼解讀
本次的用例如下:
// 注意這裏的測試用例,筆者拋棄了官方文檔的樣例,旨在希望能提供了一種更具普適性的控制方式.
// 下面的方式結合camel-servlet可以無需額外的java代碼實現諸如start,stop,suspend,resume,status,stats等等操作.
CamelTestUtil.defaultPrepareTest2(new RouteBuilder() {
@Override
public void configure() throws Exception {
from("timer://myTimer?period=5000")//
.id("timeRoute")//
.setBody(simple("Current time is [ ${header.firedTime} ]"))//
.to("stream:err");
// 以下是我們本次的研究重點
from("stream:in?promptMessage=Enter something:")//
.setBody(simple("camelContext.getRouteStatus('${body}')"))
.to("controlbus:language:simple?async=true");
}
});
以上測試用例啓動後:
- 啓動一個定時任務,每五秒中會以紅色字體向控制檯打印當前時間。
- 在控制檯接收用戶輸入內容,作爲routeId對相應的Route進行停止操作。
接下來我們依然參照之前的Apache Camel系列文章的慣用格式,分兩部分進行討論。
2.1 啓動時
以上代碼啓動時候,我們重點關注.to("controlbus:language:simple?async=true")
。我們將得到以下堆棧圖:
-
關於
ControlBusComponent
。// ControlBusComponent.createEndpoint() protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters) throws Exception { ControlBusEndpoint answer = new ControlBusEndpoint(uri, this); // does the control bus use a language // 本測試用例中將走以下分支 if (remaining != null && remaining.startsWith("language:")) { String lan = remaining.substring(9); answer.setLanguage(getCamelContext().resolveLanguage(lan)); } // 得益於Camel優秀的類層次設計, 我們可以直接複用基類邏輯來實現賦值, 避免重複枯燥且易錯的操作. setProperties(answer, parameters); return answer; }
-
關於
ControlBusEndpoint
。正如之前的文章,Endpoint作爲Producer和Consumer的工廠,從其實現中我們就可以看出當前組件的支持情況。這裏我們可以非常清晰地看出 ControlBus模式是隻支持Producer的,也就是無法作爲from()節點。@Override public Producer createProducer() throws Exception { CamelLogger logger = new CamelLogger(ControlBusProducer.class.getName(), loggingLevel); return new ControlBusProducer(this, logger); } @Override public Consumer createConsumer(Processor processor) throws Exception { // 不支持Consumer throw new RuntimeCamelException("Cannot consume from a ControlBusEndpoint: " + getEndpointUri()); }
至此,關於ControlBus的啓動就算是完成了,剩下的就是等待用戶的主動調用。
2.2 運行時
在上面的測試用例中,用戶輸入"timeRoute"(前面特意定義的routeId名稱),邏輯最終會跳轉到ControlBusProducer.process(Exchange exchange, AsyncCallback callback)
中。
// ======================== ControlBusProducer.java
@Override
public boolean process(Exchange exchange, AsyncCallback callback) {
// 本例中跳轉到如下分支
if (getEndpoint().getLanguage() != null) {
try {
processByLanguage(exchange, getEndpoint().getLanguage());
} catch (Exception e) {
exchange.setException(e);
}
} else if (getEndpoint().getAction() != null) {
// action模式的調用更爲簡單, 但如果想要獲得靈活性的支持, 就需要參考官方示例:
// template.sendBody("controlbus:route?routeId=foo&action=start", null);
try {
processByAction(exchange);
} catch (Exception e) {
exchange.setException(e);
}
}
callback.done(true);
return true;
}
protected void processByLanguage(Exchange exchange, Language language) throws Exception {
// 使用專門的內部類進行邏輯處理
LanguageTask task = new LanguageTask(exchange, language);
if (getEndpoint().isAsync()) {
getEndpoint().getComponent().getExecutorService().submit(task);
} else {
task.run();
}
}
// ======================== ControlBusProducer.LanguageTask.java
/**
* Tasks to run when processing by language.
*/
private final class LanguageTask implements Runnable {
private final Exchange exchange;
private final Language language;
private LanguageTask(Exchange exchange, Language language) {
this.exchange = exchange;
this.language = language;
}
@Override
public void run() {
String task = null;
Object result = null;
try {
// create dummy exchange
Exchange dummy = ExchangeHelper.createCopy(exchange, true);
// 獲取上一步傳入的內容, 作爲將要被執行的task
task = dummy.getIn().getMandatoryBody(String.class);
if (task != null) {
// language爲進行controlbus配置時候設置的, 本例中是simple
// 其它支持的語言類型參見 ControlBusEndpoint類中字段language聲明。
Expression exp = language.createExpression(task);
result = exp.evaluate(dummy, Object.class);
}
if (result != null && !getEndpoint().isAsync()) {
// can only set result on exchange if sync
exchange.getIn().setBody(result);
}
if (task != null) {
logger.log("ControlBus task done [" + task + "] with result -> " + (result != null ? result : "void"));
}
} catch (Exception e) {
logger.log("Error executing ControlBus task [" + task + "]. This exception will be ignored.", e);
}
}
}
因此,在ControlBus實現中:
- Language實現方式其實就是藉助了Language擁有的強大指令執行能力來實現對於系統的監控管理。因此更具靈活性。
- Action實現方式則是通過約定的"start","stop"等關鍵字來實現對於系統的監控管理。
3. 總結
依託於Apache Camel的既有優秀設計,ControlBus模式的實現還是相當簡單的,等同於對既有功能的一些簡單回調,而無需額外的實現。這也正是我們產品開發中一直在追求的。
4. 福利環節
上面章節提到我們可以結合camel內置的servelt組件實現對於系統的遠程控制,這裏筆者給出一個經過驗證的可行版本。
// ===== 爲了確保讀者快速上手看到效果, 這裏筆者用jetty組件代替通常的servlet
restConfiguration().host("127.0.0.1")//127.0.0.1:8080/foo
.port("8080")//
.contextPath("foo")//
.setComponent("jetty");
// ===== 核心控制邏輯
from("rest:get:cb/{action}/{routeId}") // 以rest的形式約定調用格式
.log("begin [ ${header.action} ] the route with id [ ${header.routeId}") //
// 其它諸如 suspend,resume,status,stats 可依據下面格式進行選擇性添加
.choice() //
.when(simple("${header.action} == 'start'")).setBody(simple("camelContext.startRoute('${header.routeId}')"))//
.when(simple("${header.action} == 'stop'")).setBody(simple("camelContext.stopRoute('${header.routeId}')"))//
.otherwise().log("unkown action ${header.action}").setBody(simple("unkown action [ ${header.action} ] FOR [ ${header.routeId} ] ")).stop() // 傳入未知action, 則返回提示性信息
.end() //
.to("controlbus:language:simple");
// ===== 請求示例:
// 1. 停止指定Route
// http://127.0.0.1:8080/foo/cb/stop/timeRoute
// 2. 啓動指定Route
// http://127.0.0.1:8080/foo/cb/start/timeRoute
5. Links
- Apache Camel監控之使用hawtio
- Office Site - Control Bus
- Apache Camel使用之集成SpingBoot Actuator2.0
- 《Apache Camel Developer’s Cookbook》P44