• java调用python文件的几种方式【超细讲解!】


      java调用python的契机来自于一个项目需要用到算法,但是算法工程师们写的python,于是就有了java后端调用python脚本的需求,中间遇到了许多问题,特此记录整理了一次。

    1、java调用python的方式有哪几种

        1.1  方法一:jpython

              专门为java调用python2开发出来的类库,但由于不支持python3版本,python2和3之间的语法又不兼容导致jpython库并非特别通用。github有人问到过什么时候出python3版本的库,官方答复说是可行的但很困难(截止2022年8月份 jpython官方目前没有开发出支持python3的类库)

       jpython的语法特别简单,使用PythonIntercepter即可简单的操作python文件。

      1.1.1 导入jar包

    1. <dependency>
    2. <groupId>org.pythongroupId>
    3. <artifactId>jython-standaloneartifactId>
    4. <version>2.7.0version>
    5. dependency>

       1.1.2 调用python脚本中的method1()方法

    1. PythonInterpreter interpreter = new PythonInterpreter();
    2. interpreter.execfile("C:\\Users\\Dick\\Desktop\\demo.py");
    3. // 调用demo.py中的method1方法
    4. PyFunction func = interpreter.get("method1",PyFunction.class);
    5. Integer a = 10;
    6. Integer b = 10;
    7. PyObject pyobj = func.__call__(new PyInteger(a), new PyInteger(b));
    8. System.out.println("获得方法的返回值 = " + pyobj.toString());

      注:如无返回值 仅执行interpreter.execfile()方法即可

       1.2   方法二:ProcessBuilder

       ProcessBuilder是jdk提供的脚本执行工具类,无论是python文件还是shell脚本还是其他的指令,都可以通过此类来执行,我们来看看它是如何调用python脚本的

        1.2.1 首先我们把python文件放入resource下

        1.2.2 接下来就是执行脚本了 

    1. /**
    2. * 执行python脚本
    3. * @param fileName 脚本文件名称
    4. * @param params 脚本参数
    5. * @throws IOException
    6. */
    7. public static void execPythonFile(String fileName, String params) throws IOException {
    8. // 获取python文件所在目录地址
    9. String windowsPath = ClassUtils.getDefaultClassLoader().getResource("").getPath().substring(1) + "py/";
    10. // windows执行脚本需要使用 cmd.exe /c 才能正确执行脚本
    11. Process process = new ProcessBuilder("cmd.exe", "/c", "python", windowsPath + fileName, params).start();
    12. logger.info("读取python文件 开始 fileName={}", fileName);
    13. BufferedReader errorReader = null;
    14. // 脚本执行异常时的输出信息
    15. errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
    16. List errorString = read(fileName, errorReader);
    17. logger.info("读取python文件 异常 fileName={}&errorString={}", fileName, errorString);
    18. // 脚本执行正常时的输出信息
    19. BufferedReader inputReader = null;
    20. inputReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
    21. List returnString = read(fileName, inputReader);
    22. logger.info("读取python文件 fileName={}&returnString={}", fileName, returnString);
    23. try {
    24. logger.info("读取python文件 wait fileName={}", fileName);
    25. process.waitFor();
    26. } catch (InterruptedException e) {
    27. logger.error("读取python文件 fileName="+fileName+" 等待结果返回异常", e);
    28. }
    29. logger.info("读取python文件 fileName={} == 结束 ==", fileName);
    30. }
    31. private static List read(String fileName, BufferedReader reader) {
    32. List resultList = Lists.newArrayList();
    33. String res = "";
    34. while (true) {
    35. try {
    36. if (!((res = reader.readLine()) != null)) break;
    37. } catch (IOException e) {
    38. logger.error("读取python文件 fileName=" + fileName + " 读取结果异常", e);
    39. }
    40. resultList.add(res);
    41. }
    42. return resultList;
    43. }

        上述代码仅考虑了windows,而在Linux中情况会比较复杂一点。

       1.2.3 Linux中执行python存在的问题

          我们知道常规的项目部署是将项目打成jar包,然后直接放入Linux 或者通过docker等容器进行部署,这个时候resources下的py文件就在jar包里了,但我们执行python脚本时使用的是:

    python3 脚本文件所在地

        此时python脚本在jar包里面,不能通过 jar路径/BOOT-INF/classes/py/xxx.py进行访问【我测试过一段时间 发现python3 (python指令也不行) 指令无法调用在jar里面的脚本】,所以我能想到的方案是将python脚本文件直接放入服务器的某个文件夹中,方便后续访问。如果是docker部署,只需要在dockerfile中加入一个COPY指令  将py文件放到指定目录下:

     1.2.4 Linux中执行python文件

      下面代码将兼容windows和linux调用py文件【Linux执行py文件是使用python还是python3根据实际py环境变量配置来选择就好】

    1. /**
    2. * 执行python文件
    3. * @param fileName python文件地址
    4. * @param params 参数 其实可以改成传入多个参数 一个个放入ProcessBuilder中的
    5. * @throws IOException
    6. */
    7. public static void execPythonFile(String fileName, String params) throws IOException {
    8. // ① 当前系统类型
    9. String os = System.getProperty("os.name");
    10. // ② 获取python文件所在目录地址
    11. String windowsPath = ClassUtils.getDefaultClassLoader().getResource("").getPath().substring(1) + "py/";
    12. String linuxPath = "/ai/egcc/";
    13. logger.info("读取python文件 init fileName={}&path={}", fileName);
    14. Process process;
    15. if (os.startsWith("Windows")){
    16. // windows执行脚本需要使用 cmd.exe /c 才能正确执行脚本
    17. process = new ProcessBuilder("cmd.exe", "/c", "python", windowsPath + fileName, params).start();
    18. }else {
    19. // linux执行脚本一般是使用python3 + 文件所在路径
    20. process = new ProcessBuilder("python3", linuxPath + fileName, params).start();
    21. }
    22. logger.info("读取python文件 开始 fileName={}", fileName);
    23. BufferedReader errorReader = null;
    24. // 脚本执行异常时的输出信息
    25. errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
    26. List errorString = read(fileName, errorReader);
    27. logger.info("读取python文件 异常 fileName={}&errorString={}", fileName, errorString);
    28. // 脚本执行正常时的输出信息
    29. BufferedReader inputReader = null;
    30. inputReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
    31. List returnString = read(fileName, inputReader);
    32. logger.info("读取python文件 fileName={}&returnString={}", fileName, returnString);
    33. try {
    34. logger.info("读取python文件 wait fileName={}", fileName);
    35. process.waitFor();
    36. } catch (InterruptedException e) {
    37. logger.error("读取python文件 fileName="+fileName+" 等待结果返回异常", e);
    38. }
    39. logger.info("读取python文件 fileName={} == 结束 ==", fileName);
    40. }
    41. private static List read(String fileName, BufferedReader reader) {
    42. List resultList = Lists.newArrayList();
    43. String res = "";
    44. while (true) {
    45. try {
    46. if (!((res = reader.readLine()) != null)) break;
    47. } catch (IOException e) {
    48. logger.error("读取python文件 fileName=" + fileName + " 读取结果异常", e);
    49. }
    50. resultList.add(res);
    51. }
    52. return resultList;
    53. }

      以为这就完了吗,其实还没有呢,process.waitFor()方法其实存在一些问题,如果上线后可能会造成事故,具体参考:java调用exe程序  使用process.waitFor()死锁

      那我们就尝试用线程池来解决死锁的问题吧

     1.2.5 解决java调用脚本文件存在的隐式问题解决

    以下为终极版代码:

    1. private static ExecutorService taskPool = new ThreadPoolExecutor(8, 32
    2. ,200L,TimeUnit.MILLISECONDS, new LinkedBlockingQueue(600)
    3. ,new ThreadFactoryBuilder()
    4. .setNameFormat("thread-自定义线程名-runner-%d").build());
    5. /**
    6. * 执行python文件
    7. * @param fileName python文件地址
    8. * @param params 参数 多个直接逗号隔开
    9. * @throws IOException
    10. */
    11. public static void execPythonFile(String fileName, String params) throws IOException {
    12. // ① 当前系统类型
    13. String os = System.getProperty("os.name");
    14. // ② 获取python文件所在目录地址
    15. String windowsPath = ClassUtils.getDefaultClassLoader().getResource("").getPath().substring(1) + "py/";
    16. String linuxPath = "/ai/egcc/";
    17. logger.info("读取python文件 init fileName={}&path={}", fileName);
    18. Process process;
    19. if (os.startsWith("Windows")){
    20. // windows执行脚本需要使用 cmd.exe /c 才能正确执行脚本
    21. process = new ProcessBuilder("cmd.exe", "/c", "python", windowsPath + fileName, params).start();
    22. }else {
    23. // linux执行脚本一般是使用python3 + 文件所在路径
    24. process = new ProcessBuilder("python3", linuxPath + fileName, params).start();
    25. }
    26. taskPool.submit(() -> {
    27. logger.info("读取python文件 开始 fileName={}", fileName);
    28. BufferedReader errorReader = null;
    29. // 脚本执行异常时的输出信息
    30. errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
    31. List errorString = read(fileName, errorReader);
    32. logger.info("读取python文件 异常 fileName={}&errorString={}", fileName, errorString);
    33. });
    34. taskPool.submit(() -> {
    35. // 脚本执行正常时的输出信息
    36. BufferedReader inputReader = null;
    37. inputReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
    38. List returnString = read(fileName, inputReader);
    39. logger.info("读取python文件 fileName={}&returnString={}", fileName, returnString);
    40. });
    41. try {
    42. logger.info("读取python文件 wait fileName={}", fileName);
    43. process.waitFor();
    44. } catch (InterruptedException e) {
    45. logger.error("读取python文件 fileName="+fileName+" 等待结果返回异常", e);
    46. }
    47. logger.info("读取python文件 fileName={} == 结束 ==", fileName);
    48. }
    49. private static List read(String fileName, BufferedReader reader) {
    50. List resultList = Lists.newArrayList();
    51. String res = "";
    52. while (true) {
    53. try {
    54. if (!((res = reader.readLine()) != null)) break;
    55. } catch (IOException e) {
    56. logger.error("读取python文件 fileName=" + fileName + " 读取结果异常", e);
    57. }
    58. resultList.add(res);
    59. }
    60. return resultList;
    61. }

        好了 上述代码已经可以正确的调用python脚本了,但博主目前仍然有些问题还没解决:比如如何调用java的jar包内部的py文件?在windows上的jar包内的py文件是可以调用成功的【我在windows本地启动jar包做过测试】,但是docker容器里面的jar却无法调用成功的原因是什么?

        如果有朋友遇到问题欢迎在评论区留言和讨论

     1.2.6 终极版python执行工具类【建议使用】

    1. import com.google.common.collect.Lists;
    2. import com.google.common.util.concurrent.ThreadFactoryBuilder;
    3. import org.slf4j.Logger;
    4. import org.slf4j.LoggerFactory;
    5. import org.springframework.stereotype.Component;
    6. import org.springframework.util.ClassUtils;
    7. import java.io.BufferedReader;
    8. import java.io.IOException;
    9. import java.io.InputStreamReader;
    10. import java.util.List;
    11. import java.util.concurrent.ExecutorService;
    12. import java.util.concurrent.LinkedBlockingQueue;
    13. import java.util.concurrent.ThreadPoolExecutor;
    14. import java.util.concurrent.TimeUnit;
    15. /**
    16. * java调用python的执行器
    17. */
    18. @Component
    19. public class PythonExecutor {
    20. private static final Logger logger = LoggerFactory.getLogger(PythonExecutor.class);
    21. private static final String OS = System.getProperty("os.name");
    22. private static final String WINDOWS_PATH = ClassUtils.getDefaultClassLoader().getResource("").getPath().substring(1) + "py/automl/"; // windows为获取项目根路径即可
    23. private static final String LINUX_PATH = "/ai/xx";// linux为python文件所在目录
    24. private static ExecutorService taskPool = new ThreadPoolExecutor(8, 16
    25. , 200L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(600)
    26. , new ThreadFactoryBuilder()
    27. .setNameFormat("thread-自定义线程名-runner-%d").build());
    28. /**
    29. * 执行python文件【异步 无需等待py文件执行完毕】
    30. *
    31. * @param fileName python文件地址
    32. * @param params 参数
    33. * @throws IOException
    34. */
    35. public static void execPythonFile(String fileName, String params) {
    36. taskPool.submit(() -> {
    37. try {
    38. exec(fileName, params);
    39. } catch (IOException e) {
    40. logger.error("读取python文件 fileName=" + fileName + " 异常", e);
    41. }
    42. });
    43. }
    44. /**
    45. * 执行python文件 【同步 会等待py执行完毕】
    46. *
    47. * @param fileName python文件地址
    48. * @param params 参数
    49. * @throws IOException
    50. */
    51. public static void execPythonFileSync(String fileName, String params) {
    52. try {
    53. execSync(fileName, params);
    54. } catch (IOException e) {
    55. logger.error("读取python文件 fileName=" + fileName + " 异常", e);
    56. }
    57. }
    58. private static void exec(String fileName, String params) throws IOException {
    59. logger.info("读取python文件 init fileName={}&path={}", fileName, WINDOWS_PATH);
    60. Process process;
    61. if (OS.startsWith("Windows")) {
    62. // windows执行脚本需要使用 cmd.exe /c 才能正确执行脚本
    63. process = new ProcessBuilder("cmd.exe", "/c", "python", WINDOWS_PATH + fileName, params).start();
    64. } else {
    65. // linux执行脚本一般是使用python3 + 文件所在路径
    66. process = new ProcessBuilder("python3", LINUX_PATH + fileName, params).start();
    67. }
    68. new Thread(() -> {
    69. logger.info("读取python文件 开始 fileName={}", fileName);
    70. BufferedReader errorReader = null;
    71. // 脚本执行异常时的输出信息
    72. errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
    73. List errorString = read(fileName, errorReader);
    74. logger.info("读取python文件 异常 fileName={}&errorString={}", fileName, errorString);
    75. }).start();
    76. new Thread(() -> {
    77. // 脚本执行正常时的输出信息
    78. BufferedReader inputReader = null;
    79. inputReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
    80. List returnString = read(fileName, inputReader);
    81. logger.info("读取python文件 fileName={}&returnString={}", fileName, returnString);
    82. }).start();
    83. try {
    84. logger.info("读取python文件 wait fileName={}", fileName);
    85. process.waitFor();
    86. } catch (InterruptedException e) {
    87. logger.error("读取python文件 fileName=" + fileName + " 等待结果返回异常", e);
    88. }
    89. logger.info("读取python文件 fileName={} == 结束 ==", fileName);
    90. }
    91. private static void execSync(String fileName, String params) throws IOException {
    92. logger.info("同步读取python文件 init fileName={}&path={}", fileName, WINDOWS_PATH);
    93. Process process;
    94. if (OS.startsWith("Windows")) {
    95. // windows执行脚本需要使用 cmd.exe /c 才能正确执行脚本
    96. process = new ProcessBuilder("cmd.exe", "/c", "python", WINDOWS_PATH + fileName, params).start();
    97. } else {
    98. // linux执行脚本一般是使用python3 + 文件所在路径
    99. process = new ProcessBuilder("python3", LINUX_PATH + fileName, params).start();
    100. }
    101. taskPool.submit(() -> {
    102. logger.info("读取python文件 开始 fileName={}", fileName);
    103. BufferedReader errorReader = null;
    104. // 脚本执行异常时的输出信息
    105. errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
    106. List errorString = read(fileName, errorReader);
    107. logger.info("读取python文件 异常 fileName={}&errorString={}", fileName, errorString);
    108. });
    109. taskPool.submit(() -> {
    110. // 脚本执行正常时的输出信息
    111. BufferedReader inputReader = null;
    112. inputReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
    113. List returnString = read(fileName, inputReader);
    114. logger.info("读取python文件 fileName={}&returnString={}", fileName, returnString);
    115. });
    116. try {
    117. logger.info("同步读取python文件 wait fileName={}", fileName);
    118. process.waitFor();
    119. } catch (InterruptedException e) {
    120. logger.error("同步读取python文件 fileName=" + fileName + " 等待结果返回异常", e);
    121. }
    122. logger.info("同步读取python文件 fileName={} == 结束 ==", fileName);
    123. }
    124. private static List read(String fileName, BufferedReader reader) {
    125. List resultList = Lists.newArrayList();
    126. String res = "";
    127. while (true) {
    128. try {
    129. if (!((res = reader.readLine()) != null)) break;
    130. } catch (IOException e) {
    131. logger.error("读取python文件 fileName=" + fileName + " 读取结果异常", e);
    132. }
    133. resultList.add(res);
    134. }
    135. return resultList;
    136. }
    137. }

    ===== 补充 =====

      有小伙伴可能在别的博文上找到下面的java调用脚本方式

    Runtime.getRuntime().exec()

     其实上面的脚本底层用的也是ProcessBuilder对象,所以是一样的。

      

  • 相关阅读:
    serve error code=5011(RtcRtpMuxer)(Failed to mux RTP packet for RTC)
    PriorityQueue 源码解析(JDK1.8)
    Oauth2系列7:授权码和访问令牌的颁发流程是怎样的?
    2023秋冬系列丨追求本真的自然纯粹之美
    LeetCode 每日一题 2022/9/26-2022/10/2
    【第六章-函数】1.函数的灵活性
    [R] ggplot2 - exercise (“fill =“)
    微信小程序:页面配置/控制/跳转摘要
    常用数据库的最大并发和实际并发
    在IDEA中使用.env文件配置信息
  • 原文地址:https://blog.csdn.net/zjq852533445/article/details/126598270