在Java中调用Python

写在前面

在微服务架构大行其道的今天,对于将程序进行嵌套调用的做法其实并不可取,甚至显得有些愚蠢。当然,之所以要面对这个问题,或许是因为一些历史原因,或者仅仅是为了简单。恰好我在项目中就遇到了这个问题,需要在 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");
<span class="hljs-comment">// 1. Python面向函数式编程: 在Java中调用Python函数</span>
<span class="hljs-type">String</span> <span class="hljs-variable">pythonFunc</span> <span class="hljs-operator">=</span> <span class="hljs-string">"D:\\calculator_func.py"</span>;

<span class="hljs-type">PythonInterpreter</span> <span class="hljs-variable">pi1</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">PythonInterpreter</span>();
<span class="hljs-comment">// 加载python程序</span>
pi1.execfile(pythonFunc);
<span class="hljs-comment">// 调用Python程序中的函数</span>
<span class="hljs-type">PyFunction</span> <span class="hljs-variable">pyf</span> <span class="hljs-operator">=</span> pi1.get(<span class="hljs-string">"power"</span>, PyFunction.class);
<span class="hljs-type">PyObject</span> <span class="hljs-variable">dddRes</span> <span class="hljs-operator">=</span> pyf.__call__(Py.newInteger(<span class="hljs-number">2</span>), Py.newInteger(<span class="hljs-number">3</span>));
System.out.println(dddRes);
pi1.cleanup();
pi1.close();

<span class="hljs-comment">// 2. 面向对象式编程: 在Java中调用Python对象实例的方法</span>
<span class="hljs-type">String</span> <span class="hljs-variable">pythonClass</span> <span class="hljs-operator">=</span> <span class="hljs-string">"D:\\calculator_clazz.py"</span>;
<span class="hljs-comment">// python对象名</span>
<span class="hljs-type">String</span> <span class="hljs-variable">pythonObjName</span> <span class="hljs-operator">=</span> <span class="hljs-string">"cal"</span>;
<span class="hljs-comment">// python类名</span>
<span class="hljs-type">String</span> <span class="hljs-variable">pythonClazzName</span> <span class="hljs-operator">=</span> <span class="hljs-string">"Calculator"</span>;
<span class="hljs-type">PythonInterpreter</span> <span class="hljs-variable">pi2</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">PythonInterpreter</span>();
<span class="hljs-comment">// 加载python程序</span>
pi2.execfile(pythonClass);
<span class="hljs-comment">// 实例化python对象</span>
pi2.exec(pythonObjName + <span class="hljs-string">"="</span> + pythonClazzName + <span class="hljs-string">"()"</span>);
<span class="hljs-comment">// 获取实例化的python对象</span>
<span class="hljs-type">PyObject</span> <span class="hljs-variable">pyObj</span> <span class="hljs-operator">=</span> pi2.get(pythonObjName);
<span class="hljs-comment">// 调用python对象方法,传递参数并接收返回值</span>
<span class="hljs-type">PyObject</span> <span class="hljs-variable">result</span> <span class="hljs-operator">=</span> pyObj.invoke(<span class="hljs-string">"power"</span>, <span class="hljs-keyword">new</span> <span class="hljs-title class_">PyObject</span>[] {Py.newInteger(<span class="hljs-number">2</span>), Py.newInteger(<span class="hljs-number">3</span>)}); 
<span class="hljs-type">double</span> <span class="hljs-variable">power</span> <span class="hljs-operator">=</span> 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):

