接上一篇内容
五、使用服务--mytoaster-consumer
与官方wiki的两点区别:
1、Odl的wiki中采用直接定义java api的方式实现的comsumer,这里采用yang模型定义。因此在实际开发中用的比较多还是yang来定义模型。
2、Wiki中还是采用jmx当作客户端,为了简便(主要引入jmx太复杂,而且本人也不是很了解jmx,哈哈大实话)不使用jmx,采用postman、restconf当作客户端。如果有能力自己可以使用node.js搭建一个最简单的web server,直接调用接口以及实现监听。
目录结构如下:
[root@localhost mytoaster-consumer]# tree
├── api
│ ├── pom.xml
│ ├── src
│ └── main
│ └── yang
│ └── mytoaster-consumer.yang
├── impl
│ ├── pom.xml
│ └── src
│ └──main
│ ├──java
│ │ └── org.opendaylight.controller.demo2.mytoaster.myconsumer.impl
│ │ └──KitchenServiceImpl.java
│ └──resources
└── pom.xml
[root@localhost mytoaster-consumer]#
mytaoster-consumer.yang文件内容如下(参考odl wiki将java代码写成yang模型):
module mytoaster-consumer {
yang-version 1;
namespace
"http://mytoaster.org/toaster/consumer";
prefix consumer;
organization "Netconf Central";
contact
"Andy Bierman <andy@netconfcentral.org>";
description
"YANG version of the TOASTER-MIB.";
import mytoaster {
prefix mytoaster;
}
revision "2017-10-14" {
description
"Toaster module in progress.";
}
typedef EggType {
type enumeration {
enum SCRAMBLED;
enum OVER_EASY;
enum POACHED;
}
}
rpc makeBreakfast {
input {
leaf eggType {
type EggType;
}
leaf toastType {
type identityref {
base mytoaster:mytoast-type;
}
}
leaf toastDoneness {
type uint32;
}
}
output {
leaf info {
type string;
}
}
}
}
mytoaster-consumer目录下面pom.xml文件,主要内容(可以参考之前pom进行修改):
<groupId>org.opendaylight.controller.demo2</groupId>
<artifactId>mytoaster-consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>api</module>
<module>impl</module>
</modules>
api目录下面pom.xml文件,主要内容(可以参考之前pom进行修改):
<groupId>org.opendaylight.controller.demo2</groupId>
<version>0.0.1-SNAPSHOT</version>
<artifactId>mytoaster</artifactId>
<packaging>bundle</packaging>
Impl目录下面pom.xml文件,主要内容(可以参考之前pom进行修改):
<groupId>org.opendaylight.controller.demo2</groupId>
<artifactId>mytoaster-consumer-impl</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>bundle</packaging>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mytoaster</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>mytoaster-consumer-api</artifactId>
<version>${project.version}</version>
</dependency>
public class KitchenServiceImpl implements MytoasterListener, MytoasterConsumerService {
private final MytoasterService mytoaster;
private final ListeningExecutorService executor = MoreExecutors.listeningDecorator(
Executors.newCachedThreadPool());
public KitchenServiceImpl(MytoasterService mytoaster) {
this.mytoaster = mytoaster;
}
private Future<RpcResult<MakeToastOutput>> makeToast(Long doness, java.lang.Class<? extends org.opendaylight.yang.gen.v1.http.mytoaster.org.toaster.rev171014.MytoastType> type) {
MakeToastInputBuilder builder = new MakeToastInputBuilder();
builder.setToasterDoneness(doness);
builder.setToasterToastType(type);
return mytoaster.makeToast(builder.build());
}
@Override
public Future<RpcResult<MakeBreakfastOutput>> makeBreakfast(MakeBreakfastInput input) {
//制作面包
ListenableFuture<RpcResult<MakeToastOutput>> makeToastFuture = JdkFutureAdapters.listenInPoolThread(
makeToast(input.getToastDoneness(), input.getToastType()), executor);
//制作鸡蛋
ListenableFuture<RpcResult<MakeToastOutput>> makeEggsFuture = new EggOutput().makeEggs(input.getEggType());
/* 将两个future合并 然后统一监测结果 */
ListenableFuture<List<RpcResult<MakeToastOutput>>> combinedFutures = Futures
.allAsList(ImmutableList.of(makeToastFuture, makeEggsFuture));
return Futures.transform(combinedFutures, new AsyncFunction<List<RpcResult<MakeToastOutput>>, RpcResult<MakeBreakfastOutput>>() {
@Override
public ListenableFuture<RpcResult<MakeBreakfastOutput>> apply(List<RpcResult<MakeToastOutput>> rpcResults) throws Exception {
boolean atLeastOneSucceeded = true;
ImmutableList.Builder<RpcError> errorList = ImmutableList.builder();
for (RpcResult<MakeToastOutput> result :rpcResults) {
if (!result.isSuccessful()) {
atLeastOneSucceeded = false;
}
if (result.getErrors() != null) {
errorList.addAll(result.getErrors());
}
}
MakeBreakfastOutputBuilder builder = new MakeBreakfastOutputBuilder();
if (atLeastOneSucceeded) {
builder.setInfo("Make Breakfast Completed.");
} else {
builder.setInfo("Make Breakfast UnCompleted.");
}
return Futures.immediateFuture(RpcResultBuilder.success(builder.build()).build());
}
});
}
/* 包装一下 为了统一处理*/
private class EggOutput {
//鸡蛋采用异步通知方式
public ListenableFuture<RpcResult<MakeToastOutput>> makeEggs(EggType eggType) {
return KitchenServiceImpl.this.executor.submit(new Callable<RpcResult<MakeToastOutput>>() {
@Override
public RpcResult<MakeToastOutput> call() throws Exception {
MakeToastOutputBuilder builder = new MakeToastOutputBuilder();
builder.setMakeInfo("Wait a memont for Eggs");
return RpcResultBuilder.success(builder.build()).build();
}
});
}
}
}
实现了具体代码之后,我们还需要设置blueprint以及添加feature定义:
<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
xmlns:odl="http://opendaylight.org/xmlns/blueprint/v1.0.0"
odl:use-default-for-reference-types="true">
<!-- 由于需要制作面包 因此导入rpc服务 -->
<odl:rpc-service id="MyToasterService" interface="org.opendaylight.yang.gen.v1.http.mytoaster.org.toaster.rev171024.MytoasterService"/>
<!-- 定义一个KitchenServiceImpl类 -->
<bean id="kitchenService" class="org.opendaylight.controller.demo2.mytoaster.myconsumer.impl.KitchenServiceImpl">
<argument ref="MyToasterService"/>
</bean>
<service ref="kitchenService" interface="org.opendaylight.yang.gen.v1.http.mytoaster.org.toaster.consumer.rev171014.MytoasterConsumerService"
odl:type="default"/>
<odl:rpc-implementation ref="kitchenService" />
</blueprint>
在mdsal/features-mdsal目录中pom文件增加:
<dependency>
<groupId>org.opendaylight.controller.demo2</groupId>
<artifactId>mytoaster-consumer-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.opendaylight.controller.demo2</groupId>
<artifactId>mytoaster-consumer-impl</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
在mdsal/features-mdsal/src/main/features/features.xml增加对comsumer bundle的定义:
<bundle>mvn:org.opendaylight.controller.demo2/mytoaster-consumer-api/{{VERSION}}</bundle>
<bundle>mvn:org.opendaylight.controller.demo2/mytoaster-consumer-impl/{{VERSION}}</bundle>
然后进行编译即可,下图是测试toaster:
URL:http://localhost:8080/restconf/operations/mytoaster-consumer:makeBreakfast
数据:{
"input": {
"toastType": "mytoaster:white-bread",
"toastDoneness": "5",
"eggType": "SCRAMBLED"
}
}
六、实现Notification
在现实生活中烤面包是异步的,例如,人不需要一直守在面包机前面,当烤完面包之后,面包机会发出“嘀嘀..”声响。若实现这种方式,则需要用到notification了。
我们需要改造makeBreakFast方法:收到rpc请求后,将制作鸡蛋和制作面包这两个任务提交给线程池处理,然后立即响应rpc请求。等面包和鸡蛋制作完成后以notification方式上报给请求者(odl官方wiki中没有实现这种方式。),后面实现这种实现。
现实生活中烤面机不可能无休止的能制作出面包,它也是有原材料限制的,因此还是需要设置MyopendaylightToaster.java
/* wiki中采用NotificationProviderService 此类即将被废弃 */
private NotificationPublishService notificationProvider;/* 用于发送notification消息 */
private final AtomicLong amountOfBreadInStock = new AtomicLong(100);/* 面包数量 默认可以烤100个面包 */
public void setNotificationProvider(NotificationPublishService notificationProvider) {
this.notificationProvider = notificationProvider;
}
private void checkStatusAndMakeToast(final MakeToastInput input, final int tries,
final SettableFuture<RpcResult<MakeToastOutput>> futureResult) {
...
if (status == Mytoaster.ToasterStatus.Idle) {
if(outOfBread()) {//表示面包已经用完 无法再烤面包
return Futures.immediateFailedCheckedFuture(
new TransactionCommitFailedException("",
RpcResultBuilder.newError(RpcError.ErrorType.APPLICATION,
"Warning", "Out Bread")));
}
tx.put(LogicalDatastoreType.OPERATIONAL, TOASTER_IID,
createToaster(Mytoaster.ToasterStatus.Work));
return tx.submit();
}
...
}
private boolean outOfBread() {
return amountOfBreadInStock.get() == 0;
}
@Override
public Future<RpcResult<Void>> restockToaster(RestockToasterInput input) {
amountOfBreadInStock.set(input.getAmountOfBreadToStock());
if (amountOfBreadInStock.get() > 0) {//表示已经添加面包 可以继续烤
ToasterRestocked reStockedNotification = new ToasterRestockedBuilder()
.setAmountOfBread(input.getAmountOfBreadToStock()).build();
notificationProvider.offerNotification(reStockedNotification);
}
return Futures.immediateFuture(RpcResultBuilder.<Void>success().build());
}
private class MakeToastTask implements Callable<Void> {
....
@Override
public Void call() throws Exception {
try {
amountOfBreadInStock.getAndDecrement();//表示消耗一个面包
// 睡眠15秒 表示制作面包
Thread.sleep(1000*15);
//表示面包已经制作完成
notificationProvider.offerNotification(new ToasterOutOfBreadBuilder().build());
System.out.println("Break is out!!Please take it");
} catch (InterruptedException e) {
...
}
}
代码写完后需要在blueprint中增加notification服务,修改prodiver下面的blueprint文件:
<reference id="notification" interface="org.opendaylight.controller.md.sal.binding.api.NotificationPublishService"/>
<bean ...>
<property name="notificationProvider" ref="notification"/>
</bean>
以上功能只是完成notification消息分发,但是接收消息还没有处理,在cosumer中进行消息处理:
public class KitchenServiceImpl implements MytoasterListener, MytoasterConsumerService {
...
@Override
public void onToasterRestocked(ToasterRestocked notification) {
System.out.println("ToasterOutOfBread notification");
}
@Override
public void onToasterOutOfBread(ToasterOutOfBread notification) {
//终端显示:面包完成
System.out.println("ToasterRestocked notification - amountOfBread: " + notification.getMessage() );
}
}
consumer中blueprint需要进行notification注册,只有注册之后,才能接收到消息:
<odl:notification-listener ref="kitchenService" />
好了整个notification机制已经完成,测试一下:
通过postman下发制作早餐,rpc立即响应回来了,但是下面的打印,在15秒之后打印出来的。说明我们已经能异步接收消息了。
扩展内容:如果了解netconf协议的同学,都知道netconf中有一个种功能时订阅。也就是说订阅某种数据(对于netconf来说应该时tree\node),当数据发生变化的时候会发给订阅者。代码中consumer也相当于是一个订阅者。这里在介绍一种,通过websockect方式订阅。
通过websocket订阅必须有以下三步骤:
1、查询发布通知(Postman-Get):http://127.0.0.1:8181/restconf/18/data/ietf-restconf-monitoring:restconf-state/streams
返回内容
{
"streams": {
"stream": [
....
{
"name": "toasterOutOfBread",
"replay-support": true,
"access": [
{
"encoding": "XML",
"location": "ws://127.0.0.1:8185/create-notification-stream/mytoaster:toasterOutOfBread"
},
{
"encoding": "JSON",
"location": "ws://127.0.0.1:8185/create-notification-stream/mytoaster:toasterOutOfBread/JSON"
}
],
"description": "Indicates that the toaster has run of out bread."
},
{
"name": "toasterRestocked",
"replay-support": true,
"access": [
{
"encoding": "XML",
"location": "ws://127.0.0.1:8185/create-notification-stream/mytoaster:toasterRestocked"
},
{
"encoding": "JSON",
"location": "ws://127.0.0.1:8185/create-notification-stream/mytoaster:toasterRestocked/JSON"
}
],
"description": "Indicates that the toaster has run of out bread."
},
....
]
}
}
2、订阅某个通知(Postman-Get):http://localhost:8181/restconf/18/data/ietf-restconf-monitoring:restconf-state/streams/stream=toasterOutOfBread/access=JSON/location
注意:1)蓝色替换成第一步中查询回来的name名字,也就是notifiaction名字2)订阅支持xml、json,默认是xml。
返回内容
{
"location": "ws://127.0.0.1:8185/create-notification-stream/mytoaster:toasterOutOfBread/JSON"
}
3、通过rest-client进行websocket连接(Postman支持websocket):
url地址是第二步返回内容, websocket建立成功,再次通过postman下发rpc则在rest-cleint中显示notification信息:
七、系统启动时自动安装feature
正demo在实现过程中,总是在karaf启动之后,手动安装feature,这样很是麻烦。如果在karaf启动之后feature自动安装呢?目前有两种方式:
方法1:直接修改opendaylight-karaf/target/assembly/etc/org.apache.karaf.features.cfg文件,在featuresBoot配置项中增加自定义的feature。但是这种方式存在两个问题:
1)每次编译版本之后文件又被修改成原来的。
2)在我们发布产品A的时候,可能需要Feature-F,但是发布产品B的时候又不需要Feature-F,那么如何解决这种问题呢?因此有方法2。
方法2:在同事和网友的指点下,在插件karaf-maven-plugin中有一个配置项,可以支持在启动的时候去修改。修改karaf/opendaylight-karaf/pom.xml,增加如下内容:
<profiles>
<profile>
<id>build-mytoaster</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.karaf.tooling</groupId>
<artifactId>karaf-maven-plugin</artifactId>
<configuration>
<bootFeatures>
<bootFeature>odl-restconf-all</bootFeature>
<bootFeature>odl-mytoaster</bootFeature>
</bootFeatures>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
上面这种方式,增加了一个profile,然后指定bootFeature就可以完成自动安装,同时又可支持不同OEM,可谓一举两得。在编译时增加一个编译参数-Pbuild-mytoaster,具体如下:mvn clean install -DskipTests -Dskip.karaf.featureTest=true -Dmaven.test.skip=true -Dcheckstyle.skip=true -Dmaven.javadoc.skip=true-Pbuild-mytoaster 。
启动完成之后可以查看,feature是否已经安装成功。
总结:
这次文章写的内容比较多,前前后后写了有两周。自从搞odl开始,toaster这个demo前前后后写了有三四遍,每写一次都会加深对odl的理解,这次总结也算是对odl总结,希望通过这篇文章,能狗帮助更多人,同时感谢我的同时以及帮助我的网友。