杰瑞科技汇

Linux中Java如何调用Python脚本?

我将为你详细介绍几种主流的实现方法,从最简单到最健壮,并附上优缺点分析和代码示例。

Linux中Java如何调用Python脚本?-图1
(图片来源网络,侵删)

方法概览

方法 优点 缺点 适用场景
通过命令行调用 简单直接,无需额外依赖,适合快速实现 性能差(频繁创建进程),数据传递复杂,错误处理麻烦 一次性任务、简单脚本调用、对性能要求不高
使用 JNI (Java Native Interface) 性能高,可以深度集成,共享内存 极其复杂,需要编写 C/C++ 中间层,开发成本高,可移植性差 对性能有极致要求的特定功能模块,调用成熟的 C/C++ 库
使用 IPC 机制 (如 Socket, gRPC) 解耦性好,服务化设计,可扩展性强 架构复杂,需要自行处理网络通信、序列化/反序列化 构建微服务,Python 作为独立服务运行,Java 作为客户端
使用第三方库 (如 Jython, JPype, GraalVM) 代码集成度高,调用方便,性能较好 库可能不活跃,兼容性问题(如 Jython 不支持 Python 3),GraalVM 配置复杂 需要在 JVM 内部直接调用 Python,追求代码简洁和性能的平衡

通过命令行调用 (Runtime.exec / ProcessBuilder)

这是最基础、最通用的方法,Java 启动一个子进程来执行 Python 解释器,并传入 Python 脚本的路径和参数。

实现步骤

  1. 编写 Python 脚本my_script.py,它可以接收命令行参数,并将结果打印到标准输出。
  2. 编写 Java 代码:使用 Runtime.getRuntime().exec() 或更推荐的 ProcessBuilder 来执行命令。
  3. 处理输入/输出:Java 需要从进程的输入流 (InputStream) 读取 Python 脚本的输出,并向进程的输出流 (OutputStream) 写入数据(如果需要)。
  4. 等待进程结束:调用 process.waitFor() 确保进程执行完毕。

代码示例

Python 脚本 (add_numbers.py)

# import sys
# 检查参数数量
# if len(sys.argv) != 3:
#     print("Usage: python add_numbers.py <num1> <num2>")
#     sys.exit(1)
try:
    # 从命令行参数获取数字
    num1 = float(sys.argv[1])
    num2 = float(sys.argv[2])
    result = num1 + num2
    # 将结果打印到标准输出,Java会读取这个输出
    print(result)
except (IndexError, ValueError) as e:
    # 错误信息也打印到标准错误流
    print(f"Error: Invalid arguments. {e}", file=sys.stderr)
    sys.exit(1)

Java 调用代码 (CommandLineCaller.java)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class CommandLineCaller {
    public static void main(String[] args) {
        // Python 脚本的路径和参数
        String[] command = {
            "python3",  // 确保 python3 在系统 PATH 中,或使用绝对路径如 /usr/bin/python3
            "/path/to/your/add_numbers.py", // 替换为你的脚本绝对路径
            "10",
            "20"
        };
        Process process = null;
        try {
            // 使用 ProcessBuilder 更灵活,可以设置工作目录等
            ProcessBuilder pb = new ProcessBuilder(command);
            process = pb.start();
            // --- 读取 Python 脚本的输出 ---
            // 使用 BufferedReader 逐行读取,避免缓冲区溢出
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            StringBuilder output = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                output.append(line);
            }
            // --- 读取 Python 脚本的错误输出 ---
            BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            StringBuilder errorOutput = new StringBuilder();
            String errorLine;
            while ((errorLine = errorReader.readLine()) != null) {
                errorOutput.append(errorLine);
            }
            // 等待 Python 脚本执行完毕
            int exitCode = process.waitFor();
            if (exitCode == 0) {
                System.out.println("Python script executed successfully.");
                System.out.println("Result: " + output.toString());
            } else {
                System.err.println("Python script execution failed with exit code: " + exitCode);
                System.err.println("Error output: " + errorOutput.toString());
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (process != null) {
                process.destroy(); // 销毁进程,释放资源
            }
        }
    }
}

优缺点分析

  • 优点
    • 无需任何额外库,所有 Linux 系统都自带。
    • 概念简单,易于上手。
    • 可以调用任何 Python 脚本,不受版本限制(只要系统有对应解释器)。
  • 缺点
    • 性能开销大:每次调用都需要创建一个新的 Python 进程,启动成本高,不适合高频调用。
    • 数据传递复杂:数据需要通过命令行参数(字符串)或标准输入/输出流进行传递,对于复杂数据结构(如对象、列表)需要手动序列化和反序列化(如 JSON),非常麻烦。
    • 错误处理困难:需要手动检查进程的退出码和错误流,实现健壮的错误处理逻辑比较繁琐。

使用第三方库 (JPype - 推荐)

JPype 是一个专门为设计的库,它可以在 JVM 启动时启动一个 Python 解释器,让 Java 代码能够像调用本地 Java 类一样直接调用 Python 模块和函数,性能远高于命令行调用。

Linux中Java如何调用Python脚本?-图2
(图片来源网络,侵删)

实现步骤

  1. 安装 JPype:下载 JPype 的 .jar 包,并将其添加到 Java 项目的类路径中。
  2. 启动 Python 解释器:在 Java 代码中,使用 JPackage.start() 启动 Python 环境。
  3. 导入 Python 模块:使用 JPackage.importModule() 导入 Python 模块。
  4. 调用 Python 函数:通过返回的 JObject 调用 Python 函数,并传递 Java 对象(JPype 会自动转换)。
  5. 关闭 Python 解释器:调用 JPackage.shutdown() 释放资源。

代码示例

Python 脚本 (calculator.py)

def add(a, b):
    """Adds two numbers."""
    print(f"Python received: {a} and {b}")
    return a + b
def greet(name):
    """Greets a person."""
    return f"Hello, {name} from Python!"

Java 调用代码 (JPypeCaller.java)

import jpype.JArray;
import jpype.JClass;
import jpype.JInt;
import jpype.JObject;
import jpype.JPackage;
import jpype.JString;
import jpype.JException;
import jpype.PyObject;
import jpype.startup.JarMain;
public class JPypeCaller {
    static {
        // 设置 JPype 的库路径,指向 libjvm.so 的位置
        // 通常在 /usr/lib/jvm/java-11-openjdk-amd64/lib/server/ (路径根据你的安装而变化)
        // System.setProperty("java.library.path", "/path/to/your/jvm/lib");
    }
    public static void main(String[] args) {
        try {
            // 1. 启动 JPype
            // JPackage.start("python3"); // 指定 python3 可执行文件
            JPackage.start(); // JPype 能在 PATH 中找到 python
            // 2. 导入 Python 模块
            // JPackage.importModule 返回一个 JObject,代表 Python 模块
            JObject calculatorModule = JPackage.importModule("calculator");
            // 3. 调用 Python 函数
            // add 函数接受两个参数
            PyObject result1 = (PyObject) calculatorModule.callAttr("add", 15, 25);
            System.out.println("Java received from Python: " + result1.toString());
            // greet 函数接受一个字符串
            PyObject result2 = (PyObject) calculatorModule.callAttr("greet", new JString("Java World"));
            System.out.println("Java received from Python: " + result2.toString());
        } catch (JException e) {
            System.err.println("JPype error occurred: " + e.getMessage());
            e.printStackTrace();
        } finally {
            // 4. 关闭 Python 解释器
            JPackage.shutdown();
        }
    }
}

如何编译和运行

  1. 下载 JPype 的 JAR 包(JPype-1.4.2.jar)。
  2. 编译并运行,将 JAR 包加入类路径:
# 编译
javac -cp ".:/path/to/JPype-1.4.2.jar" JPypeCaller.java
# 运行
java -cp ".:/path/to/JPype-1.4.2.jar" JPypeCaller

优缺点分析

  • 优点
    • 性能好:在同一个进程中运行,避免了进程创建和销毁的开销。
    • 调用方便:代码风格接近原生调用,数据类型可以自动转换。
    • 集成度高:Java 和 Python 可以深度交互,传递复杂数据结构。
  • 缺点
    • 需要额外引入 JPype 库。
    • 配置稍显复杂,需要正确设置 java.library.path 或使用 JPackage.start("python3")
    • JPype 的活跃度不如一些其他项目,但社区仍在维护。

