spring boot框架(java)和 hyperf框架(php)多语言开发微服务,nacos作为服务发现,实现php服务与java服务跨服务调用,实现方案【feign和jsonRpc配合使用】

129 篇文章 0 订阅

项目微服务架构,采用php和java 混合开发 避免不了 服务与服务调用,经过php和java的框架的研究,终于两边调用成功了

还是直接上代码 依赖 搭建哪些 都略过了

java 这边 spring cloud + spring boot +nacos + feign + jsonRpc
php 这边 hyperf + nacos + jsonRpc

没有用getway 用 nginx 进行服务切换

hyperf 官方文档地址
https://hyperf.wiki/3.1/#/zh-cn/json-rpc

服务与服务调用 要求是 必须用 jsonRpc 调用
对于jsonRpc的概念 也不多说了

先说一下我测试出来的兼容问题 :

php jsonRpc结构
{"method":"findById","context":[],"id":"663b59258ee1b","jsonrpc":"2.0","params":{"id":"1"}}
java jsonRpc结构
{"id":"64803436","jsonrpc":"2.0","method":"aaa/findById","params":[{"id":"1"}]}

问题1
spring boot 默认起一个端口 可以jsonRpc和http用一个端口
但是jsonRpc一般是 内网服务之间调用 所以端口号不能暴露出去 所以要给jsonRpc单独起一个端口
php 是jsonRpc 一个端口 http 一个端口

问题2
php 在请求的时候 会拼接 服务名 “method”:“服务名前部分/findById” 他会根据nacos上的 Service 取前部分 好像去不掉
而且 “服务名前部分” 如果前半部分是两个以上单词组成 并且是小驼峰 比如 testRpc php就会把小驼峰 变成下划线 test_rpc
java这边也很难受
服务名 可以理解为springboot配置文件中 application.name=xxx 这个

问题3
php jsonRpc传输数据 php调用php 可以"params":{“id”:“1”} 这样
但是 如果php调用java 必须是数组 “params”:[{“id”:“1”}] 外层必须包一个 []

问题4 这个问题是最烦人
java 这边路径有个前缀路径 @JsonRpcService(“bookRpcService”) 而且这个参数是必填的
php这边没有地方定义

问题5 是问题4的解释
php 是 哪个类需要跨服务调用 就把哪个类 注册到 nacos里
java 是整个服务 注册到 nacos
所以 php 不要路径前缀 可以直接找到 调用类
而 java 是整个服务 没有前缀路径 就不知道 调用哪个类

实际把上面的5个问题解决了 ,他们相互调用就ok了
先说依赖
php

composer require hyperf/json-rpc
composer require hyperf/rpc-server
composer require hyperf/rpc-client

java

       <!--        feign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
              <!--     jsonrpc4j-->
         <dependency>
            <groupId>com.github.briandilley.jsonrpc4j</groupId>
            <artifactId>jsonrpc4j</artifactId>
            <version>1.5.3</version>
        </dependency>

一 .java 调用 php
比较顺利 直接使用feign 即可 他默认支持 feign 的使用 就不说了 直接上代码

定义 跨服务接口

package com.xxx.init.feign;



import com.xxx.init.out.R;
import com.xxx.api.request.JsonRpcRequest;
import com.xxx.api.request.feign.publics.AreaRequest;
import com.xxx.api.response.JsonRpcResponse;
import com.xxx.init.feign.fallback.publics.AreaFeignFallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

/**
 * User:Json
 * Date: 2024/3/26
 **/
@FeignClient(name = "AreaService",fallbackFactory = AreaFeignFallbackFactory.class)
public interface AreaFeign {
    //定义 php的这个 area/getArea 这个地址名  是服务名 AreaService 取前部分 area/php那边定义的方法名
    // 如果前半部分 是两个以上单词组成 比如 testJsonService  取前部分  test_json/php那边定义的方法名
    // AreaService 这个服务名命名规则 hyperf 文档里有 详细说明
     String getAreaUrl="area/getArea";
    //地区查找
    @RequestMapping
    public ResponseEntity<JsonRpcResponse<R>> getArea(@RequestBody JsonRpcRequest<AreaRequest> param);



}


定义熔断器

package com.xxx.init.feign.fallback.publics;


import com.xxx.init.out.R;
import com.xxx.api.request.feign.publics.AreaRequest;


import com.xxx.api.response.JsonRpcResponse;
import com.xxx.init.feign.AreaFeign;

import com.xxx.api.request.JsonRpcRequest;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;


import java.util.HashMap;
import java.util.Map;

