Nifi元件指令碼開發—ExecuteScript 使用指南(三)

糖拌蕃茄發表於2021-02-24

上一篇: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. 這裡有兩個重要事需要了解:

  1. 因為 property 繫結為變數名, dynamic properties的命名規則必須滿足相應的程式語言的規範。 例如, Groovy 不支援 (.) 作為變數名字元, 像 "my.value" 引起processor處理失敗. 有效的可選項是 "myValue"
  2. 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的使用, 這裡提及主要是為了:

  1. 在 NiFi 1.0.0前, scripting NAR (包括 ExecuteScript 和 InvokeScriptedProcessor) 不需要指定nifi-standard-services-api-nar 作為父級. 這意味著只有明確的引用能被用於 ControllerServices 介面 (及其實現), 同樣的原因, 只有沒有要求其他不可用類的介面方法可以被使用. 這限制了 ExecuteScript 對Controller Services的使用
  2. 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(keyserializerdeserializer)去獲取值. 在這個例子中,因為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

相關文章