使用 JNI (Java Native Interface)

这是一种非常底层但性能最高的方法,Java 通过 JNI 调用 C/C++ 代码,然后由 C/C++ 代码通过 Python 的 C API (Python.h) 来调用 Python。

Linux中Java如何调用Python脚本?-图3
(图片来源网络,侵删)

实现步骤

  1. 编写 C/C++ 中间层:创建一个动态链接库(.so 文件),其中包含 JNI 函数和 Python C API 调用。
  2. 编写 Java 代码:使用 native 关键字声明需要由 C/C++ 实现的方法,并加载动态库。
  3. 编译:使用 gcc 将 C/C++ 代码编译成 .so 文件,确保链接了 Python 的开发库。

代码示例 (概念性)

Java 代码 (NativeCaller.java)

public class NativeCaller {
    // 声明一个 native 方法
    public native String callPython(String scriptPath, String functionName, String arg);
    static {
        // 加载编译好的 .so 文件
        System.loadLibrary("native_caller");
    }
    public static void main(String[] args) {
        NativeCaller caller = new NativeCaller();
        String result = caller.callPython("/path/to/script.py", "add", "10,20");
        System.out.println("Result from C/Python: " + result);
    }
}

C/C++ 代码 (native_caller.c)

#include <jni.h>
#include <Python.h>
#include <stdio.h>
// JNI 方法实现
JNIEXPORT jstring JNICALL Java_NativeCaller_callPython(JNIEnv *env, jobject obj, jstring scriptPath, jstring functionName, jstring arg) {
    const char *py_script_path = (*env)->GetStringUTFChars(env, scriptPath, 0);
    const char *py_func_name = (*env)->GetStringUTFChars(env, functionName, 0);
    const char *py_arg = (*env)->GetStringUTFChars(env, arg, 0);
    // 1. 初始化 Python 解释器
    Py_Initialize();
    if (!Py_IsInitialized()) {
        // 错误处理
        return (*env)->NewStringUTF(env, "Failed to initialize Python");
    }
    // 2. 添加脚本路径到 Python 路径
    PyRun_SimpleString("import sys");
    char path_cmd[256];
    sprintf(path_cmd, "sys.path.append('%s')", py_script_path);
    PyRun_SimpleString(path_cmd);
    // 3. 导入模块
    PyObject *pName = PyUnicode_DecodeFSDefault("my_module"); // 假设模块名为 my_module.py
    PyObject *pModule = PyImport_Import(pName);
    Py_DECREF(pName);
    if (pModule != NULL) {
        // 4. 获取函数
        PyObject *pFunc = PyObject_GetAttrString(pModule, py_func_name);
        if (pFunc && PyCallable_Check(pFunc)) {
            // 5. 准备参数
            PyObject *pArgs = Py_BuildValue("(s)", py_arg);
            PyObject *pValue = PyObject_CallObject(pFunc, pArgs);
            if (pValue != NULL) {
                const char *result = PyUnicode_AsUTF8(pValue);
                // 将结果返回给 Java
                jstring jResult = (*env)->NewStringUTF(env, result);
                Py_DECREF(pValue);
                Py_DECREF(pArgs);
                Py_DECREF(pFunc);
                Py_DECREF(pModule);
                Py_Finalize();
                (*env)->ReleaseStringUTFChars(env, scriptPath, py_script_path);
                (*env)->ReleaseStringUTFChars(env, functionName, py_func_name);
                (*env)->ReleaseStringUTFChars(env, arg, py_arg);
                return jResult;
            } else {
                Py_DECREF(pArgs);
                Py_DECREF(pFunc);
                Py_DECREF(pModule);
            }
        } else {
            Py_DECREF(pModule);
        }
    }
    Py_Finalize();
    (*env)->ReleaseStringUTFChars(env, scriptPath, py_script_path);
    (*env)->ReleaseStringUTFChars(env, functionName, py_func_name);
    (*env)->ReleaseStringUTFChars(env, arg, py_arg);
    return (*env)->NewStringUTF(env, "Error calling Python function");
}