/**
 * User:Json
 * Date: 2024/3/26
 **/
@Slf4j
@Component
public class AreaFeignFallbackFactory implements FallbackFactory<AreaFeign> {


    @Override
    public AreaFeign create(Throwable cause) {
        return new AreaFeign() {
            @Override
            public ResponseEntity<JsonRpcResponse<R>> getArea(@RequestBody JsonRpcRequest<AreaRequest> param) {
                // 如果远程请求异常  就会走这里
                String msg = cause.getMessage();
                if (StringUtils.isEmpty(msg)) {
                    msg = "跨AreaService服务 【getArea】 无响应,没有返回值";
                }
                log.error("【跨AreaService服务调用异常】" + msg, cause);
                HashMap<String, Object> errorMap = new HashMap<>();
                errorMap.put("code", HttpStatus.GATEWAY_TIMEOUT.value());
                errorMap.put("message", msg);
                errorMap.put("data", null);
                JsonRpcResponse<Map<String, Object>> jsonRpcResponse = new JsonRpcResponse(new HashMap(), errorMap);
                return new ResponseEntity(jsonRpcResponse, HttpStatus.GATEWAY_TIMEOUT);
            }
        };


    }
}


定义jsonRpc请求实体类

import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.UUID;

/**
 * User:Json
 * Date: 2024/3/27
 **/
@Data
@NoArgsConstructor
public class JsonRpcRequest<T> {
    private String jsonrpc="2.0";
    private String id= UUID.randomUUID().toString();
    private String method;
    private T params;

    public JsonRpcRequest(String method,T params){
        this.method=method;
        this.params=params;
    }
}

定义jsonRpc 相应实体类

import lombok.Data;

import java.util.HashMap;


/**
 * User:Json
 * Date: 2024/3/27
 **/
@Data
public class JsonRpcResponse<T> {
    private String jsonrpc;
    private String id;
    private T result;
    private HashMap<String,Object> error;

    public  JsonRpcResponse(T result,HashMap<String,Object> error){
        this.result=result;
        this.error=error;
    }

}

R 的实体类 就不说了 就是返回前端的 数据结构 比如 状态码 data 提示语 那些

调用

   @Resource
    private AreaFeign areaFeign;
    
    public Map getArea(Long areaCode) {
        JsonRpcRequest<AreaRequest> multiplier = new JsonRpcRequest<>(areaFeign.getAreaUrl, new AreaRequest(areaCode));
        ResponseEntity<JsonRpcResponse<R>> hashMapResponseEntity= areaFeign.getArea(multiplier);
        JsonRpcResponse<R> body = hashMapResponseEntity.getBody();
        if(ObjectUtils.isEmpty(body.getResult())){
            log.error("AreaService中【getArea】接口:返回值:====>"+JSONObject.toJSONString(body));
           throw new xxxRuntimeException(body.getError().get("message").toString());
        }else{
            return(Map) body.getResult().getData();
        }
    }

这样 java 调用php 就好了
我觉得实际还是http请求 只是把 请求体 和响应体 给变成 jsonRpc的数据格式 就行了
主要要注意
这个地址的定义 如果定义错了 老是 找不到方法 我纠结了好久 才发现问题
不清楚 去打印一下 php那边的 请求格式就能看出来

String getAreaUrl="area/getArea";

二 . php 调用 java

上面的5个问题 主要就是php调用java 出现的 这里我就不说 原理了
我是看了好久 hyperf框架底层调用 和 java这边 com.github.briandilley.jsonrpc4j 这个依赖 底层调用 发现的问题
我直接说解决方案
php那边需要采用 jsonRpc方式调用 所以 java这边要起一个jsonRpc的端口来共 php调用
我也想过直接调用控制器 应该也可以 但是 控制器走的 http 接收到的数据 还要自己 转换 很麻烦

com.github.briandilley.jsonrpc4j 这个依赖会帮我们直接转好 到方法里 直接取参数用即可

定义java这边 jsonRpc服务端接口

import com.googlecode.jsonrpc4j.JsonRpcMethod;
import com.googlecode.jsonrpc4j.JsonRpcService;

import java.util.Map;


/**
 * User:Json
 * Date: 2024/5/7
 **/
 // 最烦的就是这个前缀 也是问题4 和 5  
// php那边请求的时候不能拼这个前缀 
// java这边这个前缀是必填 在网上的方案 改java这边源码 我尝试改过变动太大 不合适
//  那这个问题咋解决 主要是   靠   @JsonRpcMethod注解 命名规则 加 拦截器 重定向解决的 
//  这里就先说一下啊 @JsonRpcService("bookRpcService")   xxxRpcService  这样起名 拦截器里会用
// 下面有使用方案  因为我拦截器里 截取字符串后 会多一个/ 所以这里 就多了一个 / 如果不想写 就在拦截器里 多截取一位 
@JsonRpcService("/bookRpcService")
public interface BookRpcService {

     //这个方法名 也有规则
    // 如果java和java 用这个依赖 jsonRpc调用 不需要使用这个注解 因为他会直接调用找到方法
    // 但是php 调用java 就必须用 JsonRpcMethod 这个注解
    // /iotjava/bookRpcServiceFindById  这个的写法 解释一下 开头必须要加 /  php那边会拼一个
    //  1.java这边服务名 比如java 服务名是 iotjavaService 取 iotjava 这部分 因为php那边是这样的 上面定义feign 说过
    //  2. @JsonRpcService("bookRpcService")  bookRpcService 取这个注解的全称 bookRpcService
    //  3. 方法名 findById
    // 所以这个/iotjava/bookRpcServiceFindById 路径 最终的解释就是
    // /java这边服务名前部分/@JsonRpcService这个注解里的全程+方法名
    // 这样起名 拦截器里会用
    
    @JsonRpcMethod("/iotjava/bookRpcServiceFindById")
    public Book findById(Map<String,String> id);


}

实现这个接口

import com.googlecode.jsonrpc4j.spring.AutoJsonRpcServiceImpl;
import org.springframework.stereotype.Service;
import java.util.Map;


/**
 * User:Json
 * Date: 2024/5/7
 **/
@AutoJsonRpcServiceImpl
@Service
public class BookRpcServiceImpl implements BookRpcService{



    @Override
    public Book findById(Map<String,String> id){
        Book book = new Book();
        book.setId(id.get("id"));
        book.setName("JSON-R1111PC");
        book.setPrice(99.9);
        return book;
    }


}


定义jsonRpc服务端配置文件

import com.googlecode.jsonrpc4j.spring.AutoJsonRpcServiceImplExporter;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;



/**
 * User:Json
 * Date: 2024/5/7
 **/
@Configuration
public class RpcConfiguration {



    @Bean
    public AutoJsonRpcServiceImplExporter rpcServiceImplExporter(){
        return new AutoJsonRpcServiceImplExporter();
    }
}


再定义一个拦截器 这个拦截器就是为了解决 问题4 和 5

import com.alibaba.fastjson.JSONObject;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;


/**
 * User:Json
 * Date: 2024/5/13
 **/
@Slf4j
public class JsonRpcInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取请求路径
        String requestURI = request.getRequestURI();
        // 如果请求路径为 为 /  就检查是不是JsonRpc请求
        if ("/".equals(requestURI)) {
            JSONObject postJson = getPostJson(request);
            String newURI = "";
            if (postJson != null && postJson.containsKey("method")) {
                String methodName = postJson.getString("method");
                int secondIndex = methodName.indexOf("/", methodName.indexOf("/") + 1);
                if (secondIndex >= 0 && methodName.contains("RpcService")) {
                    int endIndex = methodName.indexOf("RpcService") + "RpcService".length();
                    newURI = methodName.substring(secondIndex, endIndex);
                }
            }
            if (StringUtils.isEmpty(newURI)) {
                newURI = "/error";
            }
            log.info("jsonRpc请求地址为:"+newURI);
            log.info("jsonRpc请求参数为:"+postJson);
            // 重定向到新的路径
            request.getRequestDispatcher(newURI).forward(request, response);
            return false;
        }
        return true; // 其他情况继续执行请求
    }

    // 获取 POST 请求的 JSON 字符串参数
    private JSONObject getPostJson(HttpServletRequest request) throws IOException {
        StringBuilder sb = new StringBuilder();
        BufferedReader reader = request.getReader();
        try {
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } finally {
            if (reader != null) {
                reader.close();
            }
        }

        return JSONObject.parseObject(sb.toString());
    }


    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {


    }


}

这样 java这边的服务端就定义好了
如果项目里 还有别的拦截器 可以在别的拦截器里判断 如果是rpc 请求 就 返回true
比如你拦截器里 做了token的验证 但是 服务与服务之间调用 jsonRpc 没有定义token
就可以在别的拦截器里定义

if(handler instanceof JsonServiceExporter)  return true; 

下面再说一下上面的问题解决方案
1. 端口号问题
定义两个端口

server:
  port: 10632 #内网端口号 jsonRpc  把springboot 带的端口配置作为 jsonRpc 端口 因为nacos默认取的这个端口
external:
  port: 10532  #外网端口号  把自定义的端口号 作为 http 端口

定义一个配置文件 spingboot 默认内置tomcat 如果使用的别的 web服务 配置文件改别的就行了

    @Value("${external.port}")
    private Integer portInternal;


    @Bean
    public ServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
        tomcat.addAdditionalTomcatConnectors(createStandardConnector());
        return tomcat;
    }

    //给 tomcat 再设置一个端口
    private Connector createStandardConnector() {
        Connector connector = new Connector(Http11NioProtocol.class.getName());
        connector.setPort(portInternal);
        return connector;
    }

服务名的定义规则:

spring:
  application:
    #  包命名规范一般都小写  所以这里服务名 前半部分为小写
    #  而且需要跟 php使用jsonRpc跨服务请求 php那边的服务命名规则 是 xxxService 这种 所以遵顼php那边服务命名规则
    #  如果是两个以上的单词 也要 全部小写  不小写的话 php那边使用jsonRpc跨服务调用 method属性里的值  会把小驼峰转成下划线
    #  php如果把小驼峰转成下划线  那java这边 在接收 也要转换小写
    #  所以为了不转来转去  所以这里服务名 前半部分 全部小写 最省事
    name: iotjavaService

2. 问题2

在定义上面的java jsonRpc服务端 已经解决 声明规则 在上面 已经说过

    @JsonRpcMethod("/iotjava/bookRpcServiceFindById")  

3. 问题3
php调用的时候 多个 [] 就行了
在这里插入图片描述
4. 问题4和 问题5 上面定义java jsonRpc服务端 已经解决

php这边调用


use Bailing\Helper\ApiHelper;
use Hyperf\RpcClient\AbstractServiceClient;
class SkeletonC extends AbstractServiceClient implements SkeletonIc
{
    /**
     * 定义对应服务提供者的服务名称
     */
    protected string $serviceName = 'iotjavaService';

    /**
     * 定义对应服务提供者的服务协议
     */
    protected string $protocol = 'jsonrpc-http';
    

    public function bookRpcServiceFindById(string $id): array
    {
        $arr[]=compact('id' );
        return $this->__request(__FUNCTION__, $arr);
    }

}

以上就全部解决了 可以php java启动测试一下

php那边测试 打印 可以分析一下源码 有兴趣可以去试试
下面的截图 都是 这两个依赖里的 源码分析

hyperf/json-rpc

hyperf/rpc-client

文档推荐
在这里插入图片描述
打印位置1
在这里插入图片描述
打印位置2
在这里插入图片描述
打印位置3
在这里插入图片描述
java 依赖 源码地址
https://github.com/briandilley/jsonrpc4j

java源码这边核心是这个类 类里主要是收集注解 然后放到springboot里
在这里插入图片描述

这个类 是 请求服务端时候会触发 的类
在这里插入图片描述

以上就是 全部解决方案 如果你发现了更好的解决方案 可以留言 相互学习一下

JsonRPC 2.0 Client and Server ============================= 轻量级 Json-RPC 2.0 客户端和服务端的php扩展,基于 multi_curl epoll的并发客户端,依据[jsonrpc](http://www.jsonrpc.org/)协议规范。 服务端: $server = new Jsonrpc_Server(); // style one function variable $add1 = function($a, $b){     return $a   $b; }; $server->register('addition1', $add1); // style two function string function add2($a, $b){   return $a   $b; } $server->register('addition2', 'add2'); // style three function closure $server->register('addition3', function ($a, $b) {     return $a   $b; }); //style four class method string class A  {   static public function add($a, $b)   {     return $a   $b;   } } $server->register('addition4', 'A::add'); echo $server->execute(); //output >>> //{"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"Parse error"}} 客户端: $client = new Jsonrpc_Client(1); $client->call('http://localhost/server.php', 'addition1', array(3,5)); $client->call('http://localhost/server.php', 'addition2', array(10,20)); $client->call('http://localhost/server.php', 'addition3', array(2,8)); $client->call('http://localhost/server.php', 'addition4', array(6,15)); /* ... */ $result = $client->execute(); var_dump($result); //output >>> /* array(2) {   [0]=>   array(3) {     ["jsonrpc"]=>     string(3) "2.0"     ["id"]=>     int(110507766)     ["result"]=>     int(8)   }   [1]=>   array(3) {     ["jsonrpc"]=>     string(3) "2.0"     ["id"]=>     int(1559316299)     ["result"]=>     int(30)   }   ... } */
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Json____

您的鼓励是我创作的动力~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值