领先一步
VMware 提供培训和认证,帮助您快速提升进度。
了解更多本周我正在巴西圣保罗参加 QCon SP 大会。我与一位非常喜欢 Spring REST 栈的人进行了有趣的讨论,他想知道是否有比普通 JSON 更高效的方法。确实有!我经常被问到 Spring 对高速基于二进制的消息编码的支持。Spring 长期以来一直支持使用 Hessian、Burlap 等进行 RPC 编码,而 Spring Framework 4.1 引入了对 Google Protocol Buffers 的支持,它也可以与 REST 服务一起使用。
来自 Google Protocol Buffer 网站
Protocol buffers 是 Google 的一种语言中立、平台中立、可扩展的序列化结构化数据机制——可以将其理解为 XML,但它更小、更快、更简单。您只需定义一次数据结构,然后可以使用特殊生成的源代码轻松地将结构化数据写入和读取到各种数据流中,并使用各种语言……
Google 在其自身的内部服务中心架构中广泛使用 Protocol Buffers。
一个 .proto
文档描述了要编码的类型(消息),并包含一个定义语言,对于任何使用过 C struct
的人都应该很熟悉。在文档中,您定义类型、这些类型中的字段以及它们在类型中彼此相对的顺序(内存偏移量!)。
.proto
文件不是实现——它们是可能通过网络传输的消息的声明性描述。它们可以规定和验证约束——给定字段的类型或该字段的基数——在编码和解码的消息上。您必须使用 Protobuf 编译器为您的选择的语言生成相应的客户端。
您可以根据需要使用 Google Protocol Buffers,但在本文中,我们将探讨将其用作编码 REST 服务有效负载的一种方式。这种方法非常强大:您可以使用内容协商为接受它的客户端(以任意数量的语言)提供高速 Protocol Buffer 有效负载,并为那些不接受的客户端提供更传统的 JSON 等格式。
Protocol Buffer 消息相较于典型的 JSON 编码消息提供了许多改进,尤其是在一个多语言系统中,微服务是用各种技术实现的,但需要能够以一致且长期的方式推断服务之间的通信。
Protocol Buffers 有一些很好的特性,可以促进稳定的 API
required
、optional
和 repeated
关键字指定验证。每个客户端都以自己的方式执行这些约束。您可能会认为您可以在同构服务环境中只使用 Java 的内置序列化机制,但是,正如 Protocol Buffers 团队在首次引入该技术时迅速指出的那样,即使这样也存在一些问题。Java 语言权威 Josh Bloch 的史诗巨著《Effective Java》第 213 页提供了更多详细信息。
让我们首先看看我们的 .proto
文档
package demo;
option java_package = "demo";
option java_outer_classname = "CustomerProtos";
message Customer {
required int32 id = 1;
required string firstName = 2;
required string lastName = 3;
enum EmailType {
PRIVATE = 1;
PROFESSIONAL = 2;
}
message EmailAddress {
required string email = 1;
optional EmailType type = 2 [default = PROFESSIONAL];
}
repeated EmailAddress email = 5;
}
message Organization {
required string name = 1;
repeated Customer customer = 2;
}
然后,您将此定义传递给 protoc
编译器并指定输出类型,如下所示
protoc -I=$IN_DIR --java_out=$OUT_DIR $IN_DIR/customer.proto
这是我为代码生成各种客户端而编写的 Bash 脚本
#!/usr/bin/env bash
SRC_DIR=`pwd`
DST_DIR=`pwd`/../src/main/
echo source: $SRC_DIR
echo destination root: $DST_DIR
function ensure_implementations(){
# Ruby and Go aren't natively supported it seems
# Java and Python are
gem list | grep ruby-protocol-buffers || sudo gem install ruby-protocol-buffers
go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
}
function gen(){
D=$1
echo $D
OUT=$DST_DIR/$D
mkdir -p $OUT
protoc -I=$SRC_DIR --${D}_out=$OUT $SRC_DIR/customer.proto
}
ensure_implementations
gen java
gen python
gen ruby
这将在 src/main/{java,ruby,python}
文件夹中生成相应的客户端类。让我们首先看看 Spring MVC REST 服务本身。
在我们的示例中,我们将注册 Spring framework 4.1 的 org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter
的一个实例。此类型是 HttpMessageConverter
。HttpMessageConverter
编码和解码 REST 服务调用中的请求和响应。它们通常在某种内容协商发生后激活:例如,如果客户端指定 Accept: application/x-protobuf
,那么我们的 REST 服务将发送回 Protocol Buffer 编码的响应。
package demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
ProtobufHttpMessageConverter protobufHttpMessageConverter() {
return new ProtobufHttpMessageConverter();
}
private CustomerProtos.Customer customer(int id, String f, String l, Collection<String> emails) {
Collection<CustomerProtos.Customer.EmailAddress> emailAddresses =
emails.stream().map(e -> CustomerProtos.Customer.EmailAddress.newBuilder()
.setType(CustomerProtos.Customer.EmailType.PROFESSIONAL)
.setEmail(e).build())
.collect(Collectors.toList());
return CustomerProtos.Customer.newBuilder()
.setFirstName(f)
.setLastName(l)
.setId(id)
.addAllEmail(emailAddresses)
.build();
}
@Bean
CustomerRepository customerRepository() {
Map<Integer, CustomerProtos.Customer> customers = new ConcurrentHashMap<>();
// populate with some dummy data
Arrays.asList(
customer(1, "Chris", "Richardson", Arrays.asList("[email protected]")),
customer(2, "Josh", "Long", Arrays.asList("[email protected]")),
customer(3, "Matt", "Stine", Arrays.asList("[email protected]")),
customer(4, "Russ", "Miles", Arrays.asList("[email protected]"))
).forEach(c -> customers.put(c.getId(), c));
// our lambda just gets forwarded to Map#get(Integer)
return customers::get;
}
}
interface CustomerRepository {
CustomerProtos.Customer findById(int id);
}
@RestController
class CustomerRestController {
@Autowired
private CustomerRepository customerRepository;
@RequestMapping("/customers/{id}")
CustomerProtos.Customer customer(@PathVariable Integer id) {
return this.customerRepository.findById(id);
}
}
此代码的大部分内容都非常简单明了。它是一个 Spring Boot 应用程序。Spring Boot 自动注册 HttpMessageConverter
bean,因此我们只需要定义 ProtobufHttpMessageConverter
bean,它就会被适当地配置。@Configuration
类播种一些虚拟数据和一个模拟的 CustomerRepository
对象。我不会在此处复制我们的 Protocol Buffer 的 Java 类型 demo/CustomerProtos.java
,因为它是由代码生成的位操作和解析代码;阅读起来并没有那么有趣。一个方便之处在于,Java 实现自动为在 Java 中快速创建这些类型的实例提供了构建器方法。
代码生成的类型是类似于普通 struct
的对象。它们适合用作 DTO,但不应作为 API 的基础。请不要使用 Java 继承扩展它们以引入新功能;这会破坏实现,而且无论如何这都是糟糕的面向对象编程实践。如果您想保持代码更简洁,只需根据需要包装和适配它们,也许在该包装器中根据需要处理从 ORM 实体到 Protocol Buffer 客户端类型的转换。
HttpMessageConverter
也可以与 Spring 的 REST 客户端 RestTemplate
一起使用。这是相应的 Java 语言单元测试
package demo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.web.client.RestTemplate;
import java.util.Arrays;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = DemoApplication.class)
@WebAppConfiguration
@IntegrationTest
public class DemoApplicationTests {
@Configuration
public static class RestClientConfiguration {
@Bean
RestTemplate restTemplate(ProtobufHttpMessageConverter hmc) {
return new RestTemplate(Arrays.asList(hmc));
}
@Bean
ProtobufHttpMessageConverter protobufHttpMessageConverter() {
return new ProtobufHttpMessageConverter();
}
}
@Autowired
private RestTemplate restTemplate;
private int port = 8080;
@Test
public void contextLoaded() {
ResponseEntity<CustomerProtos.Customer> customer = restTemplate.getForEntity(
"http://127.0.0.1:" + port + "/customers/2", CustomerProtos.Customer.class);
System.out.println("customer retrieved: " + customer.toString());
}
}
事情按预期进行,不仅在 Java 和 Spring 中,而且在 Ruby 和 Python 中也是如此。为了完整起见,这里有一个使用 Ruby 的简单客户端(省略了客户端类型)
#!/usr/bin/env ruby
require './customer.pb'
require 'net/http'
require 'uri'
uri = URI.parse('https://127.0.0.1:8080/customers/3')
body = Net::HTTP.get(uri)
puts Demo::Customer.parse(body)
… 这里有一个使用 Python 的客户端(省略了客户端类型)
#!/usr/bin/env python
import urllib
import customer_pb2
if __name__ == '__main__':
customer = customer_pb2.Customer()
customers_read = urllib.urlopen('https://127.0.0.1:8080/customers/1').read()
customer.ParseFromString(customers_read)
print customer
如果您需要非常高速的消息编码,并且可以与多种语言一起使用,那么 Protocol Buffers 是一个引人注目的选择。还有其他编码技术,如 Avro 或 Thrift,但没有一种像 Protocol Buffers 那样成熟和普及。您也不必一定要将 Protocol Buffers 与 REST 一起使用。您可以将其插入某种 RPC 服务中,如果那是您的风格的话。客户端实现的数量几乎与 Cloud Foundry 的构建包数量一样多——因此您几乎可以在 Cloud Foundry 上运行任何东西,并在所有服务中享受相同的高速、一致的消息传递!
此示例的 代码可在网上获取,因此请随时查看!
大家好,在 2015 年,我一直在尝试每周发布一篇随机的技术提示风格的文章,这些文章基于我在社区中看到的引起人们兴趣的事情,无论是这里还是在 Pivotal 博客 上。我将这些每周(好吧!好吧!——像《本周 Spring》一样定期发布它们并不容易,但到目前为止我还没有错过任何一周!:-))的文章作为一次机会,专注于不是某个特定新版本的发布,而是 Spring 在服务于某些可能跨领域或可能受益于对其进行重点关注的社区用例中的应用。到目前为止,我们已经了解了各种各样的东西——Vaadin、Activiti、12-Factor App 风格的配置、更智能的服务到服务调用、Couchbase 等等——我们也有一些有趣的内容正在排队。然而,我想知道您还想看到哪些内容被讨论。如果您有一些关于您想看到哪些内容被涵盖的想法,或者您自己要贡献的社区文章,请通过 Twitter (@starbuxman) 或电子邮件 (jlong [at] pivotal [dot] io) 联系我。我将一如既往地为您服务。