spring-boot 使用protobuf

  1. 2018-03-24 [spring-boot](http://thoreauz.com/tags/spring-boot/) [protobuf](http://thoreauz.com/tags/protobuf/) [spring](http://thoreauz.com/tags/spring/)

概述

本文简单介绍通过gradle构建以protobuf作为数据通讯格式的spring boot服务。

protobuf简介

Protocol Buffers是Google出品的一种序列化数据结构的协议。和xml,json等通讯格式一样,支持夸语言。但protobuf更小,更快,更简单。官方声称,比xml格式小3-10倍,速度快20到100倍。
具体性能对比可以参考:https://github.com/eishay/jvm-serializers/wiki.
除了更快更小,protobuf的还有其他优点:
1. 可以通过结构描述生成代码。专注于文档设计,自动生成对象模型。
2. 兼容性好
当然,也有缺点,如二进制格式导致可读性差,必须配合描述文件.proto
本文不详细介绍语法格式,可见官网proto3及其翻译Protobuf3 语法指南

spring-boot-proto 示例

gradle 配置:
build.gradle如下:

  1. buildscript {
  2. ext {
  3. springBootVersion = '2.0.0.RELEASE'
  4. }
  5. repositories {
  6. mavenCentral()
  7. }
  8. dependencies {
  9. classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
  10. // protobuf-gradle-plugin
  11. classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.5'
  12. }
  13. }
  14. apply plugin: 'java'
  15. apply plugin: 'eclipse'
  16. apply plugin: 'idea'
  17. apply plugin: 'org.springframework.boot'
  18. apply plugin: 'io.spring.dependency-management'
  19. // add protobuf plugin
  20. apply plugin: 'com.google.protobuf'
  21. group = 'com.thoreau'
  22. version = '0.0.1-SNAPSHOT'
  23. sourceCompatibility = 1.8
  24. repositories {
  25. mavenCentral()
  26. }
  27. dependencies {
  28. compile('org.springframework.boot:spring-boot-starter-web')
  29. compile('com.google.protobuf:protobuf-java:3.4.0')
  30. compile('com.googlecode.protobuf-java-format:protobuf-java-format:1.4')
  31. compile 'com.squareup.okhttp3:okhttp:3.10.0'
  32. testCompile('org.springframework.boot:spring-boot-starter-test')
  33. }
  34. // pre-compiled protoc
  35. protobuf {
  36. // Configure the protoc executable
  37. protoc {
  38. // Download from repositories
  39. artifact = 'com.google.protobuf:protoc:3.0.0'
  40. // generated java files dir
  41. // generatedFilesBaseDir = "$projectDir/gen"
  42. }
  43. }
  44. clean {
  45. delete protobuf.generatedFilesBaseDir
  46. }
  47. test {
  48. reports {
  49. junitXml.enabled = false
  50. html.enabled = true
  51. }
  52. }
  53. sourceSets{
  54. main {
  55. java {
  56. srcDir 'src/main/java'
  57. }
  58. resources {
  59. srcDir 'src/main/resources'
  60. }
  61. proto {
  62. // In addition to the default 'src/main/proto'
  63. srcDir 'src/main/proto'
  64. }
  65. }
  66. test {
  67. proto {
  68. // In addition to the default 'src/test/proto'
  69. srcDir 'src/test/proto'
  70. }
  71. java {
  72. srcDir 'src/test/java'
  73. }
  74. resources {
  75. srcDir 'src/main/resources'
  76. }
  77. }
  78. }

spring boot 使用protobuf

  1. @SpringBootApplication
  2. public class Application {
  3. public static void main(String[] args) {
  4. SpringApplication.run(Application.class);
  5. }
  6. // 使用 protobuf 作为消息协议(序列化)
  7. @Bean
  8. ProtobufHttpMessageConverter protobufHttpMessageConverter() {
  9. return new ProtobufHttpMessageConverter();
  10. }
  11. // 配置restTeamplete 解析 protobuf(反序列化)
  12. @Bean
  13. RestTemplate restTemplate(ProtobufHttpMessageConverter hmc) {
  14. return new RestTemplate(Collections.singletonList(hmc));
  15. }
  16. }

proto文件

根据gradle的配置,在src/proto目录下新建两个文件:
hobby.proto

  1. syntax = "proto3";// 版本
  2. package com.thoreau.protobuf.vo; // 命名空间
  3. option java_package = "com.thoreau.protobuf.generated.vo";//生成java包名
  4. option java_outer_classname = "HobbyProto"; //生成Java类名
  5. option java_multiple_files = true;
  6. message Hobby {
  7. string name = 1;
  8. int32 level =2;
  9. }

user.proto

  1. syntax = "proto3";
  2. package com.thoreau.protobuf.vo;
  3. import "com/thoreau/protobuf/vo/hobby.proto";// 导入上一个文件
  4. option java_package = "com.thoreau.protobuf.generated.vo";
  5. option java_outer_classname = "UserProto";
  6. option java_multiple_files = true;
  7. message User {
  8. reserved "Person";// 保留标识符
  9. string firstName = 1;
  10. string lastName = 2;
  11. string emailAddress = 3;
  12. string homeAddress = 4;
  13. repeated Hobby hobbies =5;
  14. repeated Skill skills =6;
  15. enum Skill {
  16. GOLANG = 0;
  17. PYTHON = 1;
  18. JAVA = 2;
  19. RUST = 3;
  20. CPP = 4;
  21. }
  22. }

gradle 编译:

  1. gradle build

编译后在build目录下对应包中生成如下文件:

  1. Hobby.class
  2. HobbyOrBuilder.class
  3. HobbyProto.class
  4. User.class
  5. UserOrBuilder.class
  6. UserProto.class

controller

  1. @RestController
  2. @RequestMapping("/user")
  3. public class UserResource {
  4. @GetMapping(produces = "application/x-protobuf")// 指定response的Content-Type(消息类型)
  5. public User getPersonProto() {
  6. return User.newBuilder()
  7. .setFirstName("thoreau")
  8. .setLastName("zz")
  9. .setEmailAddress("thoreau@gmail.com")
  10. .setHomeAddress("123 xxx Street")
  11. .addHobbies(Hobby.newBuilder().setName("basketball").build())
  12. .addHobbies(Hobby.newBuilder().setName("football").build())
  13. .addSkills(User.Skill.JAVA)
  14. .addSkills(User.Skill.GOLANG)
  15. .build();
  16. }
  17. }

启动服务,访问127.0.0.1:8080/user,可拿到protobuf的二进制消息。

测试

  1. package com.thoreau.protobuf;
  2. import com.googlecode.protobuf.format.JsonFormat;
  3. import com.thoreau.protobuf.generated.vo.User;
  4. import okhttp3.OkHttpClient;
  5. import okhttp3.Request;
  6. import okhttp3.Response;
  7. import org.junit.Test;
  8. import org.junit.runner.RunWith;
  9. import org.springframework.beans.factory.annotation.Autowired;
  10. import org.springframework.boot.test.context.SpringBootTest;
  11. import org.springframework.boot.test.web.client.TestRestTemplate;
  12. import org.springframework.boot.web.server.LocalServerPort;
  13. import org.springframework.http.ResponseEntity;
  14. import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
  15. import java.io.IOException;
  16. import java.io.InputStream;
  17. /**
  18. * 2018/3/23 13:55.
  19. *
  20. * @author zhaozhou
  21. */
  22. @RunWith(SpringJUnit4ClassRunner.class)
  23. @SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
  24. public class ApplicationTest {
  25. @LocalServerPort
  26. private int port;
  27. @Autowired
  28. private TestRestTemplate restTemplate;
  29. @Test
  30. public void getUserTest() {
  31. ResponseEntity<User> user = restTemplate.getForEntity("/user", User.class);
  32. // assert
  33. }
  34. @Test
  35. public void getUserJson() throws IOException {
  36. OkHttpClient client = new OkHttpClient();
  37. Request request = new Request.Builder()
  38. .url("http://127.0.0.1:" + port + "/user")
  39. .build();
  40. Response response = client.newCall(request).execute();
  41. InputStream inputStream = null;
  42. try {
  43. inputStream = response.body().byteStream();
  44. JsonFormat jsonFormat = new JsonFormat();
  45. User user = User.parseFrom(inputStream);
  46. // assert
  47. System.out.println(jsonFormat.printToString(user));
  48. } finally {
  49. if (inputStream != null) {
  50. inputStream.close();
  51. }
  52. }
  53. }
  54. }

总结

对于前后端的通讯,比如js 到Java,是否应该使用protobuf有待商榷。后端服务间rpc等调用,protobuf一定是不错的选择。spring cloud 微服务间调用使用http,就可以像上文spring boot的示例一样使用protobuf通讯和序列化。


参考文档:
https://developers.google.com/protocol-buffers/docs/proto3
https://dzone.com/articles/will-salesforce-kiss-the-mule-or-kill-the-mule
https://auth0.com/blog/beating-json-performance-with-protobuf/
http://www.baeldung.com/spring-rest-api-with-protocol-buffers
http://colobu.com/2017/03/16/Protobuf3-language-guide/
https://github.com/eishay/jvm-serializers/wiki