API REST do Spring com buffers de protocolo
*1. Visão geral *
Buffers de protocolo é um mecanismo neutro em linguagem e plataforma para serialização e desserialização de dados estruturados, que é proclamado pelo Google, seu criador, como muito mais rápido, menor e mais simples do que outros tipos de cargas, como XML e JSON.
Este tutorial orienta você na configuração de uma API REST para aproveitar essa estrutura de mensagens baseada em binário.
===* 2. Buffers de protocolo *
Esta seção fornece algumas informações básicas sobre buffers de protocolo e como eles são aplicados no ecossistema Java.
====* 2.1 Introdução aos buffers de protocolo *
Para usar os Buffers de Protocolo, precisamos definir estruturas de mensagens nos arquivos .proto. Cada arquivo é uma descrição dos dados que podem ser transferidos de um nó para outro ou armazenados em fontes de dados. Aqui está um exemplo de arquivos .proto, chamado .proto, que reside no diretório src/main/resources. Este arquivo será usado neste tutorial posteriormente:
syntax = "proto3";
package ;
option java_package = "com..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;
}
}
Neste tutorial,* usamos a versão 3 do compilador de buffer de protocolo e da linguagem de buffer de protocolo *, portanto, o arquivo .proto deve começar com a declaração syntax = "proto3" _. Se uma versão 2 do compilador estiver em uso, essa declaração será omitida. A seguir, vem a declaração _package, que é o espaço para nome dessa estrutura de mensagens para evitar conflitos de nomeação com outros projetos.
As duas declarações a seguir são usadas apenas para Java: a opção java_package especifica o pacote para nossas classes geradas residirem e a opção java_outer_classname indica o nome da classe que encerra todos os tipos definidos neste arquivo .proto.
A subseção 2.3 abaixo descreve os elementos restantes e como eles são compilados no código Java.
2.2 Buffers de protocolo com Java
Depois que uma estrutura de mensagem é definida, precisamos de um compilador para converter o conteúdo neutro dessa linguagem em código Java. Você pode seguir as instruções no repositório de buffers de protocolo para obter uma versão apropriada do compilador. Como alternativa, você pode baixar um compilador binário pré-criado no repositório central do Maven, pesquisando o https://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22com.google.protobuf%22 % 20AND% 20a% 3A% 22protoc% 22 [com.google.protobuf: protoc] artefato e escolha uma versão apropriada para sua plataforma.
Em seguida, copie o compilador para o diretório src/main do seu projeto e execute o seguinte comando na linha de comando:
protoc --java_out=java resources/.proto
Isso deve gerar um arquivo de origem para a classe BaeldungTraining dentro do pacote com..protobuf, conforme especificado nas declarações option do arquivo .proto.
Além do compilador, é necessário o tempo de execução dos Buffers de Protocolo. Isso pode ser conseguido adicionando a seguinte dependência ao arquivo POM do Maven:
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.0.0-beta-3</version>
</dependency>
Podemos usar outra versão do tempo de execução, desde que seja igual à versão do compilador. Para obter a versão mais recente, consulte https://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22com.google.protobuf%22%20AND%20a%3A%22protobuf-java%22 [esse link].
2.3 Compilando uma Descrição da Mensagem
Usando um compilador, as mensagens em um arquivo .proto são compiladas em classes Java aninhadas estáticas. No exemplo acima, as mensagens Course e Student são convertidas nas classes Java Course e Student Java, respectivamente. Ao mesmo tempo, os campos das mensagens são compilados nos getters e setters no estilo JavaBeans dentro desses tipos gerados. O marcador, composto por um sinal de igual e um número, no final de cada declaração de campo é a marca exclusiva usada para codificar o campo associado no formato binário.
Percorreremos os campos digitados das mensagens para ver como eles são convertidos em métodos de acessador.
Vamos começar com a mensagem Course. Possui dois campos simples, incluindo id e course_name. Seus tipos de buffer de protocolo, int32 e string, são convertidos nos tipos Java int e String. Aqui estão os getters associados após a compilação (com as implementações sendo deixadas de fora por brevidade):
public int getId();
public java.lang.String getCourseName();
Observe que os nomes dos campos digitados devem estar em maiúsculas e minúsculas (palavras individuais são separadas por caracteres sublinhados) para manter a cooperação com outros idiomas. O compilador converterá esses nomes em maiúsculas e minúsculas, de acordo com as convenções Java.
O último campo da mensagem Course, student, é do tipo complexo Student, que será descrito abaixo. Este campo é precedido pela palavra-chave repeated, o que significa que pode ser repetido várias vezes. O compilador gera alguns métodos associados ao campo student da seguinte maneira (sem implementações):
public java.util.List<com..protobuf.BaeldungTraining.Student> getStudentList();
public int getStudentCount();
public com..protobuf.BaeldungTraining.Student getStudent(int index);
Agora vamos passar para a mensagem Student, que é usada como um tipo complexo do campo student da mensagem Course. Seus campos simples, incluindo id, first_name, last_name e email, são usados para criar métodos de acessador Java:
public int getId();
public java.lang.String getFirstName();
public java.lang.String getLastName();
public java.lang.String.getEmail();
O último campo, phone, é do tipo complexo PhoneNumber. Semelhante ao campo student da mensagem Course, esse campo é repetitivo e possui vários métodos associados:
public java.util.List<com..protobuf.BaeldungTraining.Student.PhoneNumber> getPhoneList();
public int getPhoneCount();
public com..protobuf.BaeldungTraining.Student.PhoneNumber getPhone(int index);
A mensagem PhoneNumber é compilada no tipo aninhado BaeldungTraining.Student.PhoneNumber, com dois getters correspondentes aos campos da mensagem:
public java.lang.String getNumber();
public com..protobuf.BaeldungTraining.Student.PhoneType getType();
PhoneType, o tipo complexo do campo type da mensagem PhoneNumber, é um tipo de enumeração, que será transformado em um tipo Java enum aninhado na classe BaeldungTraining.Student:
public enum PhoneType implements com.google.protobuf.ProtocolMessageEnum {
MOBILE(0),
LANDLINE(1),
UNRECOGNIZED(-1),
;
//Other declarations
}
*3. Protobuf na API REST do Spring *
Esta seção o guiará na configuração de um serviço REST usando o Spring Boot.
====* 3.1 Declaração do Bean *
Vamos começar com a definição do nosso principal _ @ 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
}
O bean ProtobufHttpMessageConverter é usado para converter respostas retornadas pelos métodos anotados _ @ RequestMapping_ em protocolos de mensagens do buffer.
O outro bean, CourseRepository, contém alguns dados de teste para nossa API.
O importante aqui é que estamos operando com dados específicos do* Protocol Buffer - não com POJOs padrão *.
Aqui está a implementação simples do 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 Configuração do Controlador
Podemos definir a classe _ @ Controller_ para um URL de teste da seguinte maneira:
@RestController
public class CourseController {
@Autowired
CourseRepository courseRepo;
@RequestMapping("/courses/{id}")
Course customer(@PathVariable Integer id) {
return courseRepo.getCourse(id);
}
}
E novamente - o importante aqui é que o DTO do Curso que estamos retornando da camada do controlador não é um POJO padrão. Esse será o gatilho para que seja convertido em mensagens de buffer de protocolo antes de ser transferido de volta para o cliente.
*4. Clientes e testes REST *
Agora que vimos a implementação simples da API - vamos ilustrar* a desserialização de mensagens de buffer de protocolo no lado do cliente *- usando dois métodos.
O primeiro aproveita a API RestTemplate com um bean ProtobufHttpMessageConverter pré-configurado para converter automaticamente as mensagens.
O segundo está usando protobuf-java-format para transformar manualmente as respostas do buffer de protocolo em documentos JSON.
Para começar, precisamos configurar o contexto para um teste de integração e instruir o Spring Boot a encontrar informações de configuração na classe Application declarando uma classe de teste da seguinte maneira:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebIntegrationTest
public class ApplicationTest {
//Other declarations
}
Todos os trechos de código nesta seção serão colocados na classe ApplicationTest.
====* 4.1 Resposta esperada *
A primeira etapa para acessar um serviço REST é determinar o URL da solicitação:
private static final String COURSE1_URL = "http://localhost:8080/courses/1";
Este COURSE1_URL será usado para obter o primeiro curso duplo de teste do serviço REST que criamos anteriormente. Depois que uma solicitação GET é enviada para o URL acima, a resposta correspondente é verificada usando as seguintes asserções:
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("[email protected]"));
assertThat(response, containsString("[email protected]"));
assertThat(response, containsString("[email protected]"));
assertThat(response, containsString("phone"));
assertThat(response, containsString("number"));
assertThat(response, containsString("type"));
}
Usaremos esse método auxiliar nos dois casos de teste abordados nas subseções seguintes.
====* 4.2 Testando com RestTemplate *
Aqui está como criamos um cliente, enviamos uma solicitação GET para o destino designado, recebemos a resposta na forma de mensagens de buffer de protocolo e verificamos usando a API RestTemplate:
@Autowired
private RestTemplate restTemplate;
@Test
public void whenUsingRestTemplate_thenSucceed() {
ResponseEntity<Course> course = restTemplate.getForEntity(COURSE1_URL, Course.class);
assertResponse(course.toString());
}
Para fazer esse caso de teste funcionar, precisamos que um bean do tipo RestTemplate seja registrado em uma classe de configuração:
@Bean
RestTemplate restTemplate(ProtobufHttpMessageConverter hmc) {
return new RestTemplate(Arrays.asList(hmc));
}
Outro bean do tipo ProtobufHttpMessageConverter também é necessário para transformar automaticamente as mensagens de buffer de protocolo recebidas. Este bean é o mesmo que o definido na subseção 3.1. Como o cliente e o servidor compartilham o mesmo contexto de aplicativo neste tutorial, podemos declarar o bean RestTemplate na classe Application e reutilizar o bean ProtobufHttpMessageConverter.
====* 4.3 Testando com HttpClient *
A primeira etapa para usar a API HttpClient e converter manualmente as mensagens do buffer de protocolo é adicionar as duas dependências a seguir ao arquivo POM do Maven:
<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>
Para obter as versões mais recentes dessas dependências, consulte https://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22com.googlecode.protobuf-java-format%22%20AND% 20a% 3A% 22protobuf-java-format% 22 [formato protobuf-java] e https://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22org.apache.httpcomponents%22% 20AND% 20a% 3A% 22httpclient% 22 [httpclient] artefatos no repositório central do Maven.
Vamos criar um cliente, executar uma solicitação GET e converter a resposta associada em uma instância InputStream usando o URL fornecido:
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();
}
Agora, converteremos mensagens de buffer de protocolo na forma de um objeto InputStream em um documento JSON:
private String convertProtobufMessageStreamToJsonString(InputStream protobufStream) throws IOException {
JsonFormat jsonFormat = new JsonFormat();
Course course = Course.parseFrom(protobufStream);
return jsonFormat.printToString(course);
}
E aqui está como um caso de teste usa métodos auxiliares privados declarados acima e valida a resposta:
@Test
public void whenUsingHttpClient_thenSucceed() throws IOException {
InputStream responseStream = executeHttpRequest(COURSE1_URL);
String jsonOutput = convertProtobufMessageStreamToJsonString(responseStream);
assertResponse(jsonOutput);
}
====* 4.4 Resposta em JSON *
Para deixar claro, as formas JSON das respostas que recebemos nos testes descritos nas subseções anteriores estão incluídas aqui:
id: 1
course_name: "REST with Spring"
student {
id: 1
first_name: "John"
last_name: "Doe"
email: "[email protected]"
phone {
number: "123456"
}
}
student {
id: 2
first_name: "Richard"
last_name: "Roe"
email: "[email protected]"
phone {
number: "234567"
type: LANDLINE
}
}
student {
id: 3
first_name: "Jane"
last_name: "Doe"
email: "[email protected]"
phone {
number: "345678"
}
phone {
number: "456789"
type: LANDLINE
}
}
===* 5. Conclusão*
Este tutorial apresentou rapidamente os Buffers de Protocolo e ilustrou a configuração de uma API REST usando o formato com Spring. Em seguida, mudamos para o suporte ao cliente e o mecanismo de serialização-desserialização.
A implementação de todos os exemplos e trechos de código pode ser encontrada em um projeto do GitHub.