前段时间遇到了在JavaWeb项目中嵌入运行Python脚本的功能的需求。想到的方案有两种,一种是使用Java技术(Jython或Runtime.exec)运行Python脚本,另一种是搭建一个Python工程对外提供相应http或webservice接口。两种方案我都有实现,简单的测试了一下,本机环境两者的执行效率没有太大的差距。考虑到项目的情况,最终选择了第一种方案。
闲话少说,我们赶紧看看Java怎么实现运行Python脚本的吧~
简单来说,我们常说的Python是指CPython,由C语言编写。而这里说的Jython是由Java编写,在JVM中运行。显然,Jython天然就对Java有很高的亲和度(本身就是Java写的),能够利用JVM相关的技术,库和函数等资源。
Jython从2.0版本开始就和Python的版本保持一致,目前最新的版本为2.7.2。Jython的官方下载网址:https://www.jython.org/download。网址中有两个下载包Jython Installer、Jython Standalone。Jython Installer类似一个应用程序(exe),双击安装即可在cmd中使用Jython;而Jython Standalone可以作为jar包直接引入到项目中使用。对于项目编程,直接引入Maven依赖即可。
<dependency>
<groupId>org.pythongroupId>
<artifactId>jython-standaloneartifactId>
<version>2.7.1version>
dependency>
注意:引用的不是org.python.jython,这将会产生一堆疑难杂症。引入的是jython-standalone,这是专门给java调用jython的包,有较多的jar库支持。
Jython主要是使用PythonInterpreter类,调用方法大致有两种形式。
根据需要调用的方法名获取对应的PyFunction对象,调用__call__方法获取方法的返回值。
在这种调用方式下,python脚本里面是定义了一些函数,且用Jython调用的时候,需要知道所要调用函数的名称。比如python脚本为:
# coding=utf-8
def main(str):
print(str)
那么java的关键代码如下:
PythonInterpreter interpreter = new PythonInterpreter();
interpreter.exec(script);
// python脚本中的方法名为main
PyFunction function = interpreter.get("main",PyFunction.class);
// 将参数代入并执行python脚本
PyObject o = function.__call__(Py.newString(params));
另外一种调用方式是,Python脚本是定义了一个类。可以通过类名获取对应的python对象,再用该对象调用类中定义的方法获取返回值。比如python脚本为:
# coding=utf-8
class Test(object):
def main(self,str):
print(str)
那么java的关键代码如下:
PythonInterpreter interpreter = new PythonInterpreter();
interpreter.exec(script);
// 获取Python对象
PyObject pyObject = interpreter.get("Test");
// 调用方法,方法名为main。将参数代入并执行python脚本,获取返回值
// 传入参数对象的个数要与调用的方法一致
PyObject o= pyObj.invoke("main",new PyObject[]{Py.newString(params)}));
Python作为最流行的开源编程语言之一,拥有广泛的第三方包,应该说当下的Python编程已无法离开这丰富的第三方包的支持。Jython本身也包含较多的jar包支持日常的编程,同时也支持引入第三方包。引入的方式如下两种,第一种是引入Jython本身提供的jar包:以引入site为例:
Properties props = new Properties();
props.put("python.import.site", "false");
Properties preprops = System.getProperties();
PythonInterpreter.initialize(preprops, props, new String[0]);
PythonInterpreter interpreter = new PythonInterpreter();
第二种可以直接指定引入的第三方包所在的目录:
PythonInterpreter interpreter = new PythonInterpreter();
PySystemState sys = interpreter.getSystemState();
sys.path.add("C:/jython2.7.1/Lib");
第二种方法有另一种写法:
PythonInterpreter interpreter = new PythonInterpreter();
// 下面是加入jython的库,需要指定jar包所在的路径。如果有自己写的包或者第三方,可以继续追加
interpreter.exec("import sys");
interpreter.exec("sys.path.append('C:/jython2.7.1/Lib')");
interpreter.exec("sys.path.append('C:/jython2.7.1/Lib/site-packages')");
优点:本身就是Java编写的,使用起来比较方便,且相比后面的Runtime.exec的路子相对比较简单。
缺点:现在最流行的还是Python(即CPython),不管是社区活跃程度还是更新迭代速度都是Jython望尘莫及的,这也意味着Python许多强大的第三方包是无法即时更新到Jython库中,即便Jython提供引入第三方包的功能,但是总会有莫名其妙的问题,总是莫名的提示导包出错。编程的时候有不少时间浪费在处理导包问题,很影响使用的心情。
而最致命的问题是,Jython竟然不支持中文!!!只要Python脚本中包含中文,就会一直报错。我在网上找了很多资料,也尝试去在代码中转码,还是无法解决,也没有找到解决方法。由于我在使用的时候还是使用2.7.1版本,不清楚2.7.2版本有没有解决这个问题。
如果有朋友解决了这个问题,欢迎分享一下解决方式,谢谢~~
总之在项目中,我是彻底放弃使用Jython了。
Jdk提供Runtime类用于执行JVM外的程序,其效果类似于调用命令行执行指令。在安装了Python的前提下,我们可以模拟命令行窗口执行Python的脚本。如该Runtime类除了可以执行Python代码,也可执行Java、shell代码等,具有非常强大的功能。但需要注意的是,Runtime类并不等同于命令行。
在命令行中,我们可以逐行执行脚本代码:
然而使用Runtime的时候,我发现并不能每行代码像命令行那样逐行用exec执行,猜测是每次exec后返回的结果之间没有联系。
如果要使用Runtime.exec执行Python脚本,就要模拟命令行的命令:python py文件的路径 参数...
该命令的第一个参数为字符串“python”,指定命令的执行程序为python。这个需要在系统中安装python程序。有些系统可能同时安装了python2和python3,这个需要先在命令行中执行一次python看调用的是哪个。一般默认
python
调用python2,python3
调用python3。
第二个参数是需要执行的Python脚本文件的绝对路径。也就是说Python脚本文件在执行的过程中需要存放在本地。
第三个参数是参数列表,如果脚本中需要传入参数,那么这里需要根据形参的顺序依次传入对应的实参。
比方说,如果需要执行的python脚本名字为Hello.py,存放在E盘下,那么其绝对路径为E:/Hello.py,该脚本需要传入一个字符串参数,假如为“world”,那么在cmd执行的命令为:python E:/Hello.py "world"
在Java中可以用一个字符串数组存储上面的命令:
String command = "E:/Hello.py";
String params = "world";
String[] cmd = new String[]{"python",command,params};
Process process = Runtime.getRuntime().exec(cmd);
这里有个坑,当params是一个包含空格的字符串时,脚本执行结果会有问题。
比如String params=“this is a brand new day”。那么实际模拟cmd执行的命令是这样的:python E:/Hello.py this is a brand new day
。
看出问题了吗?我们理想认为参数是一个(this is a brand new day),但实际上却变成了6个,第一个是this,第二个是is,依次类推。由于脚本只需要一个参数,所以只会把第一个参数(this)传入到脚本中执行!
解决方法很简单:只要在params字符串前后加上双括号即可!String params="\"this is a brand new day\""
在获取到Process对象后,通过getInputStream()获取子进程标准输入流、getErrorStream()获取子进程错误流,从而得到脚本的执行结果、错误反馈信息。并使用waitFor()使当前线程等待至子进程结束。但如果在同一个线程中,主进程和子进程有可能会出现相互等待对方结束而出现死锁的情况。这时候需要新开一个线程将获取标准输入流或错误流分开。完整的关键代码如下:
/* 注意:cmd的格式:“python py文件的路径 参数...”
* 注意2:参数是字符串的时候,有可能会出现参数只解析第一个词的情况,此时必须在整个字符串参数首尾手动添加双引号(单引号都不行)
* 则下面的cmd=python E:/test/pythontest/Demo.py “params”
*/
//String cmd = String.format("python %s \"%s\"",command,params);
// 也可以用String[],但是params传入前也需要手动在字符串前后加双引号
params = "\"" + params + "\"";
String[] cmd = new String[]{"python",command,params};
Process process = Runtime.getRuntime().exec(cmd);
// error的要单独开一个线程处理。其实最好分成两个子线程分别处理标准输出流和错误输出流
ProcessStream stderr = new ProcessStream(process.getErrorStream(), "ERROR", charset);
stderr.start();
// 获取标准输出流的内容
BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream(), charset));
while ((line = stdout.readLine()) != null) {
rtnSb.append(line).append("\r\n");
}
rtnMap.put("result",rtnSb.toString());
rtnMap.put("error",stderr.getContent());
//关闭流
stdout.close();
int status = process.waitFor();
if (status != 0) {
System.out.println("return value:"+status);
}
process.destroy();
到此,两种方法均已介绍完毕。附上完整运行工具类和相应的进程工具类:
import org.python.core.Py;
import org.python.core.PyFunction;
import org.python.core.PyObject;
import org.python.core.PySystemState;
import org.python.util.PythonInterpreter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* java执行python的工具类
* 使用条件,python代码中有一个main方法,其他代码都在main方法中执行
* @author hjw
*/
public class RunPython {
private static Logger logger = LoggerFactory.getLogger(RunPython.class);
/**
* 使用jython运行py代码,缺点:一旦引用第三方库容易报错,而即便手动设置第三方库,也有可能出现错误
* @param script python解析代码
* @param params python代码中的参数
* @return
*/
public static Map<String,Object> runPythonByJython(String script, String params){
Map<String,Object> rtnMap = new HashMap<>();
Properties props = new Properties();
props.put("python.import.site", "false");
Properties preprops = System.getProperties();
PythonInterpreter.initialize(preprops, props, new String[0]);
PythonInterpreter interpreter = new PythonInterpreter();
// // 下面是加入jython的库,需要指定jar包所在的路径。如果有自己写的包或者第三方,可以继续追加
// interpreter.exec("import sys");
// interpreter.exec("sys.path.append('C:/jython2.7.1/Lib')");
// interpreter.exec("sys.path.append('C:/jython2.7.1/Lib/site-packages')");
try {
interpreter.exec(script);
// 假设python有一个main方法,包含所有实现需求的代码。换言之,传来的python代码只需要执行main方法就能完成需求
PyFunction function = interpreter.get("main",PyFunction.class);
// 将报文代入并执行python进行解析
PyObject o = function.__call__(Py.newString(params));
rtnMap.put("result",o);
interpreter.cleanup();
interpreter.close();
} catch (Exception e) {
e.printStackTrace();
rtnMap.put("error",e);
}
return rtnMap;
}
/**
* 使用Runtime.getRuntime().exec()解析运行python
* @param command 解析的python代码,即py文件的路径
* @param params python代码中的参数
* @param charset 码表
* @return
*/
public static Map<String,Object> runPythonByRuntime(String command, String params, String charset) {
Map<String,Object> rtnMap = new HashMap<>();
String line;
StringBuffer rtnSb = new StringBuffer();
try {
/* 注意:cmd的格式:“python py文件的路径 参数...”
* 注意2:参数是字符串的时候,必须在首尾手动添加双引号(单引号都不行)
* 则下面的cmd=python E:/test/pythontest/Demo.py “params” */
// String cmd = String.format("python %s \"%s\"",command,params);
// 也可以用String[],但是params传入前也需要手动在字符串前后加双引号
String[] cmd = new String[]{"python",command,params};
Process process = Runtime.getRuntime().exec(cmd);
// error的要单独开一个线程处理。其实最好分成两个子线程处理标准输出流和错误输出流
ProcessStream stderr = new ProcessStream(process.getErrorStream(), "ERROR", charset);
stderr.start();
// 获取标准输出流的内容
BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream(), charset));
while ((line = stdout.readLine()) != null) {
rtnSb.append(line).append("\r\n");
}
rtnMap.put("result",rtnSb.toString());
rtnMap.put("error",stderr.getContent());
//关闭流
stdout.close();
int status = process.waitFor();
if (status != 0) {
System.out.println("return value:"+status);
logger.info("event:{}", "RunExeForWindows",process.exitValue());
}
process.destroy();
} catch (Exception e) {
e.printStackTrace();
}
return rtnMap;
}
}
线程工具类:
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
/**
* java执行python的线程工具类
*/
public class ProcessStream extends Thread {
private static Logger logger = LoggerFactory.getLogger(ProcessStream.class);
private InputStream inputStream;
private String streamType;
private StringBuffer buf;
private String charset;
private volatile boolean isStopped = false; // 用于判断本线程是否执行完毕,用volatile保证线程安全
public ProcessStream(InputStream inputStream, String streamType, String charset) {
this.inputStream = inputStream;
this.streamType = streamType;
this.buf = new StringBuffer();
this.charset = charset;
this.isStopped = false;
}
@Override
public void run() {
try {
// 默认编码为UTF-8,如果有传入编码,则按外部编码
String exactCharset = StringUtils.isBlank(charset) ? "UTF-8" : charset;
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, exactCharset));
String line = null;
while ((line = bufferedReader.readLine()) != null) {
this.buf.append(line + "\n");
}
bufferedReader.close();
} catch (IOException e) {
logger.error("Failed to successfully consume and display the input stream of type " + streamType + ".", e);
} finally {
this.isStopped = true;
synchronized (this) {
notify();
}
}
}
/**
* 当主线程调用本方法获取本子线程的输出时,若本子线程还没执行完毕,主线程阻塞到子线程完成后再继续执行
*/
public String getContent() {
if (!this.isStopped) {
synchronized (this) {
try {
wait();
} catch (InterruptedException ignore) {
ignore.printStackTrace();
}
}
}
return this.buf.toString();
}
}