基于 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 调用的统一封装。

相关推荐
一线大码10 分钟前
Gradle 高级篇之构建多模块项目的方法
spring boot·gradle·intellij idea
27669582921 小时前
tiktok 弹幕 逆向分析
java·python·tiktok·tiktok弹幕·tiktok弹幕逆向分析·a-bogus·x-gnarly
用户40315986396631 小时前
多窗口事件分发系统
java·算法
用户40315986396631 小时前
ARP 缓存与报文转发模拟
java·算法
小林ixn1 小时前
大一新手小白跟黑马学习的第一个图形化项目:拼图小游戏(java)
java
nbsaas-boot1 小时前
Go语言生态成熟度分析:为何Go还无法像Java那样实现注解式框架?
java·开发语言·golang
MarkGosling1 小时前
【开源项目】网络诊断告别命令行!NetSonar:开源多协议网络诊断利器
运维·后端·自动化运维
hi0_61 小时前
03 数组 VS 链表
java·数据结构·c++·笔记·算法·链表
congvee2 小时前
springboot 学习第1期 - 创建工程
spring boot
Codebee2 小时前
OneCode3.0 VFS分布式文件管理API速查手册
后端·架构·开源