java通过jep(Java Embedded Python)实现高效重复调用python脚本

java通过jep(Java Embedded Python)实现高效重复调用python脚本

书接上回,之前我们通过命令行的方式调用python脚本并实现了传递大量数据的方案.但是新的问题再次出现:在需要频繁重复调用python脚本的应用环境下,命令行调用python脚本的方式每次都要 新建进程->启动python解释器->执行脚本->关闭python解释器->关闭进程.由此导致命令行调用的方式效率低下.

1. Java Embedded Python

基于需要频繁调用python脚本的情景,引出我们本文的主角 jep(Java Embedded Python). jep是利用了JNI去调用CPython API从而实现了java和python之间的通信.

优点:

  1. python的解释器是启动在java进程中的,而且只需首次使用时启动,调用完成后不会自动关闭,相比命令行的方式节约了进程启动和python解释器启动的时间
  2. Java和Python的调用就像是函数调用,使用方便

2. JEP简单使用

2.1 环境搭建

  • Python >= 3.5
  • Java >= 1.8
  • NumPy >= 1.7 (optional)

2.2 下载jep

执行pip install jep下载最新的jep,注意此下载不仅下载了jep的python库,jep.jar和jep.dll也在下载的文件夹下.

在这里插入图片描述

maven依赖:

<dependency>
    <groupId>black.ninia</groupId>
    <artifactId>jep</artifactId>
    <version>4.1.1</version>
</dependency>

2.3 java中简单使用

最新的jep有两种解释器SharedInterpretersSubInterpreters

SharedInterpreters

SharedInterpreters是在 Jep 3.8 中引入的。SharedInterpreter 是一个嵌入式 Python 解释器,不使用 CPython 子解释器 API。可以将它们视为 Jep/Java 中的单个 Python 解释器,但大多数事情仍然是孤立的。具体来说,每个 SharedInterpreter 实例都有自己的一组全局变量,因此在一个 SharedInterpreter 中运行 Python 代码不会影响在另一个 SharedInterpreter 中运行代码,因为它们不共享作用域。然而,它们共享的是 sys.modules,因此也是所有导入的模块。

注意,只能为所有 SharedInterpreter 使用一个 JepConfig 实例,而每个 SubInterpreter 都可以有自己的 JepConfig。这是因为 SharedInterpreters 本质上是相同的 Python 解释器,只是不同线程上有不同的全局变量。

SubInterpreters

SubInterpreters是原始的 Jep Python 解释器。子解释器使用 CPython 子解释器 API 为每个线程创建一个子解释器。子解释器大多是沙盒的并且彼此隔离。例如,如果 CPython 扩展在其 C 代码中具有不考虑子解释器的全局静态变量,则该变量在进程中以及所有子解释器中仅存在一次。因此,更改该变量将导致更改所有子解释器的设置。

子解释器可能会遇到 CPython 扩展的问题。由于大多数 CPython 扩展都是在没有考虑子解释器的情况下编写的,因此它们可能会出现问题,例如第二次初始化它们时(在第二个子解释器中)或子解释器关闭并清理内存时。为此,Jep 在子解释器中添加了共享模块的概念。这些是您专门指定的模块,因为它们应该在子解释器之间共享

注意:

无论使用哪种解释器,当你创建一个Interpreter实例时,将为该Java Interpreter实例创建一个Python解释器,并保留在内存中时,直到用Interpreter.close()关闭该Interpreter实例。由于需要管理一致的Python线程状态,创建Interpreter实例的线程必须在对该Interpreter实例的所有方法调用中重复使用.

即你对Interpreter的操作(创建,调用,关闭)必须要同一个线程内,不可更换线程.

Demo

对于初学者来说,作者推荐使用SharedInterpreter.

以下是一个简单的Demo

package com.jscoe.backend.core.commons;

import com.alibaba.fastjson.JSONObject;
import jep.Interpreter;
import jep.JepConfig;
import jep.MainInterpreter;
import jep.SharedInterpreter;
import java.util.Arrays;

