Spring REST API with Protocol Buffers – 带有协议缓冲区的Spring REST API

最后修改: 2016年 6月 19日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Protocol Buffers is a language and platform neutral mechanism for serialization and deserialization of structured data, which is proclaimed by Google, its creator, to be much faster, smaller and simpler than other types of payloads, such as XML and JSON.

协议缓冲区是一种语言和平台中立的机制,用于结构化数据的序列化和反序列化,其创建者Google宣称其比其他类型的有效载荷(如XML和JSON)更快、更小、更简单。

This tutorial guides you through setting up a REST API to take advantage of this binary-based message structure.

本教程指导你设置一个REST API,以利用这种基于二进制的消息结构。

2. Protocol Buffers

2.协议缓冲区

This section gives some basic information on Protocol Buffers and how they are applied in the Java ecosystem.

本节给出了关于协议缓冲区的一些基本信息以及它们在Java生态系统中的应用。

2.1. Introduction to Protocol Buffers

2.1.协议缓冲区简介

In order to make use of Protocol Buffers, we need to define message structures in .proto files. Each file is a description of the data that might be transferred from one node to another, or stored in data sources. Here is an example of .proto files, which is named baeldung.proto and lives in the src/main/resources directory. This file will be used in this tutorial later on:

为了利用协议缓冲区,我们需要在.proto文件中定义消息结构。每个文件都是对可能从一个节点传输到另一个节点的数据的描述,或存储在数据源中。这里有一个.proto文件的例子,它被命名为baeldung.proto,位于src/main/resources目录中。这个文件将在以后的本教程中使用。

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;
    }
}

In this tutorial, we use version 3 of both protocol buffer compiler and protocol buffer language, therefore the .proto file must start with the syntax = “proto3” declaration. If a compiler version 2 is in use, this declaration would be omitted. Next comes the package declaration, which is the namespace for this message structure to avoid naming conflicts with other projects.

在本教程中,我们使用版本3的协议缓冲区编译器和协议缓冲区语言,因此.proto文件必须以syntax = “proto3” 声明开始。如果使用的是编译器版本2,这个声明将被省略。接下来是package声明,它是这个消息结构的命名空间,以避免与其他项目的命名冲突。

The following two declarations are used for Java only: java_package option specifies the package for our generated classes to live in, and java_outer_classname option indicates name of the class enclosing all the types defined in this .proto file.

以下两个声明只用于Java。java_package选项指定了我们生成的类所处的包,java_outer_classname选项指出了包含这个.proto文件中定义的所有类型的类名称。

Subsection 2.3 below will describe the remaining elements and how those are compiled into Java code.

下面的2.3小节将描述其余的元素以及如何将这些元素编译成Java代码。

2.2. Protocol Buffers With Java

2.2.使用Java的协议缓冲区

After a message structure is defined, we need a compiler to convert this language neutral content to Java code. You can follow the instructions in the Protocol Buffers repository in order to get an appropriate compiler version. Alternatively, you may download a pre-built binary compiler from the Maven central repository by searching for the com.google.protobuf:protoc artifact, then picking up an appropriate version for your platform.

在定义了消息结构后,我们需要一个编译器来将这个语言中立的内容转换为Java代码。您可以按照协议缓冲区资源库中的说明,以获得合适的编译器版本。另外,你也可以从Maven中央仓库下载一个预建的二进制编译器,方法是搜索com.google.protobuf:protoc工件,然后为你的平台选取一个合适的版本。

Next, copy the compiler to the src/main directory of your project and execute the following command in the command line:

接下来,将编译器复制到你项目的src/main目录下,并在命令行中执行以下命令。

protoc --java_out=java resources/baeldung.proto

This should generate a source file for the BaeldungTraining class within the com.baeldung.protobuf package, as specified in the option declarations of the baeldung.proto file.

这应该为com.baeldung.protobuf包内的BaeldungTraining类生成一个源文件,如baeldung.proto文件的option声明中所指定。

In addition to the compiler, Protocol Buffers runtime is required. This can be achieved by adding the following dependency to the Maven POM file:

除了编译器外,还需要Protocol Buffers运行时。这可以通过在Maven POM文件中添加以下依赖关系来实现。

<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>3.0.0-beta-3</version>
</dependency>

We may use another version of the runtime, provided that it is the same as the compiler’s version. For the latest one, please check out this link.

我们可以使用另一个版本的运行时,只要它与编译器的版本相同。关于最新的,请查看这个链接

2.3. Compiling a Message Description

2.3.汇编信息描述

By using a compiler, messages in a .proto file are compiled into static nested Java classes. In the above example, the Course and Student messages are converted to Course and Student Java classes, respectively. At the same time, messages’ fields are compiled into JavaBeans style getters and setters inside those generated types. The marker, composed of an equal sign and a number, at the end of each field declaration is the unique tag used to encode the associated field in the binary form.

通过使用编译器,.proto文件中的消息被编译成静态嵌套的Java类。在上面的例子中,CourseStudent消息被分别转换为CourseStudent Java类。同时,消息的字段被编译成这些生成的类型中的JavaBeans风格的getter和setter。每个字段声明末尾的标记,由一个等号和一个数字组成,是用来在二进制形式中对相关字段进行编码的唯一标记。

