Apache Camel組件之Control Bus

這是一個被直接集成在camel-core中的組件,其地位由此可見一斑。而且實現組成也是相當簡單,加起來也才三個類ControlBusComponentControlBusEndpointControlBusProducer

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");
	}
});

以上測試用例啓動後:

  1. 啓動一個定時任務,每五秒中會以紅色字體向控制檯打印當前時間。
  2. 在控制檯接收用戶輸入內容,作爲routeId對相應的Route進行停止操作。

接下來我們依然參照之前的Apache Camel系列文章的慣用格式,分兩部分進行討論。

2.1 啓動時

以上代碼啓動時候,我們重點關注.to("controlbus:language:simple?async=true")。我們將得到以下堆棧圖:
ControlBusComponent

  1. 關於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;
    }
    
  2. 關於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實現中:

  1. Language實現方式其實就是藉助了Language擁有的強大指令執行能力來實現對於系統的監控管理。因此更具靈活性。
  2. 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

  1. Apache Camel監控之使用hawtio
  2. Office Site - Control Bus
  3. Apache Camel使用之集成SpingBoot Actuator2.0
  4. 《Apache Camel Developer’s Cookbook》P44
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章