一、為何需要統一封裝?
在討論統一封裝之前,我們先看看 REST 和 RPC 各自的適用場景。
REST API 基于 HTTP 協議,采用 JSON 作為數據交換格式,可讀性好且跨語言,非常適合對外提供服務。
RPC(如 Dubbo、gRPC)采用二進制協議(如 Protobuf),序列化效率高、網絡開銷小,適合內部微服務間的高頻調用。
實際項目中,不同服務可能提供了不同的通信協議,帶來服務間調用方式的不一致,帶來編碼及后續維護的復雜度。
二、設計思路:基于外觀模式的統一調用層
解決這個問題的關鍵是引入 外觀模式(Facade Pattern) ,通過一個統一的外觀類封裝所有調用細節。
同時結合適配器模式和策略模式,實現不同協議的無縫切換。
2.1 核心設計
整個設計分為三層:
統一接口層:定義通用調用接口,屏蔽底層差異
協議適配層:實現 REST 和 RPC 的具體調用邏輯
業務邏輯層:業務服務實現,完全不用關心調用方式
2.2 關鍵設計模式
外觀模式:提供統一入口 UnifiedServiceClient
,封裝所有調用細節
適配器模式:將 RestTemplate
和 DubboReference
適配為統一接口
策略模式:根據配置動態選擇調用方式(REST 或 RPC)
三、實現步驟:從統一響應到協議適配
3.1 統一響應體設計
首先要解決的是返回格式不一致問題。我們定義了統一的響應體 ApiResponse
@Data
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ApiResponse<T> implements Serializable {private String code; // 狀態碼private String message; // 消息提示private T data; // 業務數據private long timestamp; // 時間戳// 成功響應public static <T> ApiResponse<T> success(T data) {return ApiResponse.<T>builder().code("200").message("success").data(data).timestamp(System.currentTimeMillis()).build();}// 失敗響應public static <T> ApiResponse<T> fail(String code, String message) {return ApiResponse.<T>builder().code(code).message(message).timestamp(System.currentTimeMillis()).build();}
}
對于 REST 接口,通過 @RestControllerAdvice
實現自動封裝
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {@Overridepublic boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {return true; // 對所有響應生效}@Overridepublic 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); // 未封裝的自動包裝}
}
3.2 統一異常處理
異常處理同樣需要統一。對于 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("系統異常", e);return ApiResponse.fail("500", "系統內部錯誤");}
}
對于 Dubbo RPC,通過自定義過濾器實現異常轉換:
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 {@Overridepublic 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>() {@Overridepublic void accept(Result result, Throwable throwable) {result.setValue(ApiResponse.success(result.getValue()));}});} catch (Exception e) {return new AppResponse (ApiResponse.fail("500", "RPC調用異常"));}}
}
3.3 協議適配層實現
定義統一調用接口 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);
}
然后分別實現 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;}@Overridepublic <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;}@Overridepublic <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");// 從配置文件讀取直連地址(優先級:代碼 > 配置文件)String directUrl = environment.getProperty("dubbo.reference." + serviceName + ".url");if (StrUtil.isNotEmpty(directUrl)) {reference.setUrl(directUrl); // 設置直連地址,覆蓋注冊中心發現}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 外觀類與策略選擇
最后實現外觀類 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) {// 根據配置選擇調用方式String protocol = serviceConfig.getProtocol(serviceName);ServiceInvoker invoker = protocol.equals("rpc") ? invokerMap.get("DubboServiceInvoker") : invokerMap.get("RestServiceInvoker");return invoker.invoke(serviceName, method, param,resultType);}
}
服務調用方式通過配置文件指定:
service:direct-url: # 直連地址配置user-service: http://localhost:8080/user # 訂單服務REST地址config:user-service: rest # 用戶服務用rest調用order-service: rpc # 訂單服務用RPC調用# Dubbo 配置(若使用 Dubbo RPC)
dubbo:application:name: unified-client-demo # 當前應用名
# serialize-check-status: DISABLEqos-enable: falseregistry:address: N/Areference:# 為指定服務配置直連地址(無需注冊中心)order-service:interfaceName: com.example.unified.service.OrderService # 服務接口名稱url: dubbo://192.168.17.1:20880 # 格式:dubbo://IP:端口protocol:name: dubbo # RPC 協議名稱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 {@Autowiredprivate 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;}
}
六、總結
通過外觀模式 + 適配器模式 + 策略模式的組合,實現了 REST API 與 RPC 調用的統一封裝。