基于 SpringBoot 的 REST API 与 RPC 调用的统一封装

一、为何需要统一封装?

在讨论统一封装之前,我们先看看 REST 和 RPC 各自的适用场景。

REST API 基于 HTTP 协议,采用 JSON 作为数据交换格式,可读性好且跨语言,非常适合对外提供服务。

RPC(如 Dubbo、gRPC)采用二进制协议(如 Protobuf),序列化效率高、网络开销小,适合内部微服务间的高频调用。

实际项目中,不同服务可能提供了不同的通信协议,带来服务间调用方式的不一致,带来编码及后续维护的复杂度。

二、设计思路:基于外观模式的统一调用层

解决这个问题的关键是引入 外观模式(Facade Pattern) ,通过一个统一的外观类封装所有调用细节。

同时结合适配器模式和策略模式,实现不同协议的无缝切换。

2.1 核心设计

整个设计分为三层:

统一接口层 :定义通用调用接口,屏蔽底层差异 协议适配层 :实现 REST 和 RPC 的具体调用逻辑 业务逻辑层:业务服务实现,完全不用关心调用方式

2.2 关键设计模式

外观模式 :提供统一入口 UnifiedServiceClient,封装所有调用细节 适配器模式 :将 RestTemplateDubboReference 适配为统一接口 策略模式:根据配置动态选择调用方式(REST 或 RPC)

三、实现步骤:从统一响应到协议适配

3.1 统一响应体设计

首先要解决的是返回格式不一致问题。我们定义了统一的响应体 ApiResponse

less 复制代码
@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 实现自动封装

typescript 复制代码
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true; // 对所有响应生效
    }

    @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); // 未封装的自动包装
    }
}

3.2 统一异常处理

异常处理同样需要统一。对于 REST 接口,使用 @ControllerAdvice

kotlin 复制代码
@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,通过自定义过滤器实现异常转换:

java 复制代码
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调用异常"));
        }
    }
}

3.3 协议适配层实现

定义统一调用接口 ServiceInvoker

typescript 复制代码
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 适配器

typescript 复制代码
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 适配器

ini 复制代码
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");
        // 从配置文件读取直连地址(优先级:代码 > 配置文件)
        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

java 复制代码
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);
    }
}

服务调用方式通过配置文件指定:

yaml 复制代码
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: DISABLE
    qos-enable: false
  registry:
    address: N/A
  reference:
    # 为指定服务配置直连地址(无需注册中心)
    order-service:
      interfaceName: com.example.unified.service.OrderService  # 服务接口名称
      url: dubbo://192.168.17.1:20880  # 格式:dubbo://IP:端口
  protocol:
    name: dubbo    # RPC 协议名称
    port: 20880    # 端口

四、使用案例

kotlin 复制代码
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;
    }
}

六、总结

通过外观模式 + 适配器模式 + 策略模式的组合,实现了 REST API 与 RPC 调用的统一封装。

相关推荐
mitt_3 分钟前
go语言变量
开发语言·后端·golang
无限大619 分钟前
二维数组搜索:从暴力地毯到二分神技的奇幻之旅
后端
hrrrrb1 小时前
【Spring Boot 快速入门】六、配置文件
java·spring boot·intellij-idea
bobz9651 小时前
最近玩了好多把 LOL
后端
Asu52021 小时前
思途Mybatis学习 0805
java·spring boot·学习·mybatis
爱欲无极1 小时前
基于Flask的微博话题多标签情感分析系统设计
后端·python·flask
cwkiller1 小时前
heapdump深度利用之信息泄露篇
后端
心勤则明2 小时前
JVM(Java虚拟机)运行时数据区
java·jvm·chrome
皮皮林5512 小时前
多账号统一登录(实现方案)
java
越来越无动于衷2 小时前
智慧社区(八)——社区人脸识别出入管理系统设计与实现
java·开发语言·spring boot·python·mysql