【grpc】利用protobuf实现java或kotlin调用python脚本,含实现过程和全部代码

前言

在一些特殊场景中,我们可能需要使用java或者其他任意语言调用python脚本或sdk等。本文的需求衍生也不例外于此,python端有sdk,但只能在python中调用,于是就有了本文章。
常见的调用方式如jython、python提供http rest接口、python提供rpc实现、java通过jni调用转换成c的python。每种调用方式都有优缺点,我们更期待一种简单、快速、功能更自由、低侵入、方便维护的方式来实现。
快速调研了一下现有的各种实现方式,最后决定采用grpc调用,好处就是代码不多,协议定义简单方便,两端协调好就可以了,非常适合对sdk、算法、脚本、服务的调用,缺点就是更改协议后,两边要重新生成代码来保持同步,不过在有现成插件的情况下,这能很方便的控制,话不多说,下面贴出详细做法。

一、定义proto文件

创建一个文件名为script.proto,稍后需要在java端和python端引入

//@ 1 使用proto3语法
syntax = "proto3";
//@ 2 生成多个类(一个类便于管理)
option java_multiple_files = false;
//@ 3 定义调用时的java包名
option java_package= "com.kamjin.javacallpython.grpc.demo.proto";
//@ 4 生成外部类名
option java_outer_classname = "ScriptProto";
//@ 6. proto包名称(逻辑包名称)
package script;import "google/protobuf/struct.proto";//@ 7 定义一个服务来描述要生成的API接口,类似于Java的业务逻辑接口类
service ScriptService{//定义执行方法,方法名称和参数和返回值都是大驼峰//Note: 这里是 returns,不是 returnrpc Execute (ScriptRequest) returns (ScriptResponse) {}
}//@ 8 定义请求数据结构
//字符串数据类型
//等号后面的数字即索引值(表示参数顺序,以防止参数传递顺序混乱),服务启动后无法更改
//不能使用19000-1999保留数字
message ScriptRequest{string content = 1;google.protobuf.ListValue extract_params = 2;
}
//@ 9 定义响应数据结构
message ScriptResponse{string result = 1;
}

二、java/kotlin端

个人习惯使用kotlin+gradle,此处使用该组合演示,java+maven也可以,主要是gradle配置部分区别较大,有需求可以评论区留言

0.创建服务

创建一个springboot项目,版本为2.x,为了方便起见,需要是web服务,端口默认就可以

1.安装protobuf插件

在IDEA插件市场搜索protobuf下载安装,注意作者是HIGAN,不要装错了,如图
在这里插入图片描述

2.依赖和其他配置

配置模块的build.gradle.kts文件,
新增依赖和plugin如下:

plugins {//protobuf pluginid("com.google.protobuf") version "0.9.4"...
}dependencies {//grpc clientimplementation("net.devh:grpc-client-spring-boot-starter:2.15.0.RELEASE")implementation("io.grpc:grpc-stub:1.15.1")implementation("io.grpc:grpc-protobuf:1.15.1")...
}

protobuf配置和task配置如下:

import com.google.protobuf.gradle.*
import org.gradle.kotlin.dsl.proto//https://github.com/google/protobuf-gradle-plugin
sourceSets {main {proto {srcDir("src/main/proto")include("**/*.proto")}}test {proto {srcDir("src/test/proto")}}
}
protobuf {protoc {// The artifact spec for the Protobuf Compilerartifact = "com.google.protobuf:protoc:3.17.3"}plugins {// Optional: an artifact spec for a protoc plugin, with "grpc" as// the identifier, which can be referred to in the "plugins"// container of the "generateProtoTasks" closure.id("grpc") {artifact = "io.grpc:protoc-gen-grpc-java:1.40.0"}}generateProtoTasks {ofSourceSet("main").forEach {it.plugins {// Apply the "grpc" plugin whose spec is defined above, without// options. Note the braces cannot be omitted, otherwise the// plugin will not be added. This is because of the implicit way// NamedDomainObjectContainer binds the methods.id("grpc")}}}
}//配置提示proto文件重复的处理策略
tasks.withType<ProcessResources> {duplicatesStrategy = DuplicatesStrategy.INCLUDE
}

配置完成后点一下gradle的刷新按钮reload all gradle projects,此时会下载相关依赖

3.生成代码

在模块的src/main目录下新建名为proto文件夹,将定义好的script.proto文件放入该目录,运行gradle task,如图所示:
在这里插入图片描述
运行该task后将会生成可以调用的proto服务代码,将在文件夹build/generated/source/proto/main可以找到生成的代码,一般无需改动该代码,我们需要使用时直接调用引入即可。

4.服务配置

在模块配置文件application.yaml中配置如下:

grpc:client:scriptServiceGrpc:address: 'static://127.0.0.1:50051'negotiationType: plaintext
  • scriptServiceGrpc是我们在代码里需要声明的grpc server名称,可以任意自定义和在grpc.client下定义多个这样的条目
  • address指定grpc server端的地址+端口,在当前文章中对应的就是python项目中的grpc服务URL地址

关于配置项的更多详情可以查看这里。

5.编写grpc client代码

首先编写一个controller用于调试代码

package com.kamjin.javacallpython.grpc.demo.controller.testimport com.kamjin.javacallpython.grpc.demo.handle.*
import com.kamjin.common.ext.*
import org.springframework.beans.factory.annotation.*
import org.springframework.web.bind.annotation.*/*** <p>** </p>** @author kam* @since 2024/01/08*/
@RequestMapping("/test/proto/")
@RestController
class ProtoTestController {@Autowiredlateinit var grpcScriptExecuter: GrpcScriptExecuter@PostMapping("script")fun script(@RequestBody request: MutableMap<String, Any?>): String? {val contentBase64 = request["content_base64"] as String? ?: return ""return this.grpcScriptExecuter.exec(ScriptContent(content = contentBase64.base64Decode(),extractParams = request["extract_params"] as List<String>? ?: mutableListOf())).result}
}

执行脚本的GrpcScriptExecuter,内容如下:

package com.kamjin.javacallpython.grpc.demo.handleimport com.google.protobuf.*
import com.kamjin.javacallpython.grpc.demo.proto.*
import net.devh.boot.grpc.client.inject.*
import org.springframework.stereotype.*/*** <p>** </p>** @author kam* @since 2024/01/08*/
interface ScriptExecute {fun exec(content: ScriptContent): ScriptExecResult
}data class ScriptContent(val content: String,val extractParams: List<String> = mutableListOf()
)data class ScriptExecResult(val result: String? = null)@Component
class GrpcScriptExecuter : ScriptExecute {@GrpcClient("scriptServiceGrpc")private lateinit var scriptStub: ScriptServiceGrpc.ScriptServiceBlockingStuboverride fun exec(content: ScriptContent): ScriptExecResult {val c = content.contentif (c.isBlank()) return ScriptExecResult()val extractParams = content.extractParamsval r = ScriptProto.ScriptRequest.newBuilder().setContent(c).apply {if (extractParams.isNotEmpty()) {this.extractParams = ListValue.newBuilder().apply {for (ep in extractParams) {this.addValues(Value.newBuilder().setStringValue(ep).build())}}.build()}}.build()try {return ScriptExecResult(scriptStub.execute(r).result)} catch (e: io.grpc.StatusRuntimeException) {throw RuntimeException("script exec error,msg: ${e.message}", e)}}}
  • @GrpcClient("scriptServiceGrpc")的值对应的则是上一步中在appliation.yaml中配置的值
  • 当前文件做了两件事:
    1.定义一个ScriptExecute的interface和请求/响应的data class
    2.实现了GrpcScriptExecuter,用于通过调用grpc server端执行脚本内容

这样就完成了java端grpc client的创建。

三、python端

0.安装protobuf插件

同样需要安装protobuf插件,上文已经描述过了(idea plugin)不再赘述

1.创建项目

创建一个python venv项目,在模块中创建一个新的文件夹:proto_test

2.复制proto文件

把之前定义的script.proto文件复制到其中,要求和java服务端放入的文件保持一致,不用做任何改动。

3.生成代码

转到控制台,使用pip安装需要的依赖

pip install grpcio
pip install grpcio-tools googleapis-common-protos

然后进入proto_test目录,生成相应的grpc代码

python -m grpc_tools.protoc -I . --python_out=. --grpc_python_out=. script.proto

此时会在proto_test目录下生成文件:script_pb2_grpc.pyscript_pb2.py,后面会用到。

4.编写grpc server代码

创建文件:script_server.py,内容如下:

import jsonimport grpc
import script_pb2
import script_pb2_grpc
from concurrent import futures
import time_ONE_DAY_IN_SECONDS = 60 * 60 * 24# service impl
class ScriptServicer(script_pb2_grpc.ScriptServiceServicer):def Execute(self, request, context):s = request.contentresult = {}print("content: %s" % s)exec(s, result)# 根据传入的参数提取值data = {}for p in request.extract_params:data[p] = result.get(p, None)return script_pb2.ScriptResponse(result=json.dumps(data))def serve():server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))script_pb2_grpc.add_ScriptServiceServicer_to_server(ScriptServicer(), server)server.add_insecure_port('[::]:50051')server.start()try:while True:time.sleep(_ONE_DAY_IN_SECONDS)except KeyboardInterrupt:server.stop(0)if __name__ == '__main__':serve()

这样就完成了python端grpc server的创建。

四、验证

1.启动java服务:通过IDEA运行WEB服务
2.启动python服务:python script_server.py
3.使用postman或者IDEA httpclient调用接口,这里使用IDEA的http client
定义文件javacallpython-grpc.http

POST http://localhost:8080/test/proto/script
Content-Type: application/json{"content_base64": "aW1wb3J0IG1hdGgKZGVmIGZ1biAobik6CiAgICBkYXRhID0gbgogICAgZGF0YSA9IGRhdGEgKiBtYXRoLnBpCiAgICByZXR1cm4gZGF0YQpyID0gZnVuKDEwKQ==","extract_params": ["r"]
}

运行该调用,这将会调用刚刚启动的web服务(端口为8080默认)接口:/test/proto/script

  • 此处传的content_base64是因为json中不支持’‘’‘’'标注的字符串,也就没法满足python的缩进要求,故将脚本内容转为base64传入,实际脚本内容为:
import math
def fun (n):data = ndata = data * math.pireturn data
r = fun(10)

转为base64后:

aW1wb3J0IG1hdGgKZGVmIGZ1biAobik6CiAgICBkYXRhID0gbgogICAgZGF0YSA9IGRhdGEgKiBtYXRoLnBpCiAgICByZXR1cm4gZGF0YQpyID0gZnVuKDEwKQ==
  • extract_params是表明我们需要提取脚本中变量名称为r的内容的值作为脚本执行结果返回。

python端控制台打印:
在这里插入图片描述

http client执行结果:

在这里插入图片描述

这表明带import的脚本执行成功,并正确返回了我们想要提取的值

参考文章

1.拥抱云原生,Java与Python基于gRPC通信
2.base64和字符串互转
3.Import Lib not working with exec function?
4.yidongnan/grpc-spring-boot-starter
5.google/protobuf-gradle-plugin

结语

本文实现了通过grpc在java端传入脚本内容,在python端执行的脚本的实现方法,性能状况未测试,后续如果有时间会对其进行使用验证,如果发现问题,可以做相关改进,会在本文进行更新,本文的实现对实际项目中的使用具有一定的参考价值。
后面会继续更新分享更多相关内容,请多多关注~

最后,各位看众可以思考一下:

为什么以上做法可以成功执行带import的脚本?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/239041.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

DC电源模块在物联网设备中的关键作用

BOSHIDA DC电源模块在物联网设备中的关键作用 DC电源模块在物联网设备中发挥着关键作用。物联网设备通常需要稳定可靠的电源供应&#xff0c;以保证设备的正常运行。DC电源模块提供了相应的电压和电流输出&#xff0c;为物联网设备提供所需的电力。 具体来说&#xff0c;DC电…

解决英特尔无线网卡WiFi或者蓝牙突然消失问题

winR&#xff0c;输入“devmgmt.msc”&#xff0c;检查设备管理器中的无线网卡驱动是否安装好。 访问https://www.intel.cn/content/www/cn/zh/download/19351/windows-10-and-windows-11-wi-fi-drivers-for-intel-wireless-adapters.html下载对应系统版本的英特尔无线网卡WiFi…

创始人记|2023「年终总结」暨 WorkfineV6版本发布

大家好&#xff0c;我是Workfine黄坤&#xff0c;很高兴能够再次为大家进行分享。衷心感谢大家一直以来对我们的厚爱和支持。新的一年&#xff0c;祝愿大家财源滚滚&#xff0c;身体健康&#xff0c;阖家幸福。 这是我们创业的第七个年头&#xff0c;进入这一行也有整整十年。…

使用 Apache POI 更新/覆盖 特定的单元格

使用 Apache POI 更新特定的单元格 一. 需求二. 实现三. 效果 一. 需求 将以下表中第4行&#xff0c;第4列的单元格由“张宇”更新为“汤家凤”&#xff0c;并将更行后的结果写入新的Excel文件中&#xff1b; 二. 实现 使用Apache POI&#xff0c;可以精确定位到需要更改的单…

Resize:最近邻插值、双线性插值、双三次插值

Resize&#xff1a;最近邻插值、双线性插值、双三次插值 Opencv resize函数1. 最近邻插值&#xff08;INTER_NEAREST&#xff09;1.1 原理1.2 代码实例1.3 简单的代码复现1.4 特点 2. 双线性插值&#xff08;INTER_LINEAR&#xff09;&#xff08;默认值&#xff09;2.1 原理2.…

【React源码 - Diff算法】

介绍 在React学习中&#xff0c;Diff算法(协调算法)&#xff0c;想必我们并不陌生&#xff0c;简单来说就是一个对比新老节点寻找差异&#xff0c;然后找出最小的一个变化集&#xff0c;最后对这个最小变化集进行最小的DOM操作&#xff0c;本文将从源码来分析在React(17.0.2)中…

聚观早报 |一加Ace 3鸣沙金开售;地平线征程6将发布

聚观早报每日整理最值得关注的行业重点事件&#xff0c;帮助大家及时了解最新行业动态&#xff0c;每日读报&#xff0c;就读聚观365资讯简报。 整理丨Cutie 1月16日消息 一加Ace 3鸣沙金开售 地平线征程6将发布 互动短剧《谍影成双》首曝PV 台积电发布四季度财报 保时捷…

kafka系列(二)

本章承接kafka一内容&#xff0c;文章在本人博客主页都有&#xff0c;可以自行点击浏览。 幂等性 请求执行多次&#xff0c;但执行的结果是一致的。 如果&#xff0c;某个系统是不具备幂等性的&#xff0c;如果用户重复提交了某个表格&#xff0c;就可能会造成不良影响。例如…

第十讲 单片机驱动彩色液晶屏 控制RA8889软件:图像运算

单片机驱动TFT彩色液晶屏系列讲座 目录 第一讲 单片机最小系统STM32F103C6T6通过RA8889驱动彩色液晶屏播放视频 第二讲 单片机最小系统STM32F103C6T6控制RA8889驱动彩色液晶屏硬件框架 第三讲 单片机驱动彩色液晶屏 控制RA8889软件:如何初始化 第四讲 单片机驱动彩色液晶屏 控…

助力工业焊缝质量检测,YOLOv7【tiny/l/x】不同系列参数模型开发构建工业焊接场景下钢材管道焊缝质量检测识别分析系统

焊接是一个不陌生但是对于开发来说相对小众的场景&#xff0c;在我们前面的博文开发实践中也有一些相关的实践&#xff0c;感兴趣的话可以自行移步阅读即可&#xff1a;《轻量级模型YOLOv5-Lite基于自己的数据集【焊接质量检测】从零构建模型超详细教程》 《基于DeepLabV3Plus…

反向代理的本质是什么?

反向代理是一种网络架构模式&#xff0c;通常用于提供静态内容、处理安全、负载均衡和缓存等任务。在这种架构中&#xff0c;客户端发送的请求首先到达反向代理服务器&#xff0c;然后由反向代理服务器将请求转发给后端的实际服务器。反向代理服务器可以处理和修改请求和响应&a…

20. 从零用Rust编写正反向代理,四层反向代理stream(tcp与udp)实现

wmproxy wmproxy已用Rust实现http/https代理, socks5代理, 反向代理, 静态文件服务器&#xff0c;四层TCP/UDP转发&#xff0c;内网穿透&#xff0c;后续将实现websocket代理等&#xff0c;会将实现过程分享出来&#xff0c;感兴趣的可以一起造个轮子 项目地址 gite: https:…

Linux 下GEO Server发布图层后,中文乱码解决方案

发布的图层&#xff0c;显示中文乱码&#xff0c;都是框框&#xff1a;如“口口” 第一步先查看Linux字符集 如下命令所示&#xff1a; 1.查看当前系统语言 echo $LANG2.查看安装的语言包 locale如果上面的命令执行后显示的是en_US.UTF-8&#xff0c;则说明当前语言系统及安…

创意交融:集成自定义报表和仪表盘设计器,实现图标替换

前言 在现代数据分析领域&#xff0c;随着对报表和数据分析的需求不断增长&#xff0c;市场上涌现了许多嵌入式报表工具。这些工具能够与企业现有的OA、ERP、MES、CRM等应用系统深度集成&#xff0c;实现对业务数据的自助式分析。然而&#xff0c;在实际应用中&#xff0c;不同…

实战 php 使用 wkhtmltopdf 生成pdf的全过程

公司里边有生成pdf报告的业务需求,之前有过尝试用tcpdf,直接生成的pdf的过程,但是pdf报告的内容数据,根据不同内容的变化,都是各种各样的bug,一直处理修修补补的状态,让后台开发人员很是头疼. 经过思索和甄选,总结出我们的业务中是由于样式不可控导致的,当时从逻辑上就思考到用…

QT quick基础:组件gridview

组件gridview与android中gridview布局效果相同。下面记录qt quick该组件的使用方法。 方法一&#xff1a; // ContactModel.qml import QtQuick 2.0ListModel {ListElement {name: "1"portrait: "icons/ic_find.png"}ListElement {name: "2"por…

【iOS】数据存储方式总结(持久化)沙盒结构

在iOS开发中&#xff0c;我们经常性地需要存储一些状态和数据&#xff0c;比如用户对于App的相关设置、需要在本地缓存的数据等等&#xff0c;本篇文章将介绍六个主要的数据存储方式 iOS中数据存储方式&#xff08;数据持久化&#xff09; 根据要存储的数据大小、存储数据以及…

scrollTop与offsetTop解决小分辨率区域块向上滚动效果效果,结合animation与@keyframes实现标题左右闪动更换颜色效果。

scrollTop 是一个属性&#xff0c;它表示元素的滚动内容垂直滚动条的位置。对于可滚动元素&#xff0c;scrollTop 属性返回垂直滚动条滚动的像素数&#xff0c;即元素顶部被隐藏的像素数。 offsetTop 是一个属性&#xff0c;用于获取一个元素相对于其父元素的垂直偏移量&…

bee工具的使用及创建第一个项目

前提文章&#xff1a;beego的安装及配置参数说明-CSDN博客 提示&#xff1a;beego框架下项目需要再GOPATH/src下进行开发&#xff0c;我的GOPATH是C:\Users\leell\go 一、web项目创建 通过 bee new 创建web项目 C:\Users\leell\go\src>bee new beego-web 2024/01/15 21:…

【新】Unity Meta Quest MR 开发(一):Passthrough 透视配置

文章目录 &#x1f4d5;教程说明&#x1f4d5;配置透视的串流调试功能&#x1f4d5;第一步&#xff1a;设置 OVRManager&#x1f4d5;第二步&#xff1a;添加 OVRPassthroughLayer 脚本&#x1f4d5;第三步&#xff1a;在场景中添加虚拟物体&#x1f4d5;第四步&#xff1a;设置…