【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.mzph.cn/news/616182.shtml

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

相关文章

照片模糊如何变清晰不妨试试这款软件吧

很多人希望能把模糊的图片或照片变得很清晰&#xff0c;或者把一个只有几十KB的小图变成有几M大小的高清大图。一般来说&#xff0c;一张模糊或打了马赛克的图片本身很多细节信息就没有或被删除了&#xff0c;就像一本书缺了很多页&#xff0c;我们是可能百分百的还原出它原来的…

知道IP怎么反查域名?这几个方法一查一个准!

知道网络IP怎么反查出真实域名来&#xff1f;给大家分享几个我常用的方法&#xff0c;就算你不懂技术你都能查得出来&#xff01; 一、fofa 这是一个白帽黑客非常喜欢用的社工平台&#xff0c;只要你输入IP就能查到很多背后的信息。 传送门&#xff1a;https://fofa.info 二…

GPT Store开业大吉:一场AI技术与创新的盛宴

就在1.11 日&#xff0c;ChatGPT 正式上线 GPT Store &#xff01; OpenAI CEO 山姆奥特曼第一时间确认了这个消息&#xff1a; 自从GPTs的概念提出以来&#xff0c;短短两个月内&#xff0c;全球用户已经创造了超过300万个GPTs。 点击 GPT Store 或者进入ChatGpt页面&am…

【Vue】引入路径正确,不影响正常运行但文件爆红

现象&#xff1a;引入路径正确但文件爆红&#xff0c;不影响运行但不美观&#xff08;按住Ctrl可以跳转到该文件&#xff0c;关闭后过段时间再打开还是爆红&#xff09; 原因 &#xff08;1&#xff09;相对路径使用了不正确的大小写 &#xff08;2&#xff09;项目不支持force…

AD20 解决PCB铺铜与锡盘之间锯齿状连接问题的设置方法

上一篇文章&#xff1a;PCB简单绘制一般步骤 对上一篇文章中&#xff0c;关于铺铜设置的补充&#xff0c;解决铺铜与锡盘之间的锯齿状连接情况。 1、新建Demo&#xff0c;创建PCB板子&#xff0c;布置锡盘和铺铜&#xff0c;如图&#xff1a; 2、设置规则&#xff0c;参考上一…

8. 《自动驾驶与机器人中的SLAM技术》基于保存的自定义NDT地图文件进行自动驾驶车辆的激光定位

目录 1. 为 NDT 设计一个匹配度评估指标&#xff0c;利用该指标可以判断 NDT 匹配的好坏。 2. 利用第 1 题的指标&#xff0c;修改程序&#xff0c;实现 mapping 部分的回环检测。 3. 将建图结果导出为 NDT map&#xff0c;即将 NDT 体素内的均值和协方差都存储成文件。 4.…

Redis的安装与在spring中使用

1. Redis入门 1.1 Redis简介 Redis是一个基于内存的key-value结构数据库。Redis 是互联网技术领域使用最为广泛的存储中间件。 官网&#xff1a;Redis 中文网&#xff1a;Redis中文网 key-value结构存储&#xff1a;&#xff08;哈希&#xff09;时间o1 主要特点&#xff1…

在线旅游2024:新旧交锋,暗流涌动

旅游热带来的泼天富贵&#xff0c;还在继续传递。 2023年大火的“烧烤之都”淄博曾是最大受益者&#xff0c;小烧烤风靡整个夏天。最近的哈尔滨凭借冰雪和异域特色一举成为新晋“网红旅游城市”&#xff0c;元旦假期的游客接待量和旅游总收入双双达到历史峰值。 “网红城市”…

stl中的list模拟实现

目录 一、list的简单介绍二、写出节点的代码三、模拟实现迭代器&#xff08;重点&#xff09;1、list中的迭代器是怎么实现的2、编写iterator类的代码3、对const_iterator进行理解4、编写const_iterator类的代码5、对iterator类和const_iterator类进行合并 四、list类进行代码实…

VLAN 详解二(VLAN 基础配置)

VLAN 详解二&#xff08;VLAN 基础配置&#xff09; VLAN 配置其实是非常简单的&#xff0c;但是想要学得比较精还是需要花费一些功夫的&#xff0c;根据不同的 VLAN 划分方式用不同的配置方法&#xff0c;但其实配置方法基本上都大同小异。 下面就以在实际网络中最常用的基于…

js 数据回调 异步 Promise

回调顺序 JavaScript 函数按照它们被调用的顺序执行。而不是以它们被定义的顺序。 js数据顺序问题 <!DOCTYPE html> <html> <body><h2>JavaScript 函数序列</h2><p>JavaScript 函数按照它们被调用的顺序执行。</p><p id"de…

智慧厂区烟火识别系统应用

在当今的智能制造行业中&#xff0c;安全管理已成为优先考虑的重要议题。集度汽车公司在其实验室场区引入了一项创新技术——富维图像厂区烟火识别系统。这个项目的核心是利用先进的烟火识别系统&#xff0c;保障厂区的安全与稳定运行。 系统特点 烟火识别系统的准确率高和误报…

基于Docker Compose单机实现多级缓存架构2024

文章目录 一、环境参考二、专栏简介三、扩展 一、环境参考 NameVersionDocker Desktop for Windows4.23.0Openjdk8MySQL8.2.0Redis7.2Canal1.1.7OpenResty1.21.4.3-3-jammy-amd64Lua-Caffeine- 二、专栏简介 多级缓存实现过程比较长&#xff0c;将拆分为多个文章分步讲述。如…

弈 - Codeql 自动运行和项目监控工具

前言 代码审计总是离不开一些神器&#xff0c;笔者常用 Codeql[1] 这款工具辅助挖洞。当我每写一个规则都需要对其它项目手动运行检查一遍&#xff0c;效率很低&#xff0c;再加上 lgtm[2] 的关闭&#xff0c;此项目诞生了 --- 弈(Yi)[3] 。 CVE-2021-43798 这里以 Graana 的…

mysql主从复制教程

1、介绍 1.1 是什么 主从复制&#xff0c;是用来建立一个和主数据库完全一样的数据库环境&#xff0c;称为从数据库 1.2 有什么用 数据备份&#xff1a;通过主从复制&#xff0c;可以将主数据库的数据复制到一个或多个从数据库中&#xff0c;以实现数据备份和灾难恢复。当主…

认识异常及异常处理机制之try-catch

异常类 什么是异常&#xff1f;就像人会犯错一样&#xff0c;程序在运行的过程中也会犯错。程序中的错误有两类&#xff0c;一类称为Error&#xff08;错误&#xff09;&#xff0c;另一类称为Exception&#xff08;异常&#xff09;。Error类和Exception类都为Throwable的子类…

git: Updates were rejected because the tip of your current branch is behind

一、报错含义 由于本地分支的tip落后远程分支&#xff0c;push操作被拒绝。 二、产生原因 我再本地拉去了新的分支并未同步到远程仓库&#xff0c;在新分支进行开发&#xff0c;由于前几天同步也创建了该分支并同步到了远程仓库&#xff0c;导致我本次push失败 三、解决方…

解决uni-app小程序获取路由及路由参数

代码: this.id = this.$route.query.id;错误信息: 解决方案: // 获取query对象// #ifdef H5this.id = this.$route

部署ATS(Apache Traffic Server)和Nginx正向代理服务性能对比

部署ATS&#xff08;Apache Traffic Server&#xff09;和Nginx正向代理服务&性能对比 1. 正向代理的用途2. ATS(Apache Traffic Server)正向代理服务器部署3. Nginx正向代理服务器部署4. 性能对比 1. 正向代理的用途 正向代理一般是用于内部网络出去&#xff0c;反向代理一…

【LeetCode每日一题】2085. 统计出现过一次的公共字符串(哈希表)

2024-1-12 文章目录 [2085. 统计出现过一次的公共字符串](https://leetcode.cn/problems/count-common-words-with-one-occurrence/)思路&#xff1a;哈希表计算 2085. 统计出现过一次的公共字符串 思路&#xff1a;哈希表计算 1.用两个哈希表分别统计word1和word2中字符出现的…