We will walk through typed fields of the messages to see how those are converted to accessor methods.

我们将走过消息的类型字段,看看这些字段是如何转换为访问器方法的。

Let’s start with the Course message. It has two simple fields, including id and course_name. Their protocol buffer types, int32 and string, are translated into Java int and String types. Here are their associated getters after compilation (with implementations being left out for brevity):

让我们从Course信息开始。它有两个简单的字段,包括idcourse_name。它们的协议缓冲区类型,int32string,被翻译成JavaintString类型。下面是它们编译后的相关获取器(为了简洁起见,实现被省略了)。

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

Note that names of typed fields should be in snake case (individual words are separated by underscore characters) to maintain the cooperation with other languages. The compiler will convert those names to camel case according to Java conventions.

注意,键入字段的名称应使用蛇形大小写(单个字由下划线字符分隔),以保持与其他语言的合作。编译器将根据Java惯例把这些名字转换为骆驼字母大小写。

The last field of Course message, student, is of the Student complex type, which will be described below. This field is prepended by the repeated keyword, meaning that it may be repeated any number of times. The compiler generates some methods associated with the student field as follows (without implementations):

Course消息的最后一个字段,student,属于Student复合类型,这将在下面描述。这个字段的前缀是repeated关键字,意味着它可以重复任何数量的次数。编译器产生了一些与student字段相关的方法,如下(没有实现)。

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

Now we will move on to the Student message, which is used as complex type of the student field of Course message. Its simple fields, including id, first_name, last_name and email are used to create Java accessor methods:

现在我们将继续讨论Student消息,它被用作Course消息的student字段的复杂类型。它的简单字段,包括id, first_name, last_nameemail,被用来创建Java访问器方法。

public int getId();
public java.lang.String getFirstName();
public java.lang.String getLastName();
public java.lang.String.getEmail();

The last field, phone, is of the PhoneNumber complex type. Similar to the student field of Course message, this field is repetitive and has several associated methods:

最后一个字段,phone,属于PhoneNumber复合类型。与Course消息的student字段类似,这个字段是重复的,有几个相关的方法。

public java.util.List<com.baeldung.protobuf.BaeldungTraining.Student.PhoneNumber> getPhoneList();
public int getPhoneCount();
public com.baeldung.protobuf.BaeldungTraining.Student.PhoneNumber getPhone(int index);

The PhoneNumber message is compiled into the BaeldungTraining.Student.PhoneNumber nested type, with two getters corresponding to the message’s fields:

PhoneNumber消息被编译成BaeldungTraining.Student.PhoneNumber嵌套类型,有两个与消息字段对应的getters。

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

PhoneType, the complex type of the type field of the PhoneNumber message, is an enumeration type, which will be transformed into a Java enum type nested within the BaeldungTraining.Student class:

PhoneTypePhoneNumber消息的type字段的复合类型,是一个枚举类型,它将被转化为嵌套在BaeldungTraining.Student类中的Javaenum类型。

public enum PhoneType implements com.google.protobuf.ProtocolMessageEnum {
    MOBILE(0),
    LANDLINE(1),
    UNRECOGNIZED(-1),
    ;
    // Other declarations
}

3. Protobuf in Spring REST API

3.Spring REST API中的Protobuf

This section will guide you through setting up a REST service using Spring Boot.

本节将指导你使用Spring Boot建立一个REST服务。

3.1. Bean Declaration

3.1.Bean声明

Let’s start with the definition of our main @SpringBootApplication:

让我们从定义我们的主@SpringBootApplication开始。

@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);
    }

    // Other declarations
}

The ProtobufHttpMessageConverter bean is used to convert responses returned by @RequestMapping annotated methods to protocol buffer messages.

ProtobufHttpMessageConverter bean用于将@RequestMapping注释的方法返回的响应转换为协议缓冲区消息。

The other bean, CourseRepository, contains some test data for our API.

另一个Bean,CourseRepository,包含了我们API的一些测试数据。

What’s important here is that we’re operating with Protocol Buffer specific data – not with standard POJOs.

这里重要的是,我们是在用Protocol Buffer的特定数据进行操作–而不是用标准的POJO

Here’s the simple implementation of the CourseRepository:

这里是CourseRepository的简单实现。

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. Controller Configuration

3.2.控制器配置

We can define the @Controller class for a test URL as follows:

我们可以为一个测试URL定义@Controller类,如下所示。

@RestController
public class CourseController {
    @Autowired
    CourseRepository courseRepo;

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

And again – the important thing here is that the Course DTO that we’re returning from the controller layer is not a standard POJO. That’s going to be the trigger for it to be converted to protocol buffer messages before being transferred back to the Client.

再说一遍,重要的是,我们从控制器层返回的Course DTO不是一个标准的POJO。这将是触发它被转换为协议缓冲区消息的原因,然后再转回给客户端。

4. REST Clients and Testing

4.REST客户端和测试

Now that we had a look at the simple API implementation – let’s now illustrate deserialization of protocol buffer messages on the client side – using two methods.

现在我们看了一下简单的API实现–现在让我们来说明在客户端对协议缓冲区消息进行反序列化–使用两种方法。

The first one takes advantage of the RestTemplate API with a pre-configured ProtobufHttpMessageConverter bean to automatically convert messages.

第一个是利用RestTemplate API与一个预先配置的ProtobufHttpMessageConverter Bean来自动转换消息。

The second is using protobuf-java-format to manually transform protocol buffer responses into JSON documents.

第二种是使用protobuf-java-format来手动将协议缓冲区响应转化为JSON文档。

To begin, we need to set up the context for an integration test and instruct Spring Boot to find configuration information in the Application class by declaring a test class as follows:

首先,我们需要为集成测试设置上下文,并指示Spring Boot在Application类中寻找配置信息,方法是声明一个测试类如下。

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebIntegrationTest
public class ApplicationTest {
    // Other declarations
}

All code snippets in this section will be placed in the ApplicationTest class.

本节中的所有代码片段都将放在ApplicationTest类中。

4.1. Expected Response

4.1.预期的反应

The first step to access a REST service is to determine the request URL:

访问REST服务的第一步是确定请求URL。

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

This COURSE1_URL will be used for getting the first test double course from the REST service we created before. After a GET request is sent to the above URL, the corresponding response is verified using the following assertions:

这个COURSE1_URL将用于从我们之前创建的REST服务中获取第一门测试双倍课程。在向上述URL发送GET请求后,相应的响应将使用以下断言进行验证。

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@baeldung.com"));
    assertThat(response, containsString("richard.roe@baeldung.com"));
    assertThat(response, containsString("jane.doe@baeldung.com"));
    assertThat(response, containsString("phone"));
    assertThat(response, containsString("number"));
    assertThat(response, containsString("type"));
}

We will make use of this helper method in both test cases covered in the succeeding sub-sections.

我们将在接下来的小节中涉及的两个测试案例中使用这个辅助方法。

4.2. Testing With RestTemplate

4.2.使用RestTemplate测试

Here is how we create a client, send a GET request to the designated destination, receive the response in the form of protocol buffer messages and verify it using the RestTemplate API:

下面是我们如何创建一个客户端,向指定的目的地发送GET请求,以协议缓冲区信息的形式接收响应,并使用RestTemplate API进行验证。

@Autowired
private RestTemplate restTemplate;

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

To make this test case work, we need a bean of the RestTemplate type to be registered in a configuration class:

为了使这个测试案例工作,我们需要在配置类中注册一个RestTemplate类型的bean。

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

Another bean of the ProtobufHttpMessageConverter type is also required to automatically transform the received protocol buffer messages. This bean is the same as the one defined in sub-section 3.1. Since the client and server share the same application context in this tutorial, we may declare the RestTemplate bean in the Application class and re-use the ProtobufHttpMessageConverter bean.

还需要另一个ProtobufHttpMessageConverter类型的Bean来自动转换收到的协议缓冲区消息。这个Bean与第3.1小节中定义的Bean相同。由于在本教程中,客户端和服务器共享同一个应用程序上下文,我们可以在Application类中声明RestTemplate Bean,并重新使用ProtobufHttpMessageConverter Bean。

4.3. Testing With HttpClient

4.3.使用HttpClient测试

The first step to use the HttpClient API and manually convert protocol buffer messages is adding the following two dependencies to the Maven POM file:

使用HttpClient API和手动转换协议缓冲区消息的第一步是在Maven POM文件中添加以下两个依赖项。

<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.2</version>
</dependency>

For the latest versions of these dependencies, please have a look at protobuf-java-format and httpclient artifacts in Maven central repository.

关于这些依赖的最新版本,请查看Maven中央仓库中的protobuf-java-formathttpclient构件。

Let’s move on to create a client, execute a GET request and convert the associated response to an InputStream instance using the given URL:

让我们继续创建一个客户端,执行一个GET请求,并使用给定的URL将相关的响应转换为一个InputStream实例。

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();
}

Now, we will convert protocol buffer messages in the form of an InputStream object to a JSON document:

现在,我们将把InputStream对象形式的协议缓冲区信息转换为JSON文档。

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

And here is how a test case uses private helper methods declared above and validates the response:

下面是一个测试用例如何使用上面声明的私有帮助器方法并验证响应。

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

4.4. Response in JSON

4.4.JSON中的响应

In order to make it clear, JSON forms of the responses we received in the tests described in previous sub-sections are included herein:

为了使之清晰,这里包括了我们在前面几个小节中描述的测试中收到的JSON形式的响应。

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

5. Conclusion

5.结论

This tutorial quickly introduced Protocol Buffers and illustrated the setting up of a REST API using the format with Spring. We then moved to client support and the serialization-deserialization mechanism.

本教程快速介绍了协议缓冲区,并说明了如何使用Spring的格式来设置REST API。然后,我们转向了客户端支持和序列化-反序列化机制。

The implementation of all the examples and code snippets can be found in a GitHub project.

所有例子和代码片段的实现都可以在一个GitHub项目中找到