深入解析Jolt

Jolt概覽

目前json數據已經逐步取代xml成爲主流的數據交換和存儲格式。在數據交互過程中經常遇到數據格式不一致的問題,在xml時代有XSTL(Extensible Stylesheet Language Transformation)可以實現,在json時代,我們可以用jolt。在當今大數據場景主流的ETL pipeline 工具如NIFI和 StreamSets都支持jolt作爲json轉換插件,在讀取ElasticSearch, MongoDb, Cassandra等數據時可以用jolt做數據格式轉換。

Jolt的核心功能:

  • 提供了一組轉換,可以“鏈接”在一起形成整體JSON到JSON的轉換。
  • 專注於轉換JSON數據的結構,而不是操縱特定值
  • 消費並生成新結構的JSON:實際是內存中的Map,List,String等。

jolt插件主要轉(transfrom)換方法
(1)shift:主要作用是拷貝,將輸入json的某個結構拷貝到目標json
(2)default:主要作用是設置默認值,如果輸入json某個value不存在,則使用默認值補齊
(3)remove:主要作用是刪除,可以刪除輸入json結構的某個結構
(4)sort:對key值進行排序
(5)cardinality:jsonobject和jsonarray之間的切換
(6)modify:修改json數值

每個轉換都有自己的DSL(Domain Special Language 領域特定語言),以便定義於它特定的工作。目前,所有默認插件變換隻會影響數據的“結構”。要進行數據操作,需要自行編寫Java代碼。

Jolt轉換語法規則DSL的說明

Jolt使用json數據定義(specification)轉換規則。由於json語法結構的限制,jolt需要組合不同的DSL以便解析程序能夠正確的執行、避免歧義。所以jolt約定轉換規則的輸入必須是一個json array,通過定義多個轉換spec定義形成一個轉換鏈從而實現在不同語法規則場景下的轉換。
Jolt spec示例:

[
  {
    "operation": "shift",
    "spec": {}
  },
  {
    "operation": "modify-default-beta",
    "spec": {}
  }
  ,
  {
    "operation": "sort"
  }
]

shift:

在shift語境中,json的key值表示輸入json的路徑,value值表示目標路徑。簡單理解就是key是“匹配”,value是“去哪”,告訴程序匹配哪部分數據輸出到什麼位置 。多少數據轉換場景是同樣業務含義的字段名稱不一樣,位置不一樣等,這種可以通過shift語義實現。

shift通配符

  • “*” 通配符:僅可以匹配input的key,不能匹配value
    使用 “*”通配符的時候,需要保證輸入json具有相同的層級結構,它可以匹配某一個層級結構中的所有key,也可以匹配一個key的部分字符。
  • “&”通配符
    “&”通配符有兩個參數&(0,1),第一個參數0表示從當前層級回溯多少級去尋找所需key,第二個參數1表示取第幾個匹配到的參數(0表示取整體,1表示取第一個匹配結果,2表示取第二個匹配結果)。參數可以省略,&=&0=&(0)=&(0,0)
  • “$”通配符
    KaTeX parse error: Expected 'EOF', got '&' at position 5: 通配符和&̲通配符具有相同的語法結構,都有…通配符通常是將輸入Json的key值作爲目標json的value使用,
  • “@”通配符
    “@”通配符也有兩個參數@(0,1),"@“和”""@"valueJsonvalue,"用法類似,不過"@"取得是指定位置的value作爲目標Json的value值,而“”取的是指定位置的key作爲目標Json的value值
  • “#”通配符
    “#”通配符最有用的一點是,可以按照輸入Json某個字段的取值,對輸出Json設置不同的值
  • “|”通配符
    “|”表示“或”的邏輯,很容易理解,它只會對列出的字符進行匹配

shift數據轉換場景

(1)拷貝JSONObject

{
  "*": {
    "@": "[&1]"
  }
}

(2)字典映射
定義

 "TYPE": {
        "A": {
          "#AA": "TYPE"
        },
        "B": {
          "#BB": "TYPE"
        }
}

輸入

{
  "TYPE": "A"
}

輸出

{
  "TYPE" : "AA"
}

如前文所說“jolt所有默認插件變換隻會影響數據的結構,要進行數據操作,需要自行編寫Java代碼”。數據字典碼錶映射是典型的數據值的操作,枚舉值較少的情況下,可以通過以上變通的方法實現。但如果有大量的碼錶,則需要另外自行實現代碼。

(3)json key輸出成value

{
	"foo": {
		"*": {
			"$": "test.&(1,0).id"
		}
	}
}

輸入

{
	"foo": {
		"barKey":"barValue",
		"bazKey":"bazValue"
	}
}

輸出

{
	"test": {
		"barKey": {
			"id": "barKey"
		},
		"bazKey": {
			"id": "bazKey"
		}
	}
}

modify:

jolt內置了一些修改json數據的方法,通過modify語法實現,包含modify-overwrite-beta、modify-default-beta、modify-define-beta。
在modify語境中,json的key值表示輸入json的路徑,value值表示對該節點value的變換。在modify語境中語法定義的葉子節點所指向的input結構中必須也是葉子節點,這樣才能夠對這個葉子節點的值進行修改。

modify的“=”操作符

Modifier定義了大量的內置函數,可以實現常見的數據轉換,比如大小寫轉換、字符串處理、求和、計數等、排序等。在value定義字符串中以“=”號開頭標識後續是方法引用。modify語境中vaule值只能是常量或者輸入json中的值(以@符號引用)。
支持以下語法:

  • =abs 這是個語法糖,等價於=abs(@(1,&0))
  • =abs(@(1,&0)) 調用“abs”函數,參數是當前節點的value,@(1,&0)的含義同shift中一樣,基於當前key的位置獲得相對位置的value
  • =abs(@(1,&0),-1,-3) 調用“abs”函數,除輸入json中的value外,還有其他的兩個參數。

自定義數據處理

jolt默認不處理數據值的轉換,在複雜的數據字典映射時上文的定義方式非常繁瑣,我們需要自定義數據字典的定義語義。
通常情況下,數據字典映射是兩個字符串集合的一一對應,json的key-value結構剛好符合這個結構,在jolt整體語義體系下,我們可以做這樣的約定:

{
	"operation": "com.github.weixingluo.json.transfrom.DataDictionaryTransform",
	"spec": {
		"rating": {
			"primary": {
				"value": "=dict(@(1,name),dictName)"
			}
		},
		"dictionary": {
			"dictName": {
				"x": "yy",
				"y": "zz"
			}
		}
	}
}

因爲字典映射類似於修改,所以我們直接接用modity的語義定義,只是針對字典映射表做特殊處理。也就是說json的key值表示輸入json的路徑,value值表示對該節點value的變換。
在operation定義了一個新的語義,我們在spec中定義了一個特殊的屬性“dictionary”,我們在這個對象中定義字典映射表。除"dictionary"外,我們認爲其他的key值都是普通的json轉換定義。我們新定義個函數“dict”,約定“dict(@(1,name),dictName)”含義爲對當前key的值做映射,依賴的數據映射字典爲“dictName”。這樣我們就定義了一個基於字典映射當前節點value到一個新的集合的方案。
爲了讓我們的定義執行起來,我們需要在jolt的體系中實現自己的代碼。jolt在編譯spec定義時,如果operation的值不在默認的值範圍時,jolt會默認這是一個類路徑,並嘗試通過默認構造函數反射出來。在spec中我們定義了一個新的映射,jolt在初始化DataDictionaryTransform時將spec整個map作爲構造函數傳給了DataDictionaryTransform。
核心代碼

public class DataDictionaryFunction  implements Function {
	private Map<String,Object> dictionary;
	public DataDictionaryFunction(Map<String,Object> dictionary) {
		this.dictionary = Collections.unmodifiableMap(dictionary);
	}

	@Override
	public Optional<Object> apply(Object... args) {
	//...
		Map dict = (Map)dictionary.get(args[1]);
		Object value = dict.get(args[0]);
		return value==null?Optional.empty():Optional.of(value);
	}
}
public class DataDictionaryTransform implements ContextualTransform, SpecDriven {
	//...
	public DataDictionaryTransform(Object spec) {
		//...
        Object dict = spec.get(dictString);
		Map<String, Object> dictionary = (Map<String, Object>) dict;
		DataDictionaryFunction dictMapping = new DataDictionaryFunction(dictionary);
		Map<String,Function> functionsMap = new HashMap<>();
		functionsMap.put("dict", dictMapping);

		functionsMap = Collections.unmodifiableMap(functionsMap);
		TemplatrSpecBuilder templatrSpecBuilder = new TemplatrSpecBuilder(OpMode.OVERWRITR, functionsMap);
		rootSpec = new ModifierCompositeSpec(ROOT_KEY, (Map<String, Object>) spec, OpMode.OVERWRITR,
				templatrSpecBuilder);
	}

	@Override
	public Object transform( final Object input, final Map<String, Object> context ) {
		Map<String, Object> contextWrapper = new HashMap<>(  );
        contextWrapper.put( ROOT_KEY, context );

        MatchedElement rootLpe = new MatchedElement( ROOT_KEY );
        WalkedPath walkedPath = new WalkedPath();
        walkedPath.add( input, rootLpe );

        rootSpec.apply( ROOT_KEY, Optional.of( input), walkedPath, null, contextWrapper );
        return input;
	}

}

參考

Jolt官方文檔: https://github.com/bazaarvoice/jolt
jolt插件使用簡介: https://blog.csdn.net/m0_37674755/article/details/85530142
JSON JOLT介紹 及語法詳解-shift篇: https://blog.csdn.net/weixin_36048246/article/details/89287200
jolt操作演示: https://jolt-demo.appspot.com

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章