行业知识
spring boot框架(java)和 hyperf框架(php)多语言开发微服务,nacos作为服务发现,实现php服务与java服务跨服务调用,实现方案【feign和jsonRpc配合使用】
项目微服务架构,采用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-rpccomposer require hyperf/rpc-servercomposer require hyperf/rpc-client
java
org.springframework.cloud spring-cloud-starter-openfeign com.github.briandilley.jsonrpc4j jsonrpc4j 1.5.3
一 .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 getArea(@RequestBody JsonRpcRequest 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@Componentpublic class AreaFeignFallbackFactory implements FallbackFactory { @Override public AreaFeign create(Throwable cause) { return new AreaFeign() { @Override public ResponseEntity getArea(@RequestBody JsonRpcRequest param) { // 如果远程请求异常 就会走这里 String msg = cause.getMessage(); if (StringUtils.isEmpty(msg)) { msg = "跨AreaService服务 【getArea】 无响应,没有返回值"; } log.error("【跨AreaService服务调用异常】" + msg, cause); HashMap errorMap = new HashMap(); errorMap.put("code", HttpStatus.GATEWAY_TIMEOUT.value()); errorMap.put("message", msg); errorMap.put("data", null); JsonRpcResponse 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@NoArgsConstructorpublic class JsonRpcRequest { 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 **/@Datapublic class JsonRpcResponse { private String jsonrpc; private String id; private T result; private HashMap error; public JsonRpcResponse(T result,HashMap error){ this.result=result; this.error=error; }} R 的实体类 就不说了 就是返回前端的 数据结构 比如 状态码 data 提示语 那些
调用
@Resource private AreaFeign areaFeign; public Map getArea(Long areaCode) { JsonRpcRequest multiplier = new JsonRpcRequest(areaFeign.getAreaUrl, new AreaRequest(areaCode)); ResponseEntity hashMapResponseEntity= areaFeign.getArea(multiplier); JsonRpcResponse 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 id);} 实现这个接口
import com.googlecode.jsonrpc4j.spring.AutoJsonRpcServiceImpl;import org.springframework.stereotype.Service;import java.util.Map;/** * User:Json * Date: 2024/5/7 **/@AutoJsonRpcServiceImpl@Servicepublic class BookRpcServiceImpl implements BookRpcService{ @Override public Book findById(Map 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 **/@Configurationpublic 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 **/@Slf4jpublic 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那边测试 打印 可以分析一下源码 有兴趣可以去试试
下面的截图 都是 这两个依赖里的 源码分析
hyperf/json-rpchyperf/rpc-client
文档推荐
打印位置1
打印位置2
打印位置3
java 依赖 源码地址
https://github.com/briandilley/jsonrpc4j
java源码这边核心是这个类 类里主要是收集注解 然后放到springboot里
这个类 是 请求服务端时候会触发 的类