java vo转map_Jython:在 Java 程序里运行 Python 代码 4.5
彭翌
彭翌,網(wǎng)易游戲資深運(yùn)維開發(fā)工程師,從事大數(shù)據(jù)相關(guān)的基礎(chǔ)架構(gòu)平臺(tái)研發(fā)工作,業(yè)余時(shí)間也關(guān)注分布式系統(tǒng)等相關(guān)領(lǐng)域。
前言
眾所周知,JVM 在大數(shù)據(jù)基礎(chǔ)架構(gòu)領(lǐng)域可以說是獨(dú)占鰲頭,當(dāng)我們需要開發(fā)大數(shù)據(jù)處理的相關(guān)組件時(shí),首先會(huì)想到要使用的語言便是 Java 和 Scala。相比于 Java,Scala 的代碼會(huì)更加簡(jiǎn)潔,但也有著高得多的入門門檻,因此為了保證核心組件的穩(wěn)定和易于維護(hù),我們多數(shù)時(shí)候都會(huì)更傾向于使用 Java 進(jìn)行開發(fā)。
不過,組件中相對(duì)穩(wěn)定的基本功能和框架尚且不談,對(duì)于那些需要快速靈活變化的部分,使用 Java 進(jìn)行開發(fā)則會(huì)有些捉襟見肘。例如,我們?cè)跒闃I(yè)務(wù)方開發(fā)一套通用的實(shí)時(shí)作業(yè)時(shí),業(yè)務(wù)方需要作業(yè)在特定的處理環(huán)節(jié)中支持通過配置自定義的代碼來指定算子的行為,并且在配置發(fā)生變化時(shí)需要可以在不重啟實(shí)時(shí)作業(yè)的情況下進(jìn)行熱更新。直接使用 Java 實(shí)現(xiàn)這樣的功能無疑會(huì)有點(diǎn)力不從心,為此我們就需要借助動(dòng)態(tài)語言的力量了。
實(shí)際上,在 JVM 平臺(tái)上使用動(dòng)態(tài)語言的場(chǎng)景并不少見:Groovy 便是為此而生的一門語言。盡管在部分場(chǎng)景下 Groovy 確實(shí)是不錯(cuò)的選擇,但對(duì)于大數(shù)據(jù)分析來說,Groovy 并不為多數(shù)數(shù)據(jù)開發(fā)人員所熟知,相比之下 Python 會(huì)是更好的選擇。
目前也有不少的大數(shù)據(jù)框架支持用戶提交運(yùn)行 Python 代碼:
Hadoop MapReduce 借助 Hadoop Streaming,使用標(biāo)準(zhǔn)輸入流和標(biāo)準(zhǔn)輸出流進(jìn)行進(jìn)程間的數(shù)據(jù)交換,可以運(yùn)行包括 Python 在內(nèi)任意語言寫成的可執(zhí)行文件
Apache Spark 提供了 pyspark 編程入口,其使用了 Py4J 來實(shí)現(xiàn) JVM 與 Python 進(jìn)程間的高效數(shù)據(jù)傳輸
Apache Flink 則使用了 Jython 來運(yùn)行用戶的 Python 代碼。
最終,我們選擇了使用 Jython 來實(shí)現(xiàn)這樣的功能。Jython 類似于 Groovy,能夠與宿主 Java 程序在同一個(gè) JVM 進(jìn)程中運(yùn)行,相比于 Hadoop Streaming 或是 Py4J 的方案減少了進(jìn)程間數(shù)據(jù)傳輸?shù)膿p耗,以換來更高的性能。
但在使用 Jython 的時(shí)候我們?nèi)匀恍枰⒁鈳c(diǎn):
部分 PyPI 包可能無法在 Jython 中運(yùn)行,尤其是那些包含 C 語言擴(kuò)展的包
和 Groovy 一樣,隨意地使用 Jython 可能會(huì)導(dǎo)致內(nèi)存泄漏
目前,Jython 已在 2017 年 6 月發(fā)布了 2.7.1 版,支持所有 Python 2.7 語法。
Jython 基本使用
本文剩下的內(nèi)容會(huì)集中介紹如何在 Java 程序中使用 Jython。關(guān)于其他使用 Jython 的方式,可以參考 Jython 官方給出的 Jython Book,這里我們便不再贅述。
實(shí)際上,Jython 的官方文檔也給出了在 Java 中嵌入 Jython 的基本示例,極其簡(jiǎn)單:
import?org.python.util.PythonInterpreter;?import?org.python.core.*;?
public?class?SimpleEmbedded?{?
????public?static?void?main(String[]?args)?throws?PyException?{?
????????PythonInterpreter?interp?=?new?PythonInterpreter();
????????System.out.println("Hello,?brave?new?world");
????????interp.exec("import?sys");
????????interp.exec("print?sys");
????????interp.set("a",?new?PyInteger(42));
????????interp.exec("print?a");
????????interp.exec("x?=?2+2");
????????PyObject?x?=?interp.get("x");
????????System.out.println("x:?"+x);
????????System.out.println("Goodbye,?cruel?world");
????}
}
簡(jiǎn)單,但并不可用。
首先,PythonInterpreter 是個(gè)非常重的類,其中包含了 Jython 用于編譯 Python 代碼所需的所有資源和上下文信息。你不會(huì)想要大量創(chuàng)建這樣的實(shí)例的。
此外,Jython 的實(shí)現(xiàn)導(dǎo)致對(duì) PythonInterpreter.eval 方法的重復(fù)調(diào)用會(huì)對(duì)相同的 Python 代碼不斷重復(fù)編譯運(yùn)行,導(dǎo)致內(nèi)存泄漏。
要解決以上問題,我們需要復(fù)用 PythonInterpreter 對(duì)象,并盡可能不要調(diào)用 PythonInterpreter.eval 方法。
復(fù)用 PythonInterpreter 對(duì)象十分簡(jiǎn)單:將其實(shí)現(xiàn)為單例維護(hù)起來即可。你可以以任何形式實(shí)現(xiàn)這樣的單例模式,簡(jiǎn)單起見我們這里直接將其設(shè)置為一個(gè) private static final 變量:
public?class?PythonRunner?{????private?static?final?PythonInterpreter?intr?=?new?PythonInterpreter();
????public?PythonRunner(String?code)?{
????????//?...
????}
????public?Object?run()?{
????????//?...
????}
}
要想繞過 PythonInterpreter.eval 并不容易,畢竟這是 PythonInterpreter 提供給我們唯一可以運(yùn)行指定 Python 代碼并獲取結(jié)果的方法。
Groovy 提供了 GroovyShell.parse 方法,可以對(duì)給定的 Groovy 代碼進(jìn)行編譯,并返回一個(gè) Script 對(duì)象。Groovy 這里做的事情實(shí)際上是把客戶端給定的 Groovy 代碼封裝在了一個(gè)新的 Java 類中(這個(gè)類繼承了 Script),因此實(shí)際上程序可以使用這個(gè) Script 對(duì)象的類創(chuàng)建出新的 Script 對(duì)象,即可復(fù)用這段 Groovy 代碼。
我們同樣可以在 Jython 這邊實(shí)現(xiàn)類似的功能 —— 實(shí)際上官方的 Jython Book 有提到類似的做法,名為對(duì)象工廠模式。按照 Jython Book 中給出的示例,你可以將你需要使用的 Python 代碼放到一個(gè) Python 類中,再進(jìn)行編譯,但考慮到我們的場(chǎng)景比較簡(jiǎn)單,這里我們就簡(jiǎn)單地將代碼放在一個(gè) Python 函數(shù)中即可:
import?org.python.core.PyFunction;import?org.python.util.PythonInterpreter;
public?class?PythonRunner?{
????private?static?final?PythonInterpreter?intr?=?new?PythonInterpreter();
????private?static?final?String?FUNC_TPL?=?String.join("\n",?new?String[]{
????????"def?func():",
????????"????%s",
????????"",
????});
????private?final?PyFunction?func;
????public?PythonRunner(String?code)?{
????????//?渲染函數(shù)內(nèi)容
????????String[]?lines?=?code.split("\n");
????????for?(int?i?=?1;?i?????????????lines[i]?=?"????"?+?lines[i];
????????code?=?String.join("\n",?lines);
????????code?=?String.format(FUNC_TPL,?code);
????????//?編譯并獲取?PyFunction?對(duì)象
????????intr.exec(code);
????????func?=?(PyFunction)?intr.get(funcName);
????}
????public?Object?run()?{
????????//?使用?PyFunction?對(duì)象的?__call__?方法,調(diào)用指定的?Python?代碼
????????return?func.__call__();
????}
}
功能擴(kuò)展
目前,你已經(jīng)學(xué)到了如何在 Java 程序中使用 Jython 安全地運(yùn)行 Python 代碼,你可以對(duì)上述代碼進(jìn)行進(jìn)一步的擴(kuò)展來滿足你的需求。這里我再簡(jiǎn)單介紹下我們做的兩個(gè)比較有用的擴(kuò)展。
在 Python 代碼中使用 Java 對(duì)象
在你使用編譯后得到的 PyFunction 對(duì)象時(shí),你可能會(huì)注意到它的 __call__ 方法可以接收任意個(gè)類型為 PyObject 的參數(shù)。這是不是說,我們得把我們的 Java 對(duì)象轉(zhuǎn)換成 PyObject,我們的 Python 代碼才能使用這些 Java 對(duì)象呢?
答案是否定的,實(shí)際上 Jython 已經(jīng)實(shí)現(xiàn)了類似的自動(dòng)轉(zhuǎn)換功能。如果你提供的是「標(biāo)準(zhǔn)的」 Java 對(duì)象,那么 Jython 就會(huì)把它「mock」 成對(duì)應(yīng)的 Python 基本類型對(duì)象:
所有的 Java 基本數(shù)據(jù)類型都會(huì)被轉(zhuǎn)換為對(duì)應(yīng)的 Python 基本數(shù)據(jù)類型(例如 short 轉(zhuǎn) int、boolean 轉(zhuǎn) bool)
可以像使用普通 Python dict 對(duì)象那樣使用 java.util.Map 實(shí)例
可以像使用普通 Python list 對(duì)象那樣使用 java.util.List 實(shí)例
舉個(gè)例子,我們的項(xiàng)目需要使用到 FastJSON 的 JSONObject,而這個(gè)類實(shí)現(xiàn)了 java.util.Map,因此在我們的 Python 代碼中,我們只要將它當(dāng)做一個(gè)普通的 Python dict 來使用就好了:
def?func(json):????if?not?json['test']:
????????json['test']?=?True
????return?True
值得注意的是,Jython 并不會(huì)改變你的對(duì)象的類型:如果你在你的 Python 代碼中使用 instanceof 的話就會(huì)發(fā)現(xiàn),實(shí)際上傳入對(duì)象的類型并未改變。除外,如果你對(duì)一個(gè) Java bool 值在 Python 代碼中使用 is True 或 is False 判斷時(shí),你都會(huì)得到 False 結(jié)果。實(shí)際上 Jython 僅僅是為你給定的 Java 對(duì)象模擬出了對(duì)應(yīng)的 Python 類型的行為(鴨子類型),但實(shí)際上它們依然是不同的類型。
引入 PyPI 包
為了進(jìn)一步減少我們需要寫的 Python 代碼量,我們也可以把部分公用的 Python 代碼維護(hù)在統(tǒng)一的包中,然后在自定義的 Python 代碼中 import 并使用它。要做到這一點(diǎn),首先我們要設(shè)置好 sys.path。
Jython 默認(rèn)會(huì)把當(dāng)前工作目錄放到 sys.path 中(實(shí)際上這應(yīng)該是所有 Python 解釋器的標(biāo)準(zhǔn)行為),所以如果我們需要復(fù)用某個(gè)自制的 Python 庫文件,我們只要將它放在當(dāng)前工作目錄下然后 import 就可以了。但如果我們想要使用 PIP 安裝的包,我們就需要額外做一些配置了。
實(shí)際上,我們只要把本地的 PIP 安裝目錄路徑放到 Jython 的 sys.path 中即可。有很多種方法可以做到這一點(diǎn),但最安全的做法就是直接詢問本地安裝好的 Python:
public?class?PythonRunner?{????private?static?final?PythonInterpreter?intr?=?new?PythonInterpreter();
????static?{
????????intr.exec("import?sys");
????????try?{
????????????//?啟動(dòng)子進(jìn)程,運(yùn)行本地安裝的?Python,獲取?sys.path?配置
????????????Process?p?=?Runtime.getRuntime().exec(new?String[]{
????????????????"python2",?"-c",?"import?json;?import?sys;?print?json.dumps(sys.path)"});
????????????p.waitFor();
????????????//?從中獲取到相關(guān)的?PIP?安裝路徑,放入?Jython?的?sys.path
????????????String?stdout?=?IOUtils.toString(p.getInputStream());
????????????JSONArray?syspathRaw?=?JSONArray.parseArray(stdout);
????????????for?(int?i?=?0;?i?????????????????String?path?=?syspathRaw.getString(i);
????????????????if?(path.contains("site-packages")?||?path.contains("dist-packages"))
????????????????????inter.exec(String.format("sys.path.insert(0,?'%s')",?path));
????????????}
????????}?catch?(Exception?ex)?{}
????}
????//?...
}
正如我在一開始所說的那樣,并不是所有 PyPI 包都能在 Jython 中運(yùn)行,尤其是那些包含 C 語言代碼的包。因此,在你做更多的嘗試前,不妨先在 Jython Shell 中 import 一下你想使用的包,驗(yàn)證一下。
結(jié)語
這篇博文一方面是對(duì)最近我們?cè)谧龅墓ぷ鬟M(jìn)行一次總結(jié),同時(shí)希望這些經(jīng)驗(yàn)也能夠幫助到大家。
不過,我不會(huì)認(rèn)為 Jython 是個(gè) 100% 安全的解決方案 —— 實(shí)際上,你在使用的過程中有可能會(huì)遇到十分詭異的 Bug,而且 Jython 的 API 和文檔也還遠(yuǎn)算不上是「友好」。但不管怎么說,如果你有和我們類似的需求的話,也不妨嘗試一下 Jython。
關(guān)注我們,獲一手游戲運(yùn)維方案
總結(jié)
以上是生活随笔為你收集整理的java vo转map_Jython:在 Java 程序里运行 Python 代码 4.5的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 油车车主注意了!4月起可在加油区域使用手
- 下一篇: java python混合开发_Go+P