Spring Cloud OpenFeign集成Protocol Buffer

本文介绍如何定制OpenFeign的编解码器,使用ProtocolBuffer提高RPC调用效率。通过自定义配置类,集成ProtocolBuffer进行数据传输,提升微服务通信效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文作者张天,著有《Spring Cloud 微服务架构进阶》一书。

背景

 在之前的文章中,我们介绍过基于Spring Cloud微服务架构,其中,微服务实例之间的交互方式一般为RESTful HTTP请求或RPC调用。Spring Cloud已经为开发者提供了专门用于RESTful HTTP请求处理的OpenFeign组件,但是并没有相关的RPC调用组件。今天,我们就要定制OpenFeign的编解码器,使用Google的Protocol Buffer编码,让它拥有RPC调用的数据传输和转换效率高的优点。

 OpenFeign是一个声明式RESTful HTTP请求客户端,它使得编写Web服务客户端更加方便和快捷。它有较强的定制性,可以根据自己的需求来对它的各个方面进行定制,比如说编解码器,服务路由解析和负载均衡。

 而Protocol Buffer 是Google的一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前提供了 C++、Java、Python 三种语言的 API。

 OpenFeign默认使用HttpUrlConnection进行网络请求的发送; 相关实现代码在DefaultFeignLoadBalancedConfigurationClient.Default。而其使用的编解码器默认为jackson2,默认配置为HttpMessageConvertersAutoConfiguration

 Protocol Buffer的编解码效率要远高于jackson2,在微服务实例频频通信的场景下,使用Protocol Buffer编解码时会少占用系统资源,并且效率较高。具体详见这个对比对比各种序列化和反序列化框架的性能的文档,github.com/eishay/jvm-…

客户端集成Protocol Buffer

 开发人员可以使用自定义配置类对OpenFeign进行定制,提供OpenFeign所需要的编解码组件实例,从而替代默认的组件实例,达到定制化的目的。自定义的配置类如下所示。

@Configuration
public class ProtoFeignConfiguration {
    @Autowired
    private ObjectFactory<HttpMessageConverters> messageConverterObjectFactory;
    @Bean
    public ProtobufHttpMessageConverter protobufHttpMessageConverter() {
        return new ProtobufHttpMessageConverter();
    }

    @Bean
    public Encoder springEncoder() {
        return new SpringEncoder(this.messageConverterObjectFactory);
    }

    @Bean
    public Decoder springDecoder() {
        return new ResponseEntityDecoder(new SpringDecoder(this.messageConverterObjectFactory));
    }
}
复制代码

 其中ProtobufHttpMessageConverterHttpMessageConverters的Protobuf的实现类,负责使用Protocol Buffer进行网络请求和响应的编解码。而SpringEncoderResponseEntityDecoder是OpenFeign中的编解码器实现类。

 下面,我们来看一下OpenFeign中发送网络请求的接口定义。@FeignClient中配置了ProtoFeignConfiguration为自定义配置类。

@FeignClient(name = "user", configuration = ProtoFeignConfiguration.class)
public interface UserClient {
    @RequestMapping(value = "/info", method = RequestMethod.GET,
            consumes = "application/x-protobuf", produces = "application/x-protobuf")
    UserDTO getUserInfo(@RequestParam("id") Long id);
}
复制代码

 其中,UserDTO是使用Protocol Buffer的maven插件自动生成的。需要注意的是,必须将@RequestMappingconsumesproduces属性设置为application/x-protobuf,表示网络请求和响应的编码格式必须是Protobuf,否则可能会接收到406的错误响应码。

 下面是proto文件中的数据格式定义,其中java_package是表明生成文件的目标文件夹。该文件中定义了UserDTO数据格式,它包括ID,名称和主页URL三个属性。

syntax = "proto3";

option java_multiple_files = true;
option java_package = "com.remcarpediem.feignprotobuf.proto.dto";

package com.remcarpediem.feignprotobuf.proto.dto;

message UserDTO {
    int32 id = 1;
    string name = 2;
    string url = 3;
}
复制代码

 在pom文件中配置build属性,使用Protocol Buffer的maven插件可以自动根据proto文件生成Java代码。每个配置项都在代码中有对应的解释。

<build>
        <plugins>
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.5.0</version>
                <extensions>true</extensions>
                <configuration>
                    <!--默认值-->
                    <protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>
                    <!--默认值-->
                    <!--<outputDirectory>${project.build.directory}/generated-sources/protobuf/java</outputDirectory>-->
                    <outputDirectory>${project.build.sourceDirectory}</outputDirectory>
                    <!--设置是否在生成java文件之前清空outputDirectory的文件,默认值为true,设置为false时也会覆盖同名文件-->
                    <clearOutputDirectory>false</clearOutputDirectory>
                    <!--默认值-->
                    <temporaryProtoFileDirectory>${project.build.directory}/protoc-dependencies</temporaryProtoFileDirectory>
                    <!--更多配置信息可以查看https://www.xolstice.org/protobuf-maven-plugin/compile-mojo.html-->
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>test-compile</goal>
                        </goals>
                        <!--也可以设置成局部变量,执行compile或test-compile时才执行-->
                        <!--<configuration>-->
                        <!--<protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>-->
                        <!--<outputDirectory>${project.build.directory}/generated-sources/protobuf/java</outputDirectory>-->
                        <!--<temporaryProtoFileDirectory>${project.build.directory}/protoc-dependencies</temporaryProtoFileDirectory>-->
                        <!--</configuration>-->
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
复制代码

 然后运行Protocol Buffer的maven插件可以自动生成相关的数据类。

服务端

 然后是服务端对于Protocol Buffer的集成。我们也需要使用自定义配置类将ProtobufHttpMessageConverter设置为系统默认的编解码器,如下述代码所示。

@Configuration
public class Conf {
    @Bean
    ProtobufHttpMessageConverter protobufHttpMessageConverter() {
        return new ProtobufHttpMessageConverter();
    }
}
复制代码

 然后定义Controller的关于user的info接口。返回UserDTO实例作为网络请求的返回值。ProtobufHttpMessageConverter会自动将其转换为Protocol Buffer的数据格式进行传输。

@RestController
public class UserController {
    private String host = "http://blog.com/user/";
    @GetMapping("/info")
    public UserDTO getUserInfo(@RequestParam("id") Long id) {
        return UserDTO.newBuilder().
                setId(id).setName("Tom").
                setUrl(host + "Tom").build();
    }
}
复制代码

 本文的源码地址: GitHub:github.com/ztelur/feig…

总结

 欲了解更详细的实现原理和细节,大家可以关注笔者出版的《Spring Cloud 微服务架构进阶》,本书中对Spring Cloud Finchley.RELEASE版本的各个主要组件进行原理讲解和实战应用,里边也有关于OpenFeign的原理和实现的详细解析。更多的介绍见Spring Cloud 微服务架构进阶

《Spring Cloud 微服务架构进阶》购买地址:item.jd.com/12453340.ht…

<think>我们面对的是一个基于Java8、SpringCloud Alibaba和Dubbo的微服务架构。用户希望从微服务A调用微服务B的文件下载接口并获取响应内容。 在Dubbo中,通常我们调用的是RPC接口,返回的是Java对象。但是文件下载通常涉及大文件传输,Dubbo默认的通信方式(如Hessian序列化)可能不是最佳选择,因为大文件传输会导致内存压力大和性能问题。因此,我们需要考虑流式传输或分块传输。 SpringCloud Alibaba Dubbo整合了Dubbo作为RPC框架,同时利用了SpringCloud的服务发现和配置管理。在Dubbo中,我们可以使用Dubbo的附件(Attachment)机制或者流式接口来处理文件传输。但是,对于大文件,更推荐使用流式传输。 步骤概述: 1. 在微服务B(服务提供者)中,定义一个Dubbo接口,该接口返回一个流(如InputStream)或者使用Dubbo的泛化调用结合流式传输。 2. 在微服务A(消费者)中,调用该Dubbo接口,并处理返回的流,将流写入本地文件或直接处理。 然而,Dubbo本身支持流式传输,但需要特殊配置。Dubbo从2.7.7版本开始支持Reactive Stream,但考虑到用户使用的是Java8,我们可以考虑使用Dubbo的普通接口返回InputStream,但要注意Dubbo默认的序列化方式对InputStream的支持并不好。 另一种常见的做法是:将文件存储在共享存储(如OSS)中,然后通过Dubbo接口返回文件路径或下载URL,由调用方自行下载。但用户要求直接通过Dubbo接口获取文件内容。 因此,我们采用分块传输的方式: - 在服务提供者(微服务B)中,提供一个Dubbo接口,该接口返回一个字节数组的迭代器(如`Iterator<byte[]>`)或者使用分块的方法多次调用(需要自定义接口)。 - 或者,使用Dubbo的附件机制,但附件通常用于小数据。 考虑到Dubbo的RPC框架对大文件传输的支持有限,我们也可以考虑使用HTTP直接下载,即微服务B提供一个HTTP下载接口,然后微服务A通过HTTP客户端下载。这样可以利用HTTP的分块传输编码,而且SpringCloud中可以通过OpenFeign或RestTemplate来调用。但是,用户要求使用Dubbo调用。 因此,我们决定使用Dubbo的流式传输(如果Dubbo版本支持)或者分块传输。 具体实现步骤: 1. 在微服务B中定义Dubbo服务接口,该接口返回一个包含文件数据的输入流或者分块传输的迭代器。这里我们使用分块传输,因为流式传输需要Dubbo版本支持且配置复杂。 ```java public interface FileDownloadService { // 分块传输:每次调用返回一个块,直到返回空块表示结束 byte[] downloadFileChunk(String fileName, long offset, int chunkSize); } ``` 但是这样每次调用都需要知道偏移量,调用方需要多次调用,逻辑复杂。 另一种方式是定义一个返回文件元数据(如文件大小)和获取块的方法的接口,但这样需要多次RPC调用。 或者,我们可以利用Dubbo的异步流式接口(需要Dubbo 2.7.0以上版本支持)。这里假设我们使用Dubbo的异步流式接口(Reactive Stream),但Java8不支持Reactor等响应式编程,所以我们可以使用Dubbo的`POJO`流。 在Dubbo中,我们可以这样定义: ```java public interface FileDownloadService { // 返回一个流,每次传输一个块 Iterator<byte[]> downloadFile(String fileName); } ``` 在服务提供者中,我们实现这个接口,返回一个迭代器,每次迭代返回一个字节数组块(例如1MB)。Dubbo在传输时会自动将迭代器流式传输。 2. 在微服务B中实现该接口,读取文件并分块返回。 ```java @Service public class FileDownloadServiceImpl implements FileDownloadService { private static final int CHUNK_SIZE = 1024 * 1024; // 1MB @Override public Iterator<byte[]> downloadFile(String fileName) { try { File file = new File(fileName); if (!file.exists()) { throw new FileNotFoundException(); } FileInputStream fis = new FileInputStream(file); return new FileChunkIterator(fis, CHUNK_SIZE); } catch (FileNotFoundException e) { throw new RuntimeException(e); } } static class FileChunkIterator implements Iterator<byte[]> { private final FileInputStream fis; private final int chunkSize; private long remaining; FileChunkIterator(FileInputStream fis, int chunkSize) throws IOException { this.fis = fis; this.chunkSize = chunkSize; this.remaining = fis.available(); } @Override public boolean hasNext() { return remaining > 0; } @Override public byte[] next() { try { int readSize = (int) Math.min(remaining, chunkSize); byte[] buffer = new byte[readSize]; int read = fis.read(buffer); if (read != readSize) { throw new IOException("Read error"); } remaining -= read; if (remaining == 0) { fis.close(); } return buffer; } catch (IOException e) { throw new RuntimeException(e); } } } } ``` 3. 在微服务A中,注入该Dubbo服务的引用,然后调用并接收迭代器,将块写入文件。 ```java @RestController public class FileDownloadController { @Reference private FileDownloadService fileDownloadService; @GetMapping("/download") public ResponseEntity<StreamingResponseBody> downloadFile() { Iterator<byte[]> iterator = fileDownloadService.downloadFile("example.txt"); StreamingResponseBody responseBody = outputStream -> { while (iterator.hasNext()) { byte[] chunk = iterator.next(); outputStream.write(chunk); } }; return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"example.txt\"") .body(responseBody); } } ``` 但是,这里我们是在微服务A中提供一个HTTP接口给前端下载,而用户要求的是微服务A调用微服务B的下载接口并获取响应内容。所以,我们可以在微服务A中直接调用,然后将文件保存到本地,或者再转成流提供给前端。 如果微服务A需要将文件保存到本地: ```java public void downloadAndSave(String remoteFileName, String localFilePath) { Iterator<byte[]> iterator = fileDownloadService.downloadFile(remoteFileName); try (FileOutputStream fos = new FileOutputStream(localFilePath)) { while (iterator.hasNext()) { byte[] chunk = iterator.next(); fos.write(chunk); } } catch (IOException e) { e.printStackTrace(); } } ``` 注意事项: 1. 分块大小需要根据网络情况和内存设置合理值。 2. 异常处理需要完善。 3. 如果文件很大,迭代器可能会占用服务提供者端的内存,因为Dubbo需要保持文件输入流打开直到传输完成。因此,需要确保服务提供者有足够资源。 4. 由于Dubbo的流式传输是单连接,如果同时有多个大文件传输,可能会占用连接资源,需要考虑超时设置和连接池。 另外,Dubbo的流式传输需要配置序列化方式,比如使用`dubbo`协议,并且序列化方式为`hessian2`(支持流式传输)。 在配置上,需要在服务提供者和消费者中开启流式传输支持(如果使用Dubbo的流式调用)。但是,我们上面的例子使用了迭代器,Dubbo默认支持迭代器的流式传输吗?在Dubbo中,如果返回的是`Iterator`,Dubbo会将其作为流式响应处理,但需要确保Dubbo版本支持(建议2.7.0以上)。 如果Dubbo版本较低,可能需要使用附件传输,但附件大小有限制(通常几MB),不适合大文件。 因此,如果Dubbo版本低于2.7.0,建议升级Dubbo版本,或者采用其他方式(如HTTP下载)。 总结:在SpringCloud Alibaba Dubbo架构中,通过定义返回迭代器的Dubbo接口,实现分块传输文件内容,消费者通过迭代器逐步接收文件块并写入本地文件。 引用说明: - 关于Dubbo流式调用,可以参考Dubbo官方文档:https://dubbo.apache.org/zh/docs/v2.7/user/examples/streaming/ [^1] - SpringCloud Alibaba Dubbo整合文档:https://github.com/alibaba/spring-cloud-alibaba/blob/master/spring-cloud-alibaba-docs/src/main/asciidoc/dubbo.adoc [^2] [^1]: Dubbo官方文档 [^2]: SpringCloud Alibaba Dubbo文档</think>在基于 Java8、SpringCloud Alibaba 和 Dubbo 的微服务架构中,实现微服务 A 调用微服务 B 的文件下载接口并获取响应数据,需结合 Dubbo 的 RPC 能力和文件传输特性。以下是具体实现方案: --- ### **1. 微服务 B(服务提供者)实现文件下载接口** #### **步骤 1.1:定义 Dubbo 服务接口** ```java public interface FileDownloadService { // 返回文件元数据 + 分块数据 FileChunk downloadFileChunk(String fileId, long offset, int chunkSize); } ``` #### **步骤 1.2:实现文件分块传输逻辑** ```java @Service public class FileDownloadServiceImpl implements FileDownloadService { @Override public FileChunk downloadFileChunk(String fileId, long offset, int chunkSize) { Path filePath = resolveFilePath(fileId); // 根据 ID 获取文件路径 try (RandomAccessFile file = new RandomAccessFile(filePath.toFile(), "r")) { file.seek(offset); byte[] buffer = new byte[chunkSize]; int bytesRead = file.read(buffer); // 封装分块数据 return new FileChunk( fileId, offset, bytesRead, bytesRead == chunkSize ? buffer : Arrays.copyOf(buffer, bytesRead) ); } catch (IOException e) { throw new RuntimeException("文件下载失败", e); } } } ``` #### **步骤 1.3:定义文件分块传输模型** ```java public class FileChunk implements Serializable { private String fileId; // 文件唯一标识 private long offset; // 当前分块偏移量 private int size; // 当前分块实际大小 private byte[] data; // 分块二进制数据 // 构造方法、Getter/Setter } ``` --- ### **2. 微服务 A(服务消费者)调用文件下载接口** #### **步骤 2.1:注入 Dubbo 服务代理** ```java @RestController public class FileDownloadClient { @Reference // Dubbo 远程引用 private FileDownloadService fileDownloadService; } ``` #### **步骤 2.2:分块下载并组装文件** ```java public void downloadFile(String fileId, String localPath) { int chunkSize = 1024 * 1024; // 每次下载 1MB long offset = 0; try (FileOutputStream fos = new FileOutputStream(localPath)) { while (true) { FileChunk chunk = fileDownloadService.downloadFileChunk(fileId, offset, chunkSize); if (chunk.getSize() <= 0) break; // 无数据时终止 fos.write(chunk.getData(), 0, chunk.getSize()); offset += chunk.getSize(); if (chunk.getSize() < chunkSize) break; // 最后一块 } } catch (IOException e) { throw new RuntimeException("文件保存失败", e); } } ``` --- ### **3. 关键配置与优化** #### **3.1 Dubbo 配置(`application.yml`)** ```yaml dubbo: protocol: name: dubbo payload: 8388608 # 提高单次传输上限(默认 8MB) registry: address: nacos://localhost:8848 # 使用 Nacos 注册中心 ``` #### **3.2 传输优化建议** - **分块大小**:根据网络带宽调整 `chunkSize`(建议 512KB~4MB)。 - **超时控制**:添加 Dubbo 超时配置避免阻塞: ```java @Reference(timeout = 30000) // 30秒超时 private FileDownloadService fileDownloadService; ``` - **错误重试**:通过 `@Reference(retries = 2)` 实现失败重试。 #### **3.3 替代方案:返回文件 URL** 若文件存储在 OSS 或本地服务器,可直接返回下载 URL: ```java public interface FileDownloadService { String generateDownloadUrl(String fileId); // 返回 OSS 临时链接 } ``` 消费者通过 HTTP 客户端(如 `RestTemplate`)直接下载文件,避免 RPC 传输开销。 --- ### **4. 注意事项** 1. **大文件传输**:Dubbo 默认单次请求上限为 8MB,需通过 `dubbo.protocol.payload` 调整。 2. **内存管理**:分块传输可避免一次性加载大文件到内存。 3. **序列化优化**:使用 `byte[]` 而非 `InputStream` 确保 Dubbo 序列化兼容性。 4. **服务发现**:确保微服务 A/B 均注册到同一 Nacos 中心[^3]。 --- ### **相关问题** 1. **如何优化 Dubbo 大文件传输的性能?** 建议采用分块传输、调整 `payload` 大小、启用压缩(`compression="gzip"`)或使用 HTTP 直传。 2. **Dubbo 调用文件下载接口时如何保证可靠性?** 可通过重试机制(`retries`)、熔断降级(Sentinel 集成)和分块校验(如 MD5 校验)实现。 3. **SpringCloud Alibaba 中如何监控 Dubbo 文件传输流量?** 使用 `dubbo-monitor` 模块或通过 `MetricsFilter` 采集传输指标,整合 Prometheus 可视化[^2]。 4. **是否支持断点续传?如何实现?** 支持。消费者记录已下载的 `offset`,服务提供者根据 `offset` 返回对应分块。 [^1]: 开发 Dubbo Spring Cloud 应用的方法与传统 Dubbo 或 Spring Cloud 应用类似,按照以下步骤就能完整地实现Dubbo 服务提供方和消费方的应用。 [^2]: 微服务之SpringCloud基础。SpringCloud技术流1.SpringCloud注册中心环境搭建euraka2.服务注册与发现3.SpringCloud客户端调用 rest/feign 客户端调用工具ribbon 负载均衡zuul接口网关eureka服务注册。 [^3]: SpringCloud基础使用(三)-SpringCloud Alibaba体系。类似 Eureka、ZooKeeper、Consul 等组件,既可以支持 HTTP、https 的服务注册和发现,也可以支持 RPC 的服务注册和发现,比如 Dubbo,也是出自于阿里,完全可以替代 Eureka、ZooKeeper、Consul。企业中nacos使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值