项目微服务架构,采用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里
这个类 是 请求服务端时候会触发 的类
以上就是 全部解决方案 如果你发现了更好的解决方案 可以留言 相互学习一下