<span class="hljs-comment"># 计算x的y次方</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">power</span>(<span class="hljs-params">self, x, y</span>):
    <span class="hljs-keyword">return</span> math.<span class="hljs-built_in">pow</span>(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");
<span class="hljs-comment">// Python程序路径</span>
<span class="hljs-type">String</span> <span class="hljs-variable">python</span> <span class="hljs-operator">=</span> <span class="hljs-string">"D:\\python\\fruit_controller.py"</span>;
<span class="hljs-comment">// Python实例对象名</span>
<span class="hljs-type">String</span> <span class="hljs-variable">pyObjName</span> <span class="hljs-operator">=</span> <span class="hljs-string">"pyController"</span>;
<span class="hljs-comment">// Python类名</span>
<span class="hljs-type">String</span> <span class="hljs-variable">pyClazzName</span> <span class="hljs-operator">=</span> <span class="hljs-string">"FruitController"</span>;

<span class="hljs-type">Fruit</span> <span class="hljs-variable">apple</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Apple</span>();
<span class="hljs-type">Fruit</span> <span class="hljs-variable">orange</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Orange</span>();

<span class="hljs-type">PythonInterpreter</span> <span class="hljs-variable">interpreter</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">PythonInterpreter</span>();
<span class="hljs-comment">// 如果在Python程序中引用了第三方库,需要将这些被引用的第三方库所在路径添加到系统环境变量中</span>
<span class="hljs-comment">// 否则,在执行Python程序时将会报错: ImportError: No module named xxx</span>
<span class="hljs-type">PySystemState</span> <span class="hljs-variable">sys</span> <span class="hljs-operator">=</span> interpreter.getSystemState();
sys.path.add(<span class="hljs-string">"D:\\python"</span>);

<span class="hljs-comment">// 加载Python程序</span>
interpreter.execfile(python);
<span class="hljs-comment">// 实例 Python对象</span>
interpreter.exec(pyObjName + <span class="hljs-string">"="</span> + pyClazzName + <span class="hljs-string">"()"</span>);

<span class="hljs-comment">// 1.在Java中获取Python对象,并将Python对象转换为Java对象</span>
<span class="hljs-comment">// 为什么能够转换? 因为Python类实现了Java接口,通过转换后的Java对象只能调用接口中定义的方法</span>
<span class="hljs-type">GroovyController</span> <span class="hljs-variable">controller</span> <span class="hljs-operator">=</span> (GroovyController) interpreter.get(pyObjName).__tojava__(GroovyController.class);
controller.controllFruit(apple);
controller.controllFruit(orange);

<span class="hljs-comment">// 2.在Java直接通过Python对象调用其方法</span>
<span class="hljs-comment">// 既可以调用实现的Java接口方法,也可以调用Python类自定义的方法</span>
<span class="hljs-type">PyObject</span> <span class="hljs-variable">pyObject</span> <span class="hljs-operator">=</span>	interpreter.get(pyObjName);
pyObject.invoke(<span class="hljs-string">"controllFruit"</span>, Py.java2py(apple));
pyObject.invoke(<span class="hljs-string">"controllFruit"</span>, Py.java2py(orange));
pyObject.invoke(<span class="hljs-string">"printFruit"</span>, Py.java2py(apple));
pyObject.invoke(<span class="hljs-string">"printFruit"</span>, Py.java2py(orange));

<span class="hljs-comment">// 3.在Java中获取Python类进行实例化对象: 没有事先创建 Python对象</span>
<span class="hljs-type">PyObject</span> <span class="hljs-variable">pyClass</span> <span class="hljs-operator">=</span> interpreter.get(<span class="hljs-string">"FruitController"</span>);
<span class="hljs-type">PyObject</span> <span class="hljs-variable">pyObj</span> <span class="hljs-operator">=</span> pyClass.__call__();
pyObj.invoke(<span class="hljs-string">"controllFruit"</span>, Py.java2py(apple));
pyObj.invoke(<span class="hljs-string">"controllFruit"</span>, Py.java2py(orange));

<span class="hljs-type">PyObject</span> <span class="hljs-variable">power</span> <span class="hljs-operator">=</span> pyObj.invoke(<span class="hljs-string">"power"</span>, <span class="hljs-keyword">new</span> <span class="hljs-title class_">PyObject</span>[] {Py.newInteger(<span class="hljs-number">2</span>), Py.newInteger(<span class="hljs-number">3</span>)});
<span class="hljs-keyword">if</span>(power != <span class="hljs-literal">null</span>) {
    <span class="hljs-type">double</span> <span class="hljs-variable">p</span> <span class="hljs-operator">=</span> 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):

<span class="hljs-comment"># 实现接口方法</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">controllFruit</span>(<span class="hljs-params">self, fruit</span>):
    <span class="hljs-comment"># 在Python中调用Java对象方法</span>
    fruit.show()
    
    <span class="hljs-keyword">if</span>(fruit.getType() == <span class="hljs-string">"apple"</span>):
        <span class="hljs-built_in">print</span> (<span class="hljs-string">"controllFruit Python Apple"</span>)
        
    <span class="hljs-keyword">if</span>(fruit.getType() == <span class="hljs-string">"orange"</span>):
        <span class="hljs-built_in">print</span> (<span class="hljs-string">"controllFruit Python Orange"</span>)
    
    <span class="hljs-built_in">print</span> (<span class="hljs-string">"controllFruit END"</span>)

<span class="hljs-comment"># 自定义新方法    </span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">printFruit</span>(<span class="hljs-params">self, fruit</span>):
    fruit.show()
    
    <span class="hljs-keyword">if</span>(fruit.getType() == <span class="hljs-string">"apple"</span>):
        <span class="hljs-built_in">print</span> (<span class="hljs-string">"printFruit Python Apple"</span>)
        
    <span class="hljs-keyword">if</span>(fruit.getType() == <span class="hljs-string">"orange"</span>):
        <span class="hljs-built_in">print</span> (<span class="hljs-string">"printFruit Python Orange"</span>)
    
    <span class="hljs-built_in">print</span> (<span class="hljs-string">"printFruit END"</span>)

<span class="hljs-comment"># 引用第三方python程序</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">power</span>(<span class="hljs-params">self, x, y</span>):
    cal = Calculator()
    <span class="hljs-keyword">return</span> 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";
}

<span class="hljs-keyword">public</span> String <span class="hljs-title function_">getType</span><span class="hljs-params">()</span> {
	<span class="hljs-keyword">return</span> <span class="hljs-string">"apple"</span>;
}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">show</span><span class="hljs-params">()</span> {
	System.out.println(<span class="hljs-string">"Show: I am a java apple."</span>);
}

}

// Orange
public class Orange implements Fruit {
public String getName() {
return "ava orange";
}

<span class="hljs-keyword">public</span> String <span class="hljs-title function_">getType</span><span class="hljs-params">()</span> {
	<span class="hljs-keyword">return</span> <span class="hljs-string">"orange"</span>;
}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">show</span><span class="hljs-params">()</span> {
	System.out.println(<span class="hljs-string">"Show: I am a java orange."</span>);
}

}

另外,对于在 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 的出现更显得非常便利。但是这种程序间嵌套调用的方式不可取,首先抛开调用性能不说,增加了耦合复杂度。更加有效的方式应该是通过 RPC 或者 RESTful 接口进行解耦,这样各司其职,也便于扩展,良好的架构是一个项目能够健康发展的基础。在微服务架构大行其道的今天,这种程序间嵌套调用的方式将会逐渐被淘汰。
关于 Python 解释器的说明还可以参考廖雪峰老师的博客:https://www.liaoxuefeng.com/wiki/1016959663602400/1016966024263840

【参考】
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?