上一篇:Nifi组件脚本开发—ExecuteScript 使用指南(二)
Part 3 - 高级特征
本系列的前两篇文章涵盖了 flow file 的基本操作, 如读写属性和内容, 以及使用"session" 变量 ( ProcessSession对象)获取和转移 flow files . ExecuteScript还有很多其他的能力,这里对一部分作简要介绍。
动态属性-Dynamic Properties
其中一个能力叫做 dynamic properties, 或者称为用户定义属性. processor 的一些属性可以由用户设置 property name 和 value. 不是所有的processors 都支持和使用动态属性, 在 ExecuteScript 将传递动态属性作为变量,改变了引用 PropertyValue 对象,对应于property's value. 这里有两个重要事需要了解:
- 因为 property 绑定为变量名, dynamic properties的命名规则必须满足相应的编程语言的规范。 例如, Groovy 不支持 (.) 作为变量名字符, 像 "my.value" 引起processor处理失败. 有效的可选项是 "myValue"
- PropertyValue 对象用于 (rather than a String representation of the value) 脚本执行多种操作,在转换为String之前进行。如果property已知包含合法的值, 你可以调用 该变量的 getValue() 方法得到其字符串表示. 如果值包含 Expression Language,或者希望转为除字符串外的其它值(如 'true' 对于Boolean 对象), 这里也提供了操作方法. 这些例子在下面的示例中演示, 假定我们有两个属性 'myProperty1' 和 'myProperty2',像下面这样被定义
获取 dynamic property的值
需求:在脚本中得到 dynamic property(如, 配置参数)。
方法:使用变量的PropertyValue对象的getValue() 方法. 该方法返回其字符串代表 dynamic property. 注意,如果Expression Language包含在字符串中, getValue() 将不会对其求值(参加下一个方法实现求职功能)。
例子:
Groovy
def myValue1 = myProperty1.value
Jython
myValue1 = myProperty1.getValue()
Javascript
var myValue1 = myProperty1.getValue()
JRuby
myValue1 = myProperty1.getValue()
添加模块
ExecuteScript 的另一个特征是具有添加外部模块到 classpath 的能力, 这将允许使用大量的第三方库、脚本等增强能力. 但是,每一个脚本引擎处理模块的方法都是不一样的, 因此需要分开讨论。总体上说, 主要有两种类型的模块, Java libraries (JARs) 和 scripts (以在 ExecuteScript中的同一种语言编写. 这里将讨论和显示不同 script engines 如何进行处理:
Groovy
Groovy script engine (至少在 ExecuteScript中) 不支持导入其他的 Groovy scripts, 但是允许 JARs 添加到 classpath. 因此,对于外部Groovy projects, 考虑编译为bytecode,然后指向 classes 目录或者包装为 JAR.
当使用 Groovy, 这个 Module Directory 属性设为 comma-separated 的文件列表 (JARs) 和 folders. 如果folder 被指定, ExecuteScript 将发现该目录所有的 JARs 并添加进去. 这允许你添加第三方软件,哪怕包含很多个 JARs.
Jython
Jython script engine (在 ExecuteScript) 目前仅支持导入纯 Python 模块, 不包含natively-compiled modules (如CPython),如 numpy 或 scipy. 目前也暂不支持 JARs, 这在将来版本中也许会考虑. 在Module Directory property在执行前需要加载, 使用"import sys" 跟着 "sys.path.append" 对每一个指定的模块位置进行加载.
如果 Python 已经安装, 可以将所有的安装好的纯 Python modules 添加进来,通过将 site-packages 目录加到Module Directory 属性即可, 如:
/usr/local/lib/python2.7/site-packages
然后,你的脚本就能 import 各种软件包了,如:
from user_agents import parse
Javascript
Javascript script engine (在 ExecuteScript), 允许同样的 JARs/folders设置,与 Groovy engine一样. 将查找JARs 以及指定的folder.
JRuby
JRuby script engine (在 ExecuteScript) 目前只允许单个的 JARs指定, 如果 folder 被指定,其中一定要有classes ( java compiler 需要能看见), 如果folder 包含 JARs将不会自动加入。目前, 没有pure Ruby 模块能被导入
状态管理
NiFi (如0.5.0 ) 提供了为 Processors 和其他 NiFi 组件持久化一些信息从而实现组件的状态管理功能. 例如, QueryDatabaseTable processor 保存对大数据集的跟踪, 当下次运行时, 将只会获取哪些比原来(存储在 State Manager)更大的行的数据。
状态管理的一个重要概念是Scope. NiFi 组件可以选择存储它的状态在集群级别还是本地级别. 注意,在独立的 NiFi 实例中, "cluster scope" 与 "local scope"是一样的. 这个 scope 选择的区别在于在一个数据流中,每个结点的处理器是否需要共享状态信息. 如果集群中的实例不需要共享状态,就使用local scope. 在 Java,这些选项作为一个 enum变量 Scope提供, 因此,当引用 Scope.CLUSTER 和 Scope.LOCAL, 就意味着是集群模式或本地模式.
为了探究ExecuteScript (语言独立的例子如下)状态管理的特征 , 你可以获得 StateManager的引用,通过调用 ProcessContext的 getStateManager() 方法实现 (recall that each engine gets a variable named "context" with an instance of ProcessContext). 然后调用 StateManager 对象的下面方法:
- void setState(Map<String, String> state, Scope scope) - 在给定的scope更新组件状态的值, 设置为给定的值. 注意,这个值是 Map 数据结构; 概念 "component state" 所有的 key/value键值对的 Map. 该 Map被一次全部更新,从而提供原子性.
- StateMap getState(Scope scope) - 返回组件在给定scope的当前状态. 该方法永不会返回 null; 对于 StateMap 对象,如果 state没有被设置, StateMap's 版本将是 -1, 而 map的值将是 empty. 经常,一个新的 Map<String,String> 被创建来存储更新的值,然后setState()或 replace() 被调用.
- boolean replace(StateMap oldValue, Map<String, String> newValue, Scope scope) - 更新组件的状态值 (在给定的 scope)为新的值,仅在当前值与给定的 oldValue一样时执行. 如果 state 被更新为新的值, 返回true; 否则返回 false,如果state's value 不等于oldValue.
- void clear(Scope scope) - 从给定的scope下,清除组件状态所有的键值
得到当前map的 key/value 对
需求:脚本从状态管理器得到当前的 key/value 对,然后在 script 中使用(如更新等)。
方法:使用ProcessContext的getStateManager()方法, 然后从 StateManager调用 getStateMap() , 再 toMap() 转换为Map<String,String>形式的key/value对. 注意,StateMap 也有 get(key) 方法去简化获得 value的方法, 但是不如 Map用的普遍。必须在 StateManager 一次性设置完毕。
例子:
Groovy
import org.apache.nifi.components.state.Scope
def oldMap = context.stateManager.getState(Scope.LOCAL).toMap()
Jython
from org.apache.nifi.components.state import Scope
oldMap = context.stateManager.getState(Scope.LOCAL).toMap()
Javascript
var Scope = Java.type('org.apache.nifi.components.state.Scope');
var oldMap = context.stateManager.getState(Scope.LOCAL).toMap();
JRuby
java_import org.apache.nifi.components.state.Scope
oldMap = context.stateManager.getState(Scope::LOCAL).toMap()
更新 key/value 映射的值对
需求:脚本希望通过新的包含key/value的映射值对来更新 state map。
方法:为了得到当前的 StateMap 对象, 再次用ProcessContext调用 getStateManager() 方法, 然后 StateManager调用getStateMap() . 例子中假定为新的 Map, 但是使用上面的配方 (通过 toMap() 方法), 你可以使用存在的值创建新的 Map, 然后用于更新想要的记录. 注意,如果没有当前map (i.e. the StateMap.getVersion() returns -1),replace() 将不会工作, 因此例子中检查并相应地调用 setState() 或 replace(). 当从ExecuteScript的新实例运行时,该StateMap 版本将会是 -1, 当单次执行后, 如果鼠标右键 ExecuteScript processor,然后选择 View State, 将看到如下所示的信息:
例子:
Groovy
import org.apache.nifi.components.state.Scope
def stateManager = context.stateManager
def stateMap = stateManager.getState(Scope.CLUSTER)
def newMap = ['myKey1': 'myValue1']
if (stateMap.version == -1) {
stateManager.setState(newMap, Scope.CLUSTER);
} else {
stateManager.replace(stateMap, newMap, Scope.CLUSTER);
}
Jython
from org.apache.nifi.components.state import Scope
stateManager = context.stateManager
stateMap = stateManager.getState(Scope.CLUSTER)
newMap = {'myKey1': 'myValue1'}
if stateMap.version == -1:
stateManager.setState(newMap, Scope.CLUSTER)
else:
stateManager.replace(stateMap, newMap, Scope.CLUSTER)
Javascript
var Scope = Java.type('org.apache.nifi.components.state.Scope');
var stateManager = context.stateManager;
var stateMap = stateManager.getState(Scope.CLUSTER);
var newMap = {'myKey1': 'myValue1'};
if (stateMap.version == -1) {
stateManager.setState(newMap, Scope.CLUSTER);
} else {
stateManager.replace(stateMap, newMap, Scope.CLUSTER);
}
JRuby
java_import org.apache.nifi.components.state.Scope
stateManager = context.stateManager
stateMap = stateManager.getState(Scope::CLUSTER)
newMap = {'myKey1'=> 'myValue1'}
if stateMap.version == -1
stateManager.setState(newMap, Scope::CLUSTER)
else
stateManager.replace(stateMap, newMap, Scope::CLUSTER)
end
清空 state map
需求:清空 state map所有的e key/value 值。
方法:使用ProcessContext的getStateManager()方法, 然后调用StateManager的clear(scope)方法。
例子:
Groovy
import org.apache.nifi.components.state.Scope
context.stateManager.clear(Scope.LOCAL)
Jython
from org.apache.nifi.components.state import Scopecontext.state
Manager.clear(Scope.LOCAL)
Javascript
var Scope = Java.type('org.apache.nifi.components.state.Scope');
context.stateManager.clear(Scope.LOCAL);
JRuby
java_import org.apache.nifi.components.state.Scope
context.stateManager.clear(Scope::LOCAL)
存取控制器服务
在 NiFi ARchive (NAR) 结构中, Controller Services-控制器服务被暴露为 interfaces, 在 API JAR中. 例如 , DistributedCacheClient 是一个从 ControllerService扩展来的接口, 位于 nifi-distributed-cache-client-service-api JAR中, 在 nifi-standard-services-api-nar NAR. 其他的 NARs 如果想要引用interfaces (去创建新的 client implementation, e.g.) 必须指定 nifi-standard-services-api-nar 作为父级 NAR, 然后在processor的子模块提供 API JARs 的实例。
这是一些底层的细节,可能需要的以提升 Controller Services的使用, 这里提及主要是为了:
- 在 NiFi 1.0.0前, scripting NAR (包括 ExecuteScript 和 InvokeScriptedProcessor) 不需要指定nifi-standard-services-api-nar 作为父级. 这意味着只有明确的引用能被用于 ControllerServices 接口 (及其实现), 同样的原因, 只有没有要求其他不可用类的接口方法可以被使用. 这限制了 ExecuteScript 对Controller Services的使用
- NiFi 1.0.0, scripting processors 在nifi-standard-services-api-nar中存取 Controller Service interfaces (及其相关的classes) . 这包括DBCPService, DistributedMapCacheClient, DistributedSetCacheClient, HttpContextMap 和 SSLContextService. 但是我不相信nifi-standard-services-api-nar中其它的API 将会可用, 而且没有定制化 ControllerService interfaces 将被识别
Processors 总是倾向于使用 Controller Service 实例创建 property (如PropertyDescriptor 对象) 并且调用 identifiesControllerService(class) . 当 UI component被渲染时, 将会发现所有的实现了期望接口的 Controller Services , component's ID 被使用, 友好显示名称被显示给用户。
对于ExecuteScript, 我们可以让用户选择Controller Service 实例,通过让他指定名称或者 ID 来实现. 如果我们允许用户指定name, 脚本将不得不执行一个查询Controller Service实例列表去找到匹配名称的元素。 这在上面的博客中提到了, 这里不再重复. 如果用户输入实例的ID, 然后 (在 NiFi 1.0.0) 将会更加容易滴匹配对象并存取,在下面将会看到. 这个例子将使用DistributedMapCacheClientService 实例为 "distMapClient", 连接到DistributedMapCacheServer 实例 (在标准的缺省配置下, localhost:4557), 这里 client instance 的ID为 93db6734-0159-1000-b46f-78a8af3b69ed:
在ExecuteScript 配置中, dynamic property被创建, 名为 "clientServiceId" 并且设为 93db6734-0159-1000-b46f-78a8af3b69ed:
然后我们使用clientServiceId.asControllerService(DistributedMapCacheClient), 这里参数是对DistributedMapCacheClient类对象的引用. 例如, 我有一个预先填充的缓存,字符串 key 'a' 设为字符串值 'hello'. 让 Groovy script 使用 DistributedMapCacheServer进行工作。
一旦我们有了一个 DistributedMapCacheClient 实例, 然后就可以调用get(key, serializer, deserializer)去获取值. 在这个例子中,因为keys 和 values 都是Strings, 我们只需要一个 Serializer<String> 和 Deserializer<String> 实例传给 get() 方法. 该方法对于所有语言都是一样的,通过 StreamCallback 实例的创建(在本系列文章的 Part 2). 这个将从预先填充的服务器得到 key 'a' 的值,并且记录值("Result = hello")。
得到property(存储在 DistributedMapCacheServer)
需求:用户发布值到 DistributedMapCacheServer (如配置数据),然后使用脚本进行访问。
方法:使用上面描述的方法,创建一个StringSerializer 和 StringDeserializer 对象, 然后通过ID得到DistributedMapCacheClientService 实例, 然后调用服务的 get() 方法. 记录下结果到日志,方便后面查看。
例子:
Groovy
import org.apache.nifi.distributed.cache.client.DistributedMapCacheClient
import org.apache.nifi.distributed.cache.client.Serializer
import org.apache.nifi.distributed.cache.client.Deserializer
import java.nio.charset.StandardCharsets
def StringSerializer = {value, out -> out.write(value.getBytes(StandardCharsets.UTF_8))}
as Serializer<String>
def StringDeserializer = { bytes -> new String(bytes) } as Deserializer<String>
def myDistClient = clientServiceId.asControllerService(DistributedMapCacheClient)
def result = myDistClient.get('a', StringSerializer, StringDeserializer)
log.info("Result = $result")
Jython
from org.python.core.util import StringUtil
from org.apache.nifi.distributed.cache.client
import DistributedMapCacheClient, Serializer, Deserializer
# Define a subclass of Serializer for use in the client's get() method
class StringSerializer(Serializer):
def __init__(self):
pass
def serialize(self, value, out):
out.write(value)
# Define a subclass of Deserializer for use in the client's get() method
class StringDeserializer(Deserializer):
def __init__(self):
pass
def deserialize(self, bytes):
return StringUtil.fromBytes(bytes)
myDistClient = clientServiceId.asControllerService(DistributedMapCacheClient)
result = myDistClient.get('a', StringSerializer(), StringDeserializer())
log.info('Result = ' + str(result))
Javascript
var DistributedMapCacheClient =
Java.type('org.apache.nifi.distributed.cache.client.DistributedMapCacheClient');
var Serializer = Java.type('org.apache.nifi.distributed.cache.client.Serializer');
var Deserializer = Java.type('org.apache.nifi.distributed.cache.client.Deserializer');
var StandardCharsets = Java.type('java.nio.charset.StandardCharsets');
var StringSerializer = new Serializer(function(value, out) {
out.write(value.getBytes(StandardCharsets.UTF_8));
})
var StringDeserializer = new Deserializer(function(arr) {
// For some reason I had to build a string from the character codes in the "arr" array
var s = "";
for(var i = 0; i < arr.length; i++) {
s = s + String.fromCharCode(arr[i]);
}
return s;
})
var myDistClient = clientServiceId.asControllerService(DistributedMapCacheClient.class);
var result = myDistClient.get('a', StringSerializer, StringDeserializer);
log.info("Result = "+ result);
JRuby
java_import org.apache.nifi.distributed.cache.client.DistributedMapCacheClient
java_import org.apache.nifi.distributed.cache.client.Serializer
java_import org.apache.nifi.distributed.cache.client.Deserializer
java_import java.nio.charset.StandardCharsets
# Define a subclass of Serializer for use in the client's get() method
class StringSerializer
include Serializer
def serialize(value, out)
out.write(value.to_java.getBytes(StandardCharsets::UTF_8))
end
end
# Define a subclass of Deserializer for use in the client's get() method
class StringDeserializer
include Deserializer
def deserialize(bytes)
bytes.to_s
end
end
myDistClient = clientServiceId.asControllerService(DistributedMapCacheClient.java_class)
result = myDistClient.get('a', StringSerializer.new, StringDeserializer.new)
log.info('Result = ' + result)
原文地址:https://my.oschina.net/u/2306127/blog/858943
1、Nifi:基本认识
2、Nifi:基础用法及页面常识
3、Nifi:ExcuseXXXScript组件的使用(一)
4、Nifi:ExcuseXXXScript组件的使用(二)
5、Nifi:ExcuseXXXScript组件的使用(三)
6、Nifi:自定义处理器的开发