public class Demo {

    public static void main(String[] args) {
        test();
    }

    public static void test() {
        // 主解释器加载jep.dll(pip下载的文件夹下)
        MainInterpreter.setJepLibraryPath("D:\\python\\Lib\\site-packages\\jep\\jep.dll");
        // 设置python的项目路径
        JepConfig config = new JepConfig();
        config.addIncludePaths("D:\\workspace\\CompanyProjects\\tobacco-impurity-detection\\script");
        //
        SharedInterpreter.setConfig(config);

        try (Interpreter interp = new SharedInterpreter()) {
            JSONObject requestJson = new JSONObject();
            requestJson.put("var1", 1);
            requestJson.put("var2", 3);
            requestJson.put("var3", Arrays.toString(new byte[]{1, 2, 3}));
            Object result;
            try {
                // 等同于python中的from tobacEngine import *
                interp.eval("from tobacEngine import *");
                //python脚本tobacEngine中的方法
                result = interp.invoke("Demo", requestJson.toJSONString());
                System.out.println(result);
                //也可以直接混编
//            interp.exec("from java.lang import System");
//            interp.exec("s = 'Hello World'");
//            interp.exec("System.out.println(s)");
//            interp.exec("print(s)");
//            interp.exec("print(s[1:-1])");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

3. 复杂场景下的使用

以上简单应用方式,Interpreter在执行完后还是会主动关闭,如果我们要重复调用就不能让他关闭.

具体场景

硬件相机每隔固定时间会获取到图像,获取到图像后会调用项目中的回调函数,回调函数内获取到图像后调用python算法去处理.

这种情况下,我们在使用Interpreter调用python后不希望Interpreter关闭,因为过段时间我们还会重复调用Interpreter.

具体实现:

1.初始化相机的回调函数
*****上下文省略*****
        String name = "t-cc-" + System.currentTimeMillis();
        ThreadGroup threadGroup = new ThreadGroup("CameraCallbackThreadGroup");
        CustomOnImageGrabbedCallback callback = new CustomOnImageGrabbedCallback();
        callback.setMonoCamera(monoCamera);
        callback.setMFrameBuffer(mFrameBuffer);
        callback.setEventPublisher(eventPublisher);
        callback.setJepAlgorithmCompute(jepAlgorithmCompute);
        CallbackThreadInitializer callbackThreadInitializer = new CallbackThreadInitializer(true, false, name, threadGroup);
        Native.setCallbackThreadInitializer(callback, callbackThreadInitializer);
    
*****上下文省略*****

Native.*setCallbackThreadInitializer*(callback, callbackThreadInitializer);可以保证每次回调都是在同一线程内执行的,即必须满足jep(创建,调用,关闭)必须要同一个线程内,不可更换线程.

2.回调函数实现
    @Data
    @EqualsAndHashCode(callSuper = true)
    public static class CustomOnImageGrabbedCallback extends MVSDK.CAMERA_SNAP_PROC {
        private long mFrameBuffer;
        private boolean monoCamera;
        private ApplicationEventPublisher eventPublisher;
        private JepAlgorithmCompute jepAlgorithmCompute;

        @Override
        public void OnCaptured(int hCamera, long pRawBuffer, MVSDK.tSdkFrameHead pFrameHead, long Context) {
            
*****上下文省略*****

            //发布事件
            ReceiveImageOriginalDataSource dataSource = ReceiveImageOriginalDataSource.builder()
                    .bands(1)
                    .createTime(System.currentTimeMillis())
                    .width(width)
                    .height(height)
                    .data(sourceData)
                    .deviceType(DeviceTypeEnum.RGB_CAMERA)
                    .build();

            eventPublisher.publishEvent(ReceiveImageOriginalDataEvent.build(dataSource));
*****上下文省略*****
    
        }
    }
3.监听事件调用python
    @Order(1)
    @EventListener
    @SneakyThrows
    public void executeAlgorithm(ReceiveImageOriginalDataEvent event) {
*****上下文省略*****
        //组装算法需要的参数
        ComputeRequest computeRequest = 	mergeCamerasImageData(receiveImageOriginalDataSource);
        //调用算法
        ComputeResponse computeResponse = jepAlgorithmCompute.compute(computeRequest);
*****上下文省略*****
    }
4. jep具体调用
package com.jscoe.backend.core.device.script;

import com.alibaba.fastjson.JSONObject;
import com.jscoe.backend.core.device.jna.MVCameraManager;
import com.jscoe.backend.core.device.jna.enums.JetInterpStatus;
import jep.SharedInterpreter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.Objects;

/**
 * @author honor
 */
@Component
@Slf4j
@RequiredArgsConstructor
public class JepAlgorithmCompute {
    private SharedInterpreter interp;
//    private static Interpreter interp;
    
    public ComputeResponse compute(ComputeRequest computeRequest) {
        if (Objects.isNull(interp)) {
            interp = new SharedInterpreter();
            MVCameraManager.jetInterpStatus = JetInterpStatus.RUNNING;
        }
*****上下文省略*****

        Object result;
        try {
            // TODO
            time3 = System.currentTimeMillis();

            interp.eval("from tobacEngine import *");
            result = interp.invoke("doAnalysis", requestJson.toJSONString());

*****上下文省略*****
        } catch (Exception e) {
            e.printStackTrace();
        }
        return response;
    }

    public void closeInterp() {
        if (Objects.isNull(interp)) {
            return;
        }
        interp.close();
        interp = null;
    }
}
5.关闭jep

具体场景为更新相机参数后需要关闭jep,重启相机,重新创建jep的python解释器.

重点是关的时候要去当时创建SharedInterpreter的线程中去调用interp.close();

public void close(Setting.RgbCameraSetting setting) {
        settingManager.updateById(Setting.builder().id(SETTING_ID).rgbCameraSetting(setting).build());
        //参数修改后设备需要重启
        MVCameraManager.jetInterpCloseFlag = true;
        while(MVCameraManager.jetInterpStatus == JetInterpStatus.RUNNING){
            log.warn("jep解释器还未关闭,等待中...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
//        jepAlgorithmCompute.closeInterp();
        log.info("jep解释器已经关闭");
        mvCameraManager.tryConnectAll();
    }

总结

实际使用中我们需要综合看需求以及各种技术方案的特点来决定要使用哪种方式。

Java调用Python,技术方案有两大类:External(外部调用)和Embedded(内置依赖)。

Ex方案

特点是视Python脚本为独立运行的进程,通过进程间通信的方法来调用。Ex方案的共通优点是,Java和Python运行时是完全脱钩的,对运行时的要求少。而且安全性好,不会因为Python脚本出现如进程崩溃等问题影响Java进程的稳定性,尤其适合Python代码不稳定,Bug多的场景。共通缺点是性能差于Em方案,而且接口的灵活性也不如Em方案(预先定义好的接口是唯一的调用入口)。
典型的就是Runtime.exec,直接启动进程的方法。这种方法是最简单的,但是性能和灵活性在所有方法里面是最差的。适合1.只是偶尔调用脚本,对性能要求不高 2.因为只能通过输入输出等手段通信,适合没有复杂的和脚本交互的需求。
另一个典型的Ex方案是把Python做成独立的,一直运行的服务进程,Java通过RPC(远程过程调用)或者HTTP请求来调用Python。优点是部署灵活,甚至可以突破单个机器限制做成分布式。由于RPC,接口的灵活性优于直接启动进程。缺点就是上面说的。

Em方案

Em方案的特点是在Java进程里面嵌入执行Python的模块,把Python代码也当做Java代码一样对待。这类方案的优点和缺点正好和Ex方案相反。
Jython和JEP是两个典型的Em方案。Jython和Java结合紧密些。JEP则兼容性好些,甚至C写的编译好的Python模块都可以随便调用。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值