1. 概述

Protocol Buffers(简称Protobuf)是Google推出的一种跨语言、跨平台的结构化数据序列化机制。相比XML和JSON,它具备三大优势:

  • 更快的处理速度
  • 更小的数据体积
  • 更简单的数据结构

本教程将指导你如何构建一个REST API,利用这种二进制消息格式提升性能。

2. Protocol Buffers基础

2.1 Protobuf简介

使用Protobuf需要先在.proto文件中定义消息结构。这些文件描述了节点间传输或存储的数据结构。以下是一个典型示例(位于src/main/resources/baeldung.proto):

syntax = "proto3";
package baeldung;
option java_package = "com.baeldung.protobuf";
option java_outer_classname = "BaeldungTraining";

message Course {
    int32 id = 1;
    string course_name = 2;
    repeated Student student = 3;
}
message Student {
    int32 id = 1;
    string first_name = 2;
    string last_name = 3;
    string email = 4;
    repeated PhoneNumber phone = 5;
    message PhoneNumber {
        string number = 1;
        PhoneType type = 2;
    }
    enum PhoneType {
        MOBILE = 0;
        LANDLINE = 1;
    }
}

关键点说明:

  • syntax = "proto3":声明使用Protobuf v3语法(v2可省略)
  • package:命名空间,避免命名冲突
  • java_package:指定生成Java类的包名
  • java_outer_classname:指定外层类名

2.2 Java环境集成

定义消息结构后,需通过编译器生成Java代码。有两种获取编译器的方式:

  1. 官方仓库编译源码
  2. 直接下载预编译二进制文件(Maven Central搜索com.google.protobuf:protoc

将编译器复制到项目src/main目录后执行:

protoc --java_out=java resources/baeldung.proto

这将在com.baeldung.protobuf包下生成BaeldungTraining类。同时需添加运行时依赖:

<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>3.25.3</version>
</dependency>

踩坑提示:编译器和运行库版本必须严格匹配!

2.3 消息编译原理

Protobuf将.proto文件中的消息编译为Java静态嵌套类。核心转换规则:

基础类型字段(如Course的id和course_name):

public int getId();
public java.lang.String getCourseName();

重复字段(如Course的student):

public java.util.List<com.baeldung.protobuf.BaeldungTraining.Student> getStudentList();
public int getStudentCount();
public com.baeldung.protobuf.BaeldungTraining.Student getStudent(int index);

嵌套消息(如Student的PhoneNumber):

public java.lang.String getNumber();
public com.baeldung.protobuf.BaeldungTraining.Student.PhoneType getType();

枚举类型(如PhoneType):

public enum PhoneType implements com.google.protobuf.ProtocolMessageEnum {
    MOBILE(0),
    LANDLINE(1),
    UNRECOGNIZED(-1),
    ;
    // 其他声明
}

技术要点:字段名使用snake_case(如first_name),编译器会自动转换为Java风格的camelCase(getFirstName()

3. Spring REST API集成

3.1 核心Bean配置

主应用类配置如下:

@SpringBootApplication
public class Application {
    @Bean
    ProtobufHttpMessageConverter protobufHttpMessageConverter() {
        return new ProtobufHttpMessageConverter();
    }

    @Bean
    public CourseRepository createTestCourses() {
        Map<Integer, Course> courses = new HashMap<>();
        Course course1 = Course.newBuilder()
          .setId(1)
          .setCourseName("REST with Spring")
          .addAllStudent(createTestStudents())
          .build();
        Course course2 = Course.newBuilder()
          .setId(2)
          .setCourseName("Learn Spring Security")
          .addAllStudent(new ArrayList<Student>())
          .build();
        courses.put(course1.getId(), course1);
        courses.put(course2.getId(), course2);
        return new CourseRepository(courses);
    }
    // 其他声明
}

关键组件说明:

  • ProtobufHttpMessageConverter:将响应自动转换为Protobuf消息
  • CourseRepository:提供测试数据(注意:操作的是Protobuf对象而非POJO)

仓库类实现:

public class CourseRepository {
    Map<Integer, Course> courses;
    
    public CourseRepository (Map<Integer, Course> courses) {
        this.courses = courses;
    }
    
    public Course getCourse(int id) {
        return courses.get(id);
    }
}

3.2 控制器配置

控制器实现简单粗暴:

@RestController
public class CourseController {
    @Autowired
    CourseRepository courseRepo;

    @RequestMapping("/courses/{id}")
    Course customer(@PathVariable Integer id) {
        return courseRepo.getCourse(id);
    }
}

核心机制:返回的Course对象是Protobuf类型而非POJO,这会触发ProtobufHttpMessageConverter的自动转换。

4. 客户端调用与测试

4.1 测试环境准备

集成测试基类配置:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebIntegrationTest
public class ApplicationTest {
    // 其他声明
}

定义测试URL:

private static final String COURSE1_URL = "http://localhost:8080/courses/1";

响应验证方法:

private void assertResponse(String response) {
    assertThat(response, containsString("id"));
    assertThat(response, containsString("course_name"));
    assertThat(response, containsString("REST with Spring"));
    assertThat(response, containsString("student"));
    assertThat(response, containsString("first_name"));
    assertThat(response, containsString("last_name"));
    assertThat(response, containsString("email"));
    assertThat(response, containsString("john.doe@example.com"));
    assertThat(response, containsString("richard.roe@example.com"));
    assertThat(response, containsString("jane.doe@example.com"));
    assertThat(response, containsString("phone"));
    assertThat(response, containsString("number"));
    assertThat(response, containsString("type"));
}

邮箱Mock说明:将原脱敏邮箱替换为测试专用邮箱

4.2 RestTemplate测试

自动转换测试:

@Autowired
private RestTemplate restTemplate;

@Test
public void whenUsingRestTemplate_thenSucceed() {
    ResponseEntity<Course> course = restTemplate.getForEntity(COURSE1_URL, Course.class);
    assertResponse(course.toString());
}

需配置RestTemplate Bean:

@Bean
RestTemplate restTemplate(ProtobufHttpMessageConverter hmc) {
    return new RestTemplate(Arrays.asList(hmc));
}

4.3 HttpClient手动转换测试

添加依赖:

<dependency>
    <groupId>com.googlecode.protobuf-java-format</groupId>
    <artifactId>protobuf-java-format</artifactId>
    <version>1.4</version>
</dependency>
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.14</version>
</dependency>

手动转换实现:

private InputStream executeHttpRequest(String url) throws IOException {
    CloseableHttpClient httpClient = HttpClients.createDefault();
    HttpGet request = new HttpGet(url);
    HttpResponse httpResponse = httpClient.execute(request);
    return httpResponse.getEntity().getContent();
}

private String convertProtobufMessageStreamToJsonString(InputStream protobufStream) throws IOException {
    JsonFormat jsonFormat = new JsonFormat();
    Course course = Course.parseFrom(protobufStream);
    return jsonFormat.printToString(course);
}

@Test
public void whenUsingHttpClient_thenSucceed() throws IOException {
    InputStream responseStream = executeHttpRequest(COURSE1_URL);
    String jsonOutput = convertProtobufMessageStreamToJsonString(responseStream);
    assertResponse(jsonOutput);
}

4.4 JSON响应示例

测试返回的JSON结构示例:

id: 1
course_name: "REST with Spring"
student {
    id: 1
    first_name: "John"
    last_name: "Doe"
    email: "john.doe@example.com"
    phone {
        number: "123456"
    }
}
student {
    id: 2
    first_name: "Richard"
    last_name: "Roe"
    email: "richard.roe@example.com"
    phone {
        number: "234567"
        type: LANDLINE
    }
}
student {
    id: 3
    first_name: "Jane"
    last_name: "Doe"
    email: "jane.doe@example.com"
    phone {
        number: "345678"
    }
    phone {
        number: "456789"
        type: LANDLINE
    }
}

5. 总结

本教程介绍了:

  • Protobuf核心概念与Java集成
  • 在Spring Boot中构建Protobuf REST API
  • 客户端调用与测试方案

所有示例代码可在GitHub项目中获取。相比传统JSON,Protobuf在以下场景优势明显:

  • 高频微服务通信
  • 移动端数据传输
  • 存储敏感数据场景

简单粗暴的建议:当性能成为瓶颈时,Protobuf是值得考虑的优化方案。


原始标题:Spring REST API with Protocol Buffers