优缺点分析

  • 优点
    • 性能最高:直接通过 C API 调用,几乎没有额外开销。
    • 功能强大:可以访问 Python 的所有底层功能。
  • 缺点
    • 开发极其复杂:需要同时掌握 Java JNI、C/C++ 和 Python C API,调试困难。
    • 可移植性差:为 Linux 编译的 .so 文件不能在 Windows 上使用。
    • 维护成本高:代码难以维护,不适合快速迭代。

使用 IPC 机制 (如 Socket / gRPC)

这种方法将 Python 代码封装成一个独立的服务(例如一个 HTTP 服务器或 gRPC 服务),Java 作为客户端通过网络请求来调用 Python 的功能。

实现步骤

  1. 编写 Python 服务:使用 Flask/Django/FastAPI 等 Web 框架,或者 gRPC 框架,暴露一个 API 端点。
  2. 编写 Java 客户端:使用 OkHttp, Apache HttpClient 或 gRPC Java 客户端库向 Python 服务发送请求。
  3. 数据交换:通常使用 JSON 或 Protocol Buffers 作为数据交换格式。

代码示例 (使用 Flask + OkHttp)

Python 服务 (server.py)

from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/add', methods=['POST'])
def add_numbers():
    data = request.get_json()
    if not data or 'a' not in data or 'b' not in data:
        return jsonify({'error': 'Missing parameters'}), 400
    try:
        a = float(data['a'])
        b = float(data['b'])
        result = a + b
        return jsonify({'result': result})
    except (ValueError, TypeError):
        return jsonify({'error': 'Invalid number format'}), 400
if __name__ == '__main__':
    # 在生产环境中应使用 Gunicorn/uWSGI 等应用服务器
    app.run(host='0.0.0.0', port=5000)

Java 客户端 (IPCClient.java)

import okhttp3.*;
import org.json.JSONObject;
import java.io.IOException;
public class IPCClient {
    private static final OkHttpClient client = new OkHttpClient();
    private static final String API_URL = "http://localhost:5000/add";
    public static void main(String[] args) {
        // 准备请求数据
        JSONObject requestData = new JSONObject();
        requestData.put("a", 100);
        requestData.put("b", 200);
        RequestBody body = RequestBody.create(
            requestData.toString(),
            MediaType.parse("application/json; charset=utf-8")
        );
        Request request = new Request.Builder()
                .url(API_URL)
                .post(body)
                .build();
        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                throw new IOException("Unexpected code " + response);
            }
            // 读取响应体
            String responseBody = response.body().string();
            JSONObject responseJson = new JSONObject(responseBody);
            System.out.println("Java received from Python server: " + responseJson.get("result"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

优缺点分析

  • 优点
    • 解耦性好:Java 和 Python 完全独立,可以单独部署、升级和扩展。
    • 技术栈灵活:服务端和客户端可以使用最适合的技术。
    • 可扩展性强:可以轻松地将单机服务扩展为分布式服务。
    • 健壮性高:可以通过 HTTP 状态码、重试机制等实现完善的错误处理。
  • 缺点
    • 架构复杂:需要额外维护一个服务,涉及网络通信。
    • 性能开销:网络 I/O 会带来延迟,不适合需要毫秒级响应的场景。
    • 需要额外框架:需要引入 Web 框架和 HTTP 客户端库。

总结与如何选择

方法 集成度 性能 开发难度 适用场景
命令行调用 简单 快速原型、一次性任务、调用简单脚本
JPype 中等 首选推荐,需要深度集成、高性能、且在同一个 JVM 内运行的场景
JNI 最高 最高 非常高 对性能有极致要求,且愿意投入大量开发成本的特殊模块
IPC (Socket/gRPC) 中等 中等 构建微服务、系统解耦、需要独立部署和扩展的场景

给你的建议:

  • 如果你只是想偶尔调用一个简单的 Python 脚本,并且不关心性能,使用 命令行调用 就足够了。
  • 如果你想在 Java 应用中频繁地、高性能地调用 Python 功能,并且希望代码集成度高,JPype 是目前最平衡、最实用的选择。
  • 如果你的系统是微服务架构,Python 和 Java 本来就是作为独立服务存在的,那么使用 IPC (如 gRPC) 是最合理的设计。
  • 除非你是一个性能专家,并且有非常特殊的需求,否则 尽量避免使用 JNI,因为它带来的开发痛苦远大于性能收益。
分享:
扫描分享到社交APP
上一篇
下一篇