基于SpringBoot實(shí)現(xiàn)REST?API與RPC調(diào)用的統(tǒng)一封裝
一、為何需要統(tǒng)一封裝?
在討論統(tǒng)一封裝之前,我們先看看 REST 和 RPC 各自的適用場(chǎng)景。
REST API 基于 HTTP 協(xié)議,采用 JSON 作為數(shù)據(jù)交換格式,可讀性好且跨語言,非常適合對(duì)外提供服務(wù)。
RPC(如 Dubbo、gRPC)采用二進(jìn)制協(xié)議(如 Protobuf),序列化效率高、網(wǎng)絡(luò)開銷小,適合內(nèi)部微服務(wù)間的高頻調(diào)用。
實(shí)際項(xiàng)目中,不同服務(wù)可能提供了不同的通信協(xié)議,帶來服務(wù)間調(diào)用方式的不一致,帶來編碼及后續(xù)維護(hù)的復(fù)雜度。
二、設(shè)計(jì)思路:基于外觀模式的統(tǒng)一調(diào)用層
解決這個(gè)問題的關(guān)鍵是引入 外觀模式(Facade Pattern) ,通過一個(gè)統(tǒng)一的外觀類封裝所有調(diào)用細(xì)節(jié)。
同時(shí)結(jié)合適配器模式和策略模式,實(shí)現(xiàn)不同協(xié)議的無縫切換。
2.1 核心設(shè)計(jì)
整個(gè)設(shè)計(jì)分為三層:
統(tǒng)一接口層:定義通用調(diào)用接口,屏蔽底層差異 協(xié)議適配層:實(shí)現(xiàn) REST 和 RPC 的具體調(diào)用邏輯 業(yè)務(wù)邏輯層:業(yè)務(wù)服務(wù)實(shí)現(xiàn),完全不用關(guān)心調(diào)用方式
2.2 關(guān)鍵設(shè)計(jì)模式
外觀模式:提供統(tǒng)一入口 UnifiedServiceClient,封裝所有調(diào)用細(xì)節(jié) 適配器模式:將 RestTemplate 和 DubboReference 適配為統(tǒng)一接口 策略模式:根據(jù)配置動(dòng)態(tài)選擇調(diào)用方式(REST 或 RPC)
三、實(shí)現(xiàn)步驟:從統(tǒng)一響應(yīng)到協(xié)議適配
3.1 統(tǒng)一響應(yīng)體設(shè)計(jì)
首先要解決的是返回格式不一致問題。我們定義了統(tǒng)一的響應(yīng)體 ApiResponse
@Data
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ApiResponse<T> implements Serializable {
private String code; // 狀態(tài)碼
private String message; // 消息提示
private T data; // 業(yè)務(wù)數(shù)據(jù)
private long timestamp; // 時(shí)間戳
// 成功響應(yīng)
public static <T> ApiResponse<T> success(T data) {
return ApiResponse.<T>builder()
.code("200")
.message("success")
.data(data)
.timestamp(System.currentTimeMillis())
.build();
}
// 失敗響應(yīng)
public static <T> ApiResponse<T> fail(String code, String message) {
return ApiResponse.<T>builder()
.code(code)
.message(message)
.timestamp(System.currentTimeMillis())
.build();
}
}
對(duì)于 REST 接口,通過 @RestControllerAdvice 實(shí)現(xiàn)自動(dòng)封裝
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true; // 對(duì)所有響應(yīng)生效
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof ApiResponse) {
return body; // 已封裝的直接返回
}
return ApiResponse.success(body); // 未封裝的自動(dòng)包裝
}
}
3.2 統(tǒng)一異常處理
異常處理同樣需要統(tǒng)一。對(duì)于 REST 接口,使用 @ControllerAdvice
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ApiResponse<Void> handleBusinessException(BusinessException e) {
return ApiResponse.fail(e.getCode(), e.getMessage());
}
@ExceptionHandler(Exception.class)
public ApiResponse<Void> handleException(Exception e) {
log.error("系統(tǒng)異常", e);
return ApiResponse.fail("500", "系統(tǒng)內(nèi)部錯(cuò)誤");
}
}
對(duì)于 Dubbo RPC,通過自定義過濾器實(shí)現(xiàn)異常轉(zhuǎn)換:
package com.example.unified;
import com.alibaba.dubbo.common.Constants;
import com.alibaba.dubbo.common.extension.Activate;
import com.example.unified.exception.BusinessException;
import org.apache.dubbo.rpc.*;
import java.util.function.BiConsumer;
@Activate(group = Constants.PROVIDER)
public class DubboExceptionFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
try {
AsyncRpcResult result = (AsyncRpcResult )invoker.invoke(invocation);
if (result.hasException()) {
Throwable exception = result.getException();
if (exception instanceof BusinessException) {
BusinessException e = (BusinessException) exception;
return new AppResponse (ApiResponse.fail(e.getCode(), e.getMessage()));
}
}
return result.whenCompleteWithContext(new BiConsumer<Result, Throwable>() {
@Override
public void accept(Result result, Throwable throwable) {
result.setValue(ApiResponse.success(result.getValue()));
}
});
} catch (Exception e) {
return new AppResponse (ApiResponse.fail("500", "RPC調(diào)用異常"));
}
}
}
3.3 協(xié)議適配層實(shí)現(xiàn)
定義統(tǒng)一調(diào)用接口 ServiceInvoker:
package com.example.unified.invoker;
import cn.hutool.core.lang.TypeReference;
import com.example.unified.ApiResponse;
public interface ServiceInvoker {
<T> ApiResponse<T> invoke(String serviceName, String method, Object param, TypeReference<ApiResponse<T>> resultType);
}
然后分別實(shí)現(xiàn) REST 和 RPC 適配器:
REST 適配器
package com.example.unified.invoker;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.json.JSONUtil;
import com.example.unified.ApiResponse;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@Component
public class RestServiceInvoker implements ServiceInvoker {
private final RestTemplate restTemplate;
private Environment environment;
public RestServiceInvoker(RestTemplate restTemplate,Environment environment) {
this.restTemplate = restTemplate;
this.environment = environment;
}
@Override
public <T> ApiResponse<T> invoke(String serviceName, String method, Object param, TypeReference<ApiResponse<T>> resultType) {
String serviceUrl = environment.getProperty("service.direct-url." + serviceName);
String url = serviceUrl + "/" + method;
HttpEntity request = new HttpEntity<>(param);
String result = restTemplate.postForObject(url, request, String.class);
return JSONUtil.toBean(result, resultType, true);
}
}
Dubbo 適配器
package com.example.unified.invoker;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.example.unified.ApiResponse;
import org.apache.dubbo.config.ReferenceConfig;
import org.apache.dubbo.config.RegistryConfig;
import org.apache.dubbo.config.utils.SimpleReferenceCache;
import org.apache.dubbo.rpc.service.GenericService;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Component
public class DubboServiceInvoker implements ServiceInvoker {
private final SimpleReferenceCache referenceCache;
private final Environment environment;
public DubboServiceInvoker(SimpleReferenceCache referenceCache, Environment environment) {
this.referenceCache = referenceCache;
this.environment = environment;
}
@Override
public <T> ApiResponse<T> invoke(String serviceName, String method, Object param,TypeReference<ApiResponse<T>> resultType) {
ReferenceConfig<GenericService> reference = new ReferenceConfig<>();
String interfaceName = environment.getProperty("dubbo.reference." + serviceName + ".interfaceName");
reference.setInterface(interfaceName);
reference.setGeneric("true");
reference.setRegistry(new RegistryConfig("N/A"));
reference.setVersion("1.0.0");
// 從配置文件讀取直連地址(優(yōu)先級(jí):代碼 > 配置文件)
String directUrl = environment.getProperty("dubbo.reference." + serviceName + ".url");
if (StrUtil.isNotEmpty(directUrl)) {
reference.setUrl(directUrl); // 設(shè)置直連地址,覆蓋注冊(cè)中心發(fā)現(xiàn)
}
GenericService service = referenceCache.get(reference);
Object[] params = {param};
Object result = service.$invoke(method, getParamTypes(params), params);
JSONObject jsonObject = new JSONObject(result);
ApiResponse<T> response = JSONUtil.toBean(jsonObject, resultType,true);
return response;
}
private String[] getParamTypes(Object[] params) {
return Arrays.stream(params).map(p -> p.getClass().getName()).toArray(String[]::new);
}
}
3.4 外觀類與策略選擇
最后實(shí)現(xiàn)外觀類 UnifiedServiceClient:
package com.example.unified;
import cn.hutool.core.lang.TypeReference;
import com.example.unified.config.ServiceConfig;
import com.example.unified.invoker.ServiceInvoker;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
@Component
public class UnifiedServiceClient {
private final Map<String, ServiceInvoker> invokerMap;
private final ServiceConfig serviceConfig;
public UnifiedServiceClient(List<ServiceInvoker> invokers, ServiceConfig serviceConfig) {
this.invokerMap = invokers.stream()
.collect(Collectors.toMap(invoker -> invoker.getClass().getSimpleName(), Function.identity()));
this.serviceConfig = serviceConfig;
}
public <T> ApiResponse<T> call(String serviceName, String method, Object param, TypeReference<ApiResponse<T>> resultType) {
// 根據(jù)配置選擇調(diào)用方式
String protocol = serviceConfig.getProtocol(serviceName);
ServiceInvoker invoker = protocol.equals("rpc") ?
invokerMap.get("DubboServiceInvoker") :
invokerMap.get("RestServiceInvoker");
return invoker.invoke(serviceName, method, param,resultType);
}
}
服務(wù)調(diào)用方式通過配置文件指定:
service:
direct-url: # 直連地址配置
user-service: http://localhost:8080/user # 訂單服務(wù)REST地址
config:
user-service: rest # 用戶服務(wù)用rest調(diào)用
order-service: rpc # 訂單服務(wù)用RPC調(diào)用
# Dubbo 配置(若使用 Dubbo RPC)
dubbo:
application:
name: unified-client-demo # 當(dāng)前應(yīng)用名
# serialize-check-status: DISABLE
qos-enable: false
registry:
address: N/A
reference:
# 為指定服務(wù)配置直連地址(無需注冊(cè)中心)
order-service:
interfaceName: com.example.unified.service.OrderService # 服務(wù)接口名稱
url: dubbo://192.168.17.1:20880 # 格式:dubbo://IP:端口
protocol:
name: dubbo # RPC 協(xié)議名稱
port: 20880 # 端口
四、使用案例
package com.example.unified.controller;
import cn.hutool.core.lang.TypeReference;
import com.example.unified.ApiResponse;
import com.example.unified.UnifiedServiceClient;
import com.example.unified.dto.OrderDTO;
import com.example.unified.dto.UserDTO;
import com.example.unified.service.OrderService;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class DemoController {
@Autowired
private UnifiedServiceClient serviceClient;
@RequestMapping("/user")
public ApiResponse<UserDTO> getUser(@RequestBody UserDTO qryUserDTO) {
ApiResponse<UserDTO> response = serviceClient.call("user-service", "getUser", qryUserDTO, new TypeReference<ApiResponse<UserDTO>>() {});
return response;
}
@RequestMapping("/order")
public ApiResponse<OrderDTO> getOrder(@RequestBody OrderDTO qryOrderDTO) {
ApiResponse<OrderDTO> response = serviceClient.call("order-service", "getOrder", qryOrderDTO, new TypeReference<ApiResponse<OrderDTO>>() {});
String status = response.getData().getStatus();
System.err.println("status:" + status);
return response;
}
}
五、總結(jié)
通過外觀模式 + 適配器模式 + 策略模式的組合,實(shí)現(xiàn)了 REST API 與 RPC 調(diào)用的統(tǒng)一封裝。
以上就是基于SpringBoot實(shí)現(xiàn)REST API與RPC調(diào)用的統(tǒng)一封裝的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot REST API與RPC統(tǒng)一封裝的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java基本知識(shí)點(diǎn)之變量和數(shù)據(jù)類型
這篇文章主要給大家介紹了關(guān)于Java基本知識(shí)點(diǎn)之變量和數(shù)據(jù)類型的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04
解決springSecurity 使用默認(rèn)登陸界面登錄后無法跳轉(zhuǎn)問題
這篇文章主要介紹了解決springSecurity 使用默認(rèn)登陸界面登錄后無法跳轉(zhuǎn)問題,項(xiàng)目環(huán)境springboot下使用springSecurity 版本2.7.8,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2023-12-12
Spring Cloud Ribbon實(shí)現(xiàn)客戶端負(fù)載均衡的方法
本篇文章主要介紹了Spring Cloud Ribbon實(shí)現(xiàn)客戶端負(fù)載均衡的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-05-05
全面解析JPA?倉庫repository中的findAll()方法
這篇文章主要介紹了全面解析JPA?倉庫repository中的findAll()方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02
Mybatis -如何處理clob類型數(shù)據(jù)
這篇文章主要介紹了Mybatis 如何處理clob類型數(shù)據(jù)的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06
mybatis的insert語句插入數(shù)據(jù)時(shí)的返回值的實(shí)現(xiàn)
這篇文章主要介紹了mybatis的insert語句插入數(shù)據(jù)時(shí)的返回值的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10
springboot打包實(shí)現(xiàn)項(xiàng)目JAR包和依賴JAR包分離
這篇文章主要介紹了springboot打包實(shí)現(xiàn)項(xiàng)目JAR包和依賴JAR包分離,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02

