这是一个被直接集成在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