再見 Feign!Spring Boot + JSON-RPC遠(yuǎn)程調(diào)用新選擇
環(huán)境:SpringBoot3.4.2
1. 簡(jiǎn)介
什么是JSON-RPC協(xié)議?
JSON-RPC 是一種輕量級(jí)的遠(yuǎn)程過程調(diào)用(Remote Procedure Call,RPC)協(xié)議,它允許你在不同的系統(tǒng)之間通過網(wǎng)絡(luò)進(jìn)行數(shù)據(jù)交換和調(diào)用函數(shù)。JSON-RPC 使用 JSON(JavaScript Object Notation)作為數(shù)據(jù)格式,這使得它非常適合于各種編程語言和平臺(tái)之間的通信。
目前JSON-RPC的版本是2.0,該版本是目前廣泛使用的版本,它增加了錯(cuò)誤處理、批處理請(qǐng)求(batch requests)、通知(notifications)等特性。
JSON-RPC 2.0結(jié)構(gòu)
請(qǐng)求,通常包含以下幾個(gè)字段:
- jsonrpc: String,版本號(hào),通常為 "2.0"
- method: String,要調(diào)用的方法的名稱
- params: 可選,參數(shù)列表或?qū)ο?,傳遞給方法的參數(shù)
- id: 可選,一個(gè)字符串、數(shù)字或 null。用于唯一標(biāo)識(shí)請(qǐng)求,以便響應(yīng)能夠回傳給正確
響應(yīng),通常包含以下幾個(gè)字段:
- jsonrpc: 字符串,版本號(hào),通常為 "2.0"
- result: 請(qǐng)求成功時(shí)的返回值
- error: 請(qǐng)求失敗時(shí)的錯(cuò)誤對(duì)象
- id: 與請(qǐng)求中的 ID 相匹配
說明:result與error,最終只會(huì)有一個(gè)返回。
JSON-RPC不同的編程語言都有對(duì)應(yīng)的實(shí)現(xiàn),在java中比較流行的則是jsonrpc4j,而本篇文章將基于該開源庫進(jìn)行詳細(xì)的介紹。
什么是jsonrpc4j?
jsonrpc4j 使用 Jackson 庫將 Java 對(duì)象與 JSON 對(duì)象進(jìn)行相互轉(zhuǎn)換(以及其他與 JSON-RPC 相關(guān)的功能)。包括以下功能:
- 流式服務(wù)器(InputStream / OutputStream)
- HTTP 服務(wù)器(HttpServletRequest / HttpServletResponse)
- Portlet 服務(wù)器(ResourceRequest / ResourceResponse)
- Socket 服務(wù)器(StreamServer)
- 與 Spring 框架集成
- 流式客戶端
- HTTP 客戶端
- 動(dòng)態(tài)客戶端代理
- 注解支持
- 自定義錯(cuò)誤解析
- 復(fù)合服務(wù)
2.實(shí)戰(zhàn)案例
2.1 依賴管理&接口定義
<dependency>
<groupId>com.github.briandilley.jsonrpc4j</groupId>
<artifactId>jsonrpc4j</artifactId>
<version>1.7</version>
</dependency>
目前最新版本為1.7。
接口定義
public interface UserService {
User createUser(String userName, String firstName, String password);
User createUser(String userName, String password);
User findUserByUserName(String userName);
int getUserCount();
}
// 接口實(shí)現(xiàn)
@Service
@Primary
public class UserServiceImpl implements UserService {
private static final List<User> USERS = new ArrayList<>();
public User createUser(String userName, String firstName, String password) {
User user = new User(userName, firstName, password);
USERS.add(user);
return user;
}
public User createUser(String userName, String password) {
return this.createUser(userName, null, password);
}
public User findUserByUserName(String userName) {
System.err.println("admin".equals(userName) ? 1 / 0 : "success") ;
return USERS.stream().filter(user -> user.userName().equals(userName)).findFirst().orElse(null);
}
public int getUserCount() {
return USERS.size();
}
}
接下來,我們將從以下方面對(duì) jsonrpc4j 進(jìn)行詳細(xì)說明與使用示例解析。
- RPC Server接口暴露的2種方式
- RPC Client調(diào)用的3種方式
- RPC Server錯(cuò)誤處理
- RPC Server 流式(Socket)服務(wù)
2.2 RPC Server接口暴露
定義方式1:使用JsonServiceExporter
// 注意:這里的beanName以 "/" 開頭,最終會(huì)被BeanNameUrlHandlerMapping處理
@Bean("/us")
JsonServiceExporter exporter(UserService userService, ObjectMapper objectMapper) {
JsonServiceExporter exporter = new JsonServiceExporter() ;
// 暴露的接口
exporter.setServiceInterface(UserService.class) ;
// 這里非必須設(shè)置
exporter.setObjectMapper(objectMapper) ;
// 對(duì)應(yīng)的接口實(shí)現(xiàn)
exporter.setService(userService) ;
return exporter ;
}
通過此種方式,我們就完成了服務(wù)的暴露,任何支持 JSON-RPC 的客戶端均可訪問此服務(wù),接口地址:http://localhost:8080/us
定義方式2:使用注解自動(dòng)發(fā)現(xiàn)機(jī)制
修改上面的接口已經(jīng)實(shí)現(xiàn)類,添加如下的注解:
@JsonRpcService("/us")
public interface UserService {
}
@AutoJsonRpcServiceImpl
@Service
@Primary
public class UserServiceImpl implements UserService {
}
接下來,還需要定義如下的bean(啟動(dòng)掃描功能)。
@Bean
AutoJsonRpcServiceImplExporter autoExporter() {
return new AutoJsonRpcServiceImplExporter() ;
}
這里我們推薦使用注解的方式。
2.3 RPC Client調(diào)用
配置方式1:使用AutoJsonRpcClientProxyCreator
首先,確保接口上使用了@JsonRpcServer注解:
@JsonRpcService("/us")
public interface UserService {}
接下來,配置AutoJsonRpcClientProxyCreator:
@Bean
AutoJsonRpcClientProxyCreator proxyCreator() throws Exception {
AutoJsonRpcClientProxyCreator creator = new AutoJsonRpcClientProxyCreator() ;
// 該baseUrl會(huì)自動(dòng)拼接到@JsonRpcService的路徑前面; http://localhost:8080/us
creator.setBaseUrl(URI.create("http://localhost:8080").toURL()) ;
// 接口所在的包
creator.setScanPackage("com.pack.rpc.server") ;
return creator ;
}
配置方式2:使用JsonProxyFactoryBean
@Bean
JsonProxyFactoryBean userServiceProxy() {
JsonProxyFactoryBean proxy = new JsonProxyFactoryBean() ;
proxy.setServiceUrl("http://localhost:8080/us") ;
proxy.setServiceInterface(UserService.class);
return proxy ;
}
通過該FactoryBean,會(huì)自動(dòng)的為UserService接口創(chuàng)建代理。
配置方式3:使用JsonRpcHttpClient
上面2種方式都是依賴的Spring環(huán)境,下面我們還可以使用如下的2中方式創(chuàng)建客戶端:
JsonRpcHttpClient client = new JsonRpcHttpClient(URI.create("http://localhost:8080/us").toURL()) ;
User user = client.invoke("createUser", new Object[] { "Spring Boot3實(shí)戰(zhàn)案例200講", "Pack", "123456" }, User.class);
System.err.println(user) ;
// 也可以通過如下方式創(chuàng)建代理
JsonRpcHttpClient client = new JsonRpcHttpClient(URI.create("http://localhost:8080/us").toURL());
UserService userService = ProxyUtil.createClientProxy(
ClientTest.class.getClassLoader(),
UserService.class,
client) ;
User user = userService.createUser("Pack", "xg") ;
System.err.println(user) ;
2.4 測(cè)試
@RestController
@RequestMapping("/rpc")
public class RpcController {
private final UserService userService ;
public RpcController(UserService userService) {
this.userService = userService;
}
@GetMapping("/create")
public ResponseEntity<User> createUser(String userName, String firstName, String password) {
return ResponseEntity.ok(this.userService.createUser(userName, firstName, password)) ;
}
@GetMapping("/query")
public ResponseEntity<User> queryUser(String userName) {
return ResponseEntity.ok(this.userService.findUserByUserName(userName)) ;
}
}
運(yùn)行結(jié)果
圖片
圖片
成功調(diào)用。
2.5 異常處理
我們按照上面接口實(shí)現(xiàn),我們通過如下參數(shù)訪問查詢接口:
圖片
控制臺(tái)異常信息如下:
圖片
RPC Client接收到如上的異常數(shù)據(jù)。
我們可以通過如下方式自定義異常信息,在JSON-RPC 服務(wù)的接口上添加注解:
@JsonRpcErrors({
@JsonRpcError(exception = Exception.class, code = -1, message = "服務(wù)發(fā)生異常")
})
User findUserByUserName(String userName);
再次訪問上面的接口后,控制臺(tái)輸出:
圖片
2.6 流式(Socket)服務(wù)
我們可以通過Socket方式提供服務(wù),如下示例:
服務(wù)端:
JsonRpcServer server = new JsonRpcServer(new UserServiceImpl()) ;
int maxThreads = 50 ;
int port = 8080 ;
ServerSocket serverSocket = new ServerSocket(port) ;
StreamServer ss = new StreamServer(server, maxThreads, serverSocket) ;
ss.start() ;
客戶端:
Socket socket = new Socket(InetAddress.getLocalHost(), 8080) ;
OutputStream os = socket.getOutputStream() ;
Map<String, Object> data = Map.of("jsonrpc", "2.0", "method", "createUser",
"params", new Object[] {"Spring Boot3實(shí)戰(zhàn)案例200講", "Pack", "111111"}, "id", "s-0001") ;
os.write(new ObjectMapper().writeValueAsBytes(data)) ;
socket.shutdownOutput();
InputStream is = socket.getInputStream() ;
System.err.println(new String(is.readAllBytes())) ;
運(yùn)行結(jié)果
客戶端輸出: