寫在前面
在微服務架構大行其道的今天,對於將程序進行嵌套調用的做法其實並不可取,甚至顯得有些愚蠢。當然,之所以要面對這個問題,或許是因為一些歷史原因,或者僅僅是為了簡單。恰好我在項目中就遇到了這個問題,需要在Java程序中調用Python程序。關於在Java中調用Python程序的實現,根據不同的用途可以使用多種不同的方法,在這里就將在Java中調用Python程序的方式做一個總結。
直接通過Runtime進行調用
我們知道,在Java中如果需要調用第三方程序,可以直接通過Runtime實現,這也是最直接最粗暴的做法。
public class InvokeByRuntime {
/**
* @param args
* @throws IOException
* @throws InterruptedException
*/
public static void main(String[] args) throws IOException, InterruptedException {
String exe = "python";
String command = "D:\\calculator_simple.py";
String num1 = "1";
String num2 = "2";
String[] cmdArr = new String[] {exe, command, num1, num2};
Process process = Runtime.getRuntime().exec(cmdArr);
InputStream is = process.getInputStream();
DataInputStream dis = new DataInputStream(is);
String str = dis.readLine();
process.waitFor();
System.out.println(str);
}
}
輸出:
3
calculator_simple.py:
# coding=utf-8
from sys import argv
num1 = argv[1]
num2 = argv[2]
sum = int(num1) + int(num2)
print sum
顯然,在Java中通過Runtime調用Python程序與直接執行Python程序的效果是一樣的,可以在Python中讀取傳遞的參數,也可以在Java中讀取到Python的執行結果。需要注意的是,不能在Python中通過return語句返回結果,只能將返回值寫入到標准輸出流中,然后在Java中通過標准輸入流讀取Python的輸出值。
通過Jython調用
通過Jython調用Python?我在聽到這個概念的時候一臉懵逼,不是說好的在Java中調用Python程序嗎?這個Jython是什么鬼?難道是一個在Java中調用Python程序的組件或工具?其實,關於Jython是什么這個疑問,我估計有許多人在一開始接觸的時候也是很疑惑的,下面我們就一一道來。
1. 什么是Jython
Jython主頁:http://www.jython.org/currentdocs.html
按照官方的定義,Jython是Python語言在Java平台的實現。這個概念似乎有點拗口,反正我一開始並沒有理解。Python難道不已經是一門語言了嗎?什么叫做Jython是Python語言在Java平台的實現?
實際上,之所以存在這樣的困惑主要是因為我們對Python語言的相關概念掌握和理解不清楚導致的。
Python其實只是一個語言規范,它存在多個不同語言實現的版本。具體來說,目前Python語言存在如下幾個具體實現:
(1)CPython:CPython是標准Python,也是其他Python編譯器的參考實現。通常提到“Python”一詞,都是指CPython。CPython由C編寫,將Python源碼編譯成CPython字節碼,由虛擬機解釋執行。沒有用到JIT等技術,垃圾回收方面采用的是引用計數。
(2)Jython:Jython是在JVM上實現的Python,由Java編寫。Jython將Python源碼編譯成JVM字節碼,由JVM執行對應的字節碼。因此能很好的與JVM集成,比如利用JVM的垃圾回收和JIT,直接導入並調用JVM上其他語言編寫的庫和函數。
(3)IronPython:IronPython與Jython類似,所不同的是IronPython在CLR上實現的Python,即面向.NET平台,由C#編寫。IronPython將源碼編譯成TODO CLR,同樣能很好的與.NET平台集成。即與Jython相同,可以利用.NET框架的JIT、垃圾回收等功能,能導入並調用.NET上其他語言編寫的庫和函數。IronPython默認使用Unicode字符串。
(4)PyPy:這里說的PyPy是指使用RPython實現,利用Tracing JIT技術實現的Python,而不是RPython工具鏈。PyPy可以選擇多種垃圾回收方式,如標記清除、標記壓縮、分代等。
(5)Pyston:Pyston由Dropbox開發,使用C++11編寫,采用Method-at-a-time-JIT和Mark Sweep——Stop the World的GC技術。Pyston使用類似JavaScript V8那樣的多層編譯,其中也用到了LLVM來優化代碼。
所以,我們現在再來理解什么是Jython就非常清楚了:Jython是Python語言規范在Java平台的具體實現。具體來說,可以將Python源碼編譯為JVM可以解釋執行的字節碼。
Jython原本叫做JPython,於1997年由Jim Hugunin創建,后來在1999年2.0版本發布的時候由Barry Warsaw更名為Jython,在這里我們就不再深究為什么要把JPython更名為Jython的原因了。注意: Jython從2.0版本開始就與CPython的版本保持一致,即:Jython 2.7與CPython 2.7保持對應。
雖然我們理解了什么是Jython,但是還存在一個疑問,為什么Python語言存在這么多不同語言的實現呢?為什么不能就只存在一個C語言實現的版本就可以了呢?存在這么多版本,真的給初學者帶來了許多困惑。
當然,要回答這個問題可能就需要深究一些歷史的原因了,就此打住。我們在此只討論使用Jython能做什么以及如何使用Jython?
2. 使用Jython能做什么
既然Jython是Python語言在Java平台的實現,是Java語言實現的,那么是否可以在Jython程序中調用Java,在Java中也能調用Jython呢?
答案是肯定的,實際上,Jython的主要通途就是在Java中調用Python程序;而且,還可以直接在Jython程序中引用Java。
3. 如何使用Jython
3.1 安裝Jython
在Jython的官方下載頁面我們可以看到如下描述(詳見:http://www.jython.org/downloads.html)
顯然,可以下載2個Jython的jar包。其中,jython-installer-${version}.jar
是用於安裝Jython的,jython-standalone-${version}.jar
用於嵌入到Java程序中使用。
什么意思?我一開始也是很疑惑,為什么要提供2個不同的jar包呢?他們有什么不同呢?2個不同的Jar包如何使用呢?
首先,jython-installer-${version}.jar
用於安裝Jython,就好比我們需要安裝JRE,用於運行Java程序。除此之外,當需要在Python程序中引用一些公共的第三方庫時,也需要先安裝Jython才能下載所依賴的模塊。
下載jython-installer-${version}.jar
完畢之后,進入控制台,執行如下命令:
java -jar jython-installer-${version}.jar
此時會彈出一個圖形化的安裝界面,只需要一步一步選擇相應參數進行安裝即可。安裝完畢之后,請將Jython安裝目錄添加為環境變量JYTHON_HOME,同時添加bin目錄到PATH變量中:PATH=$PATH:$JYTHON_HOME/bin
。
進入控制台,執行如下命令就可以進入Jython的交互環境,這與CPython(我們通常說的Python)的命令行交互環境是一樣的。
> jython
Jython 2.7.0 (default:9987c746f838, Apr 29 2015, 02:25:11)
[Java HotSpot(TM) 64-Bit Server VM (Oracle Corporation)] on java1.8.0_121
Type "help", "copyright", "credits" or "license" for more information.
>>> print("hello,world")
hello,world
>>>
當然,我們還可以使用jython命令運行一個Python程序。
> jython helloworld.py
hello,world
helloworld.py:
import sys
print("hello,world")
上面我們看到在Jython官網提供了2個Jar包,一個用於安裝Jython,執行Python程序。那么,jython-standalone-${version}.jar
又有什么用途呢?
實際上,當我們需要在Java中調用Python程序時,除了直接使用Java的Runtime調用,還可以直接使用Jython的API進行調用,而且通過Jython API可以直接調用Python程序中的指定函數或者對象方法,粒度更加精細。
當我們需要調用Jython的API時有兩種方式:
第一,如果項目使用Maven進行構建,可以直接添加Jython的依賴配置到pom.xml文件中,如:
<dependency>
<groupId>org.python</groupId>
<artifactId>jython</artifactId>
<version>2.7.0</version>
</dependency>
第二,可以直接將jython-standalone-${version}.jar
添加到項目classpath中,這樣也可以調用Jython的相關API了。也就是說,jython-standalone-${version}.jar
就是一個提供Jython API的jar獨立jar包。
3.2 Java調用Python程序實踐
Java通過Jython API調用Python程序,有幾種用法:
(1)在Java中執行Python語句,相當於在Java中嵌入了Python程序,這種用法不常見,也沒有太大的實際意義。
public static void main(String[] args) {
System.setProperty("python.home", "D:\\jython2.7.0");
PythonInterpreter interp = new PythonInterpreter();
// 執行Python程序語句
interp.exec("import 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);
}
輸出:
42
x: 4
(2)在Java中簡單調用Python程序,不需要傳遞參數,也不需要獲取返回值。
public static void main(String[] args) throws IOException {
System.setProperty("python.home", "D:\\jython2.7.0");
String python = "D:\\simple_python.py";
PythonInterpreter interp = new PythonInterpreter();
interp.execfile(python);
interp.cleanup();
interp.close();
}
simple_python.py:
# coding=utf-8
print("Do simple thing in Python")
print("輸出中文")
(3)在Java中單向調用Python程序中的方法,需要傳遞參數,並接收返回值。Python既支持面向函數式編程,也支持面向對象編程。因此,調用Python程序中的方法也分別以面向函數式編程和面向對象式編程進行說明。
public static void main(String[] args) throws IOException {
System.setProperty("python.home", "D:\\jython2.7.0");
// 1. Python面向函數式編程: 在Java中調用Python函數
String pythonFunc = "D:\\calculator_func.py";
PythonInterpreter pi1 = new PythonInterpreter();
// 加載python程序
pi1.execfile(pythonFunc);
// 調用Python程序中的函數
PyFunction pyf = pi1.get("power", PyFunction.class);
PyObject dddRes = pyf.__call__(Py.newInteger(2), Py.newInteger(3));
System.out.println(dddRes);
pi1.cleanup();
pi1.close();
// 2. 面向對象式編程: 在Java中調用Python對象實例的方法
String pythonClass = "D:\\calculator_clazz.py";
// python對象名
String pythonObjName = "cal";
// python類名
String pythonClazzName = "Calculator";
PythonInterpreter pi2 = new PythonInterpreter();
// 加載python程序
pi2.execfile(pythonClass);
// 實例化python對象
pi2.exec(pythonObjName + "=" + pythonClazzName + "()");
// 獲取實例化的python對象
PyObject pyObj = pi2.get(pythonObjName);
// 調用python對象方法,傳遞參數並接收返回值
PyObject result = pyObj.invoke("power", new PyObject[] {Py.newInteger(2), Py.newInteger(3)});
double power = Py.py2double(result);
System.out.println(power);
pi2.cleanup();
pi2.close();
}
輸出:
8.0
8.0
calculator_func.py:
# coding=utf-8
import math
# 面向函數式編程
def power(x, y):
return math.pow(x, y)
calculator_clazz.py:
# coding=utf-8
import math
# 面向對象編程
class Calculator(object):
# 計算x的y次方
def power(self, x, y):
return math.pow(x,y)
(4)高級調用,也是在Java中調用Python程序最常見的用法:Python程序可以實現Java接口,在Python中也可以調用Java方法。
public static void main(String[] args) throws IOException {
System.setProperty("python.home", "D:\\jython2.7.0");
// Python程序路徑
String python = "D:\\python\\fruit_controller.py";
// Python實例對象名
String pyObjName = "pyController";
// Python類名
String pyClazzName = "FruitController";
Fruit apple = new Apple();
Fruit orange = new Orange();
PythonInterpreter interpreter = new PythonInterpreter();
// 如果在Python程序中引用了第三方庫,需要將這些被引用的第三方庫所在路徑添加到系統環境變量中
// 否則,在執行Python程序時將會報錯: ImportError: No module named xxx
PySystemState sys = interpreter.getSystemState();
sys.path.add("D:\\python");
// 加載Python程序
interpreter.execfile(python);
// 實例 Python對象
interpreter.exec(pyObjName + "=" + pyClazzName + "()");
// 1.在Java中獲取Python對象,並將Python對象轉換為Java對象
// 為什么能夠轉換? 因為Python類實現了Java接口,通過轉換后的Java對象只能調用接口中定義的方法
GroovyController controller = (GroovyController) interpreter.get(pyObjName).__tojava__(GroovyController.class);
controller.controllFruit(apple);
controller.controllFruit(orange);
// 2.在Java直接通過Python對象調用其方法
// 既可以調用實現的Java接口方法,也可以調用Python類自定義的方法
PyObject pyObject = interpreter.get(pyObjName);
pyObject.invoke("controllFruit", Py.java2py(apple));
pyObject.invoke("controllFruit", Py.java2py(orange));
pyObject.invoke("printFruit", Py.java2py(apple));
pyObject.invoke("printFruit", Py.java2py(orange));
// 3.在Java中獲取Python類進行實例化對象: 沒有事先創建 Python對象
PyObject pyClass = interpreter.get("FruitController");
PyObject pyObj = pyClass.__call__();
pyObj.invoke("controllFruit", Py.java2py(apple));
pyObj.invoke("controllFruit", Py.java2py(orange));
PyObject power = pyObj.invoke("power", new PyObject[] {Py.newInteger(2), Py.newInteger(3)});
if(power != null) {
double p = Py.py2double(power);
System.out.println(p);
}
interpreter.cleanup();
interpreter.close();
}
輸出:
Show: I am a java apple.
controllFruit Python Apple
controllFruit END
Show: I am a java orange.
controllFruit Python Orange
controllFruit END
Show: I am a java apple.
controllFruit Python Apple
controllFruit END
Show: I am a java orange.
controllFruit Python Orange
controllFruit END
Show: I am a java apple.
printFruit Python Apple
printFruit END
Show: I am a java orange.
printFruit Python Orange
printFruit END
Show: I am a java apple.
controllFruit Python Apple
controllFruit END
Show: I am a java orange.
controllFruit Python Orange
controllFruit END
8.0
fruit_controller.py:
# coding=utf-8
from calculator_clazz import Calculator
from java.lang import String
from org.test.inter import GroovyController
from org.test.inter import Fruit
# 在Python中實現Java接口: org.test.inter.GroovyController
class FruitController(GroovyController):
# 實現接口方法
def controllFruit(self, fruit):
# 在Python中調用Java對象方法
fruit.show()
if(fruit.getType() == "apple"):
print ("controllFruit Python Apple")
if(fruit.getType() == "orange"):
print ("controllFruit Python Orange")
print ("controllFruit END")
# 自定義新方法
def printFruit(self, fruit):
fruit.show()
if(fruit.getType() == "apple"):
print ("printFruit Python Apple")
if(fruit.getType() == "orange"):
print ("printFruit Python Orange")
print ("printFruit END")
# 引用第三方python程序
def power(self, x, y):
cal = Calculator()
return cal.power(x, y)
Java接口和實現類:
// 該接口用於在Python中實現
public interface GroovyController {
public void controllFruit(Fruit fruit);
}
// 在Java中使用的接口
public interface Fruit {
public String getName();
public String getType();
public void show();
}
// Apple
public class Apple implements Fruit {
public String getName() {
return "java apple";
}
public String getType() {
return "apple";
}
public void show() {
System.out.println("Show: I am a java apple.");
}
}
// Orange
public class Orange implements Fruit {
public String getName() {
return "ava orange";
}
public String getType() {
return "orange";
}
public void show() {
System.out.println("Show: I am a java orange.");
}
}
另外,對於在eclipse中運行時控制台報錯:
Failed to install '': java.nio.charset.UnsupportedCharsetException: cp0
請添加VM參數:-Dpython.console.encoding=UTF-8,詳見:http://blog.csdn.net/xfei365/article/details/50955731
總結
雖然在Java中調用Python可以有多種方式解決,甚至因為Jython的出現更顯得非常便利。但是這種程序間嵌套調用的方式不可取,首先拋開調用性能不說,增加了耦合復雜度。更加有效的方式應該是通過RCP或者RESTful接口進行解耦,這樣各司其職,也便於擴展,良好的架構是一個項目能夠健康發展的基礎。在微服務架構大行其道的今天,這種程序間嵌套調用的方式將會逐漸被淘汰。
【參考】
http://tonl.iteye.com/blog/1918245 Java調用Python
http://blog.csdn.net/supermig/article/details/24005585 Learning Python -- Java 通過JyThon調用Python實現的規則
http://blog.csdn.net/hpp1314520/article/details/72854011 java 利用Runtime.getRuntime().exec()調用python腳本並傳參
http://blog.csdn.net/xingjiarong/article/details/49424253 java調用python方法總結
https://zh.wikipedia.org/wiki/Jython Jython
http://lib.csdn.net/article/python/1654 Jython的安裝及簡單例子
https://coolshell.cn/articles/2631.html 五大基於JVM的腳本語言
http://python.jobbole.com/82703/ 各種 Python 實現的簡單介紹與比較
https://www.oschina.net/translate/why-are-there-so-many-pythons 為什么有這么多 Python?