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

news2024/11/17 9:39:33

前言

在一些特殊场景中,我们可能需要使用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,不是 return
    rpc 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 plugin
    id("com.google.protobuf") version "0.9.4"
    
    ...
}

dependencies {
     //grpc client
    implementation("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 Compiler
        artifact = "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.test

import 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 {
    
    @Autowired
    lateinit 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.handle

import 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.ScriptServiceBlockingStub

    override fun exec(content: ScriptContent): ScriptExecResult {
        val c = content.content
        if (c.isBlank()) return ScriptExecResult()
        val extractParams = content.extractParams
        val 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 json

import 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.content
        result = {}
        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 = n
    data = data * math.pi
    return 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.coloradmin.cn/o/1377431.html

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

相关文章

波动,热传导,扩散方程建立

数学物理方程是从自然科学的各个领域和工程技术领域中导出的偏微分方程和积分方程.在这些以偏微分方程为基础的数学模型中&#xff0c;二阶线性偏微分方程中的三个典型方程与定解条件的建立、解法及其应用&#xff0e;描述振动和波动过程的波动方程、描述输运过程的热传导&…

2024年MIA最新生成综述:基于深度学习的MRI/CT/PET合成【文献阅读】

2024年MIA最新生成综述&#xff1a;基于深度学习的MRI/CT/PET合成【文献阅读】 基本信息 标题&#xff1a;Deep learning based synthesis of MRI, CT and PET: Review and analysis发表年份: 2024期刊/会议: Medical Image Analysis分区&#xff1a; SCI 1区IF&#xff1a;1…

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

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

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

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

Spring Cloud Alibaba整合RocketMQ架构原理分析

关于RocketMQ的原理&#xff0c;本文就不做详细分析了&#xff0c;这里就重点关注Spring Cloud Alibaba是如何整合RocketrMQ的。 Part.1 使用原生RocketMQ客户端&#xff1f; RocketMQ提供了RocketMQ Client SDK&#xff0c;开发者可以直接依赖这个SDK&#xff0c;就可以完成…

NAS使用的一些常见命令 ssh sftp 上传 下载 ALL in one

目录 登陆上传/下载内网穿透 登陆 ssh 登陆 ssh usernameserverIP -p portNumsftp 登陆 sftp -P portNum usernameserverIP上传/下载 如ls等&#xff0c;远程服务器操作 如lls等&#xff0c;本机操作&#xff0c;前缀为l 文件 put **** 将本机上文件上传到远程服务器上当…

初识Ubuntu

其实还是linux操作系统 命令都一样 但是在学习初级阶段&#xff0c;我还是将其分开有便于我的学习和稳固。 cat 查看文件 命令 Ubuntu工作中经常是用普通用户&#xff0c;在需要时才进行登录管理员用户 sudn -i 切换成管理用户 我们远程连接时 如果出现 hostname -I没有出现…

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

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

一本数学教材严谨和通俗哪个更重要?

一本教材也许无法同时兼顾严谨和通俗&#xff0c;而且在不同的场景下&#xff0c;严谨和通俗的重要性也不尽相同&#xff1a; 在正式的学术场合&#xff0c;严谨当然重要&#xff0c;一些不严谨的教材可能无法通过审校&#xff0c;在读者存在疑问的时候&#xff0c;也不一定能给…

C++ Webserver从零开始:基础知识(一)——Linux网络编程基础API

前言 本专栏将从零开始制作一个C Webserver&#xff0c;用以记录笔者学习的过程 如果你想要跟着我这个专栏制作一个C Webserver,你需要掌握以下前置基础课程知识&#xff1a; 1.C/C的语法&#xff08;在Leetcode刷100~200题的程度即可&#xff09; 2.计算机网络基础知识 3…

WSL2-Ubuntu20.04-配置

WSL2-Ubuntu20.04-配置 安装wsl2安装Ubuntu20.04安装anacondaWSL2可视化&#xff08;VcXsrv&#xff09; 安装wsl2 wsl --install wsl -l -v # 版本查看 默认的都是 wsl2 &#xff08;如果是wsl1 就自行升级 wsl --update&#xff09; 官方教程 安装Ubuntu20.04 安装wsl2之后…

CRLF漏洞靶场记录

搭建 利用 docker 搭建 vulhub 靶场 git clone https://github.com/vulhub/vulhub.git 进入 /vulhub/nginx/insecure-configuration 目录 启动前关闭现有的 8080、8081、8082 端口服务&#xff0c;避免端口占用 docker-compose up -d 进入容器 docker exec -it insecure-…

char常见问题之一【C语言】

引出 在所写的代码中&#xff1a; char ch0 "asd";报错&#xff1a;因为char类型的变量只能存储一个字符&#xff0c;不能存储字符串 char ch1a;正确 char ch2"a";报错&#xff1a;因为&#xff0c;虽然a是一个字符&#xff0c;但是用了双引号&#xf…

视频号下载小助手:教你微信视频号怎么提取视频出来

作为一名剪辑师或自由职业者,我们作为短视频创作者有时候需要下载多个视频用于制作多个解说系列的视频或者连续剧。然而,下载这些视频通常需要花费大量时间和精力,尤其是在没有合适的工具的情况下&#xff0c;让我们制作视频也确实困难&#xff0c;那么我们该如何解决呢&#x…

《产业结构调整指导目录(2024年本)》发布,模糊测试首次纳入

近日&#xff0c;第6次委务会议通过了新版的《产业结构调整指导目录&#xff08;2024年本&#xff09;》&#xff0c;该目录自2024年2月1日起正式实施。 与之前的版本相比&#xff0c;本次目录在行业设置上进行了全面升级&#xff0c;新增了“网络安全”这一重要行业大类&#…

Pytest插件pytest-cov:优雅管理测试覆盖率

在软件开发中&#xff0c;测试覆盖率是评估测试质量的关键指标之一。为了更方便地统计和管理测试覆盖率&#xff0c;Pytest插件"pytest-cov"应运而生。本文将介绍"pytest-cov"的基本用法和优雅管理测试覆盖率的方法。 什么是pytest-cov? pytest-cov 是Pyt…

Css样式制作图形倒影

该CSS样式是WebKit&#xff08;主要应用于Safari和其他基于WebKit的浏览器&#xff09;的特定前缀属性&#xff0c;用于实现元素内容的反射效果。具体解释如下&#xff1a; -webkit-box-reflect: 定义了一个盒反射效果&#xff0c;仅在支持WebKit的浏览器中生效。 below 15px&a…

JS-DOM树和DOM对象

作用和分类 作用&#xff1a;就是使用JS去操作html和浏览器 分类&#xff1a;DOM&#xff08;文档对象模型&#xff09;、BOM&#xff08;浏览器对象模型&#xff09; 什么是DOM DOM&#xff08;Document Object Model--文档对象模型&#xff09;是用来呈现以及与任意HTML或…

Laravel 框架中队列的使用

概述 Laravel 框架内置了强大的队列系统&#xff0c;用于处理异步任务、提高系统性能等。队列可以让任务异步执行&#xff0c;而不会阻塞当前进程&#xff0c;可以提高系统的处理能力。 Laravel 的队列系统支持多种驱动&#xff0c;如 Redis、Beanstalkd、SQS 等&#xff0c;…

静态代理IP是如何助力跨境电商运营的?我的跨境电商发展史

跨境电商这几年的火爆程度已经不需要我多说什么了&#xff0c;我自己与跨境电商结缘还是无意之间在某乎上看了那种所谓的“0基础小白如何在家做跨境电商&#xff0c;副业月入XX&#xff0c;附选品指南&#xff01;”。 我不知道你们刷到过这种类似的帖子没有&#xff0c;当时…