spring-boot 使用protobuf

概述

本文简单介绍通过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如下:

buildscript {
	ext {
		springBootVersion = '2.0.0.RELEASE'
	}
	repositories {
		mavenCentral()
	}
	dependencies {
		classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
		// protobuf-gradle-plugin
		classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.5'
	}
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
// add protobuf plugin
apply plugin: 'com.google.protobuf'

group = 'com.thoreau'
version = '0.0.1-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
	mavenCentral()
}


dependencies {
	compile('org.springframework.boot:spring-boot-starter-web')
	compile('com.google.protobuf:protobuf-java:3.4.0')
	compile('com.googlecode.protobuf-java-format:protobuf-java-format:1.4')
    compile 'com.squareup.okhttp3:okhttp:3.10.0'
	testCompile('org.springframework.boot:spring-boot-starter-test')
}
// pre-compiled protoc
protobuf {
	// Configure the protoc executable
	protoc {
		// Download from repositories
		artifact = 'com.google.protobuf:protoc:3.0.0'
		// generated java files dir
//		generatedFilesBaseDir = "$projectDir/gen"
	}
}
clean {
    delete protobuf.generatedFilesBaseDir
}
test {
    reports {
        junitXml.enabled = false
        html.enabled = true
    }
}
sourceSets{
    main {
        java {
            srcDir 'src/main/java'
        }
        resources {
            srcDir 'src/main/resources'
        }
        proto {
            // In addition to the default 'src/main/proto'
            srcDir 'src/main/proto'
        }
    }
    test {
        proto {
            // In addition to the default 'src/test/proto'
            srcDir 'src/test/proto'
        }
        java {
            srcDir 'src/test/java'
        }
        resources {
            srcDir 'src/main/resources'
        }
    }
}

spring boot 使用protobuf

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
    // 使用 protobuf 作为消息协议(序列化)
    @Bean
    ProtobufHttpMessageConverter protobufHttpMessageConverter() {
        return new ProtobufHttpMessageConverter();
    }
    // 配置restTeamplete 解析 protobuf(反序列化)
    @Bean
    RestTemplate restTemplate(ProtobufHttpMessageConverter hmc) {
        return new RestTemplate(Collections.singletonList(hmc));
    }
}

proto文件

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

hobby.proto

syntax = "proto3";// 版本
package com.thoreau.protobuf.vo; // 命名空间


option java_package = "com.thoreau.protobuf.generated.vo";//生成java包名
option java_outer_classname = "HobbyProto"; //生成Java类名
option java_multiple_files = true;

message Hobby {
    string name = 1;
    int32 level =2;
}

user.proto

syntax = "proto3";
package com.thoreau.protobuf.vo;

import "com/thoreau/protobuf/vo/hobby.proto";// 导入上一个文件

option java_package = "com.thoreau.protobuf.generated.vo";
option java_outer_classname = "UserProto";
option java_multiple_files = true;

message User {
    reserved "Person";// 保留标识符
    string firstName = 1;
    string lastName = 2;
    string emailAddress = 3;
    string homeAddress = 4;
    repeated Hobby hobbies =5;
    repeated Skill skills =6;
    enum Skill {
        GOLANG = 0;
        PYTHON = 1;
        JAVA = 2;
        RUST = 3;
        CPP = 4;
    }

}

gradle 编译:

gradle build

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

Hobby.class
HobbyOrBuilder.class
HobbyProto.class
User.class
UserOrBuilder.class
UserProto.class

controller

@RestController
@RequestMapping("/user")
public class UserResource {
    @GetMapping(produces = "application/x-protobuf")// 指定response的Content-Type(消息类型)
    public User getPersonProto() {
              return User.newBuilder()
                   .setFirstName("thoreau")
                   .setLastName("zz")
                   .setEmailAddress("thoreau@gmail.com")
                   .setHomeAddress("123 xxx Street")
                   .addHobbies(Hobby.newBuilder().setName("basketball").build())
                   .addHobbies(Hobby.newBuilder().setName("football").build())
                   .addSkills(User.Skill.JAVA)
                   .addSkills(User.Skill.GOLANG)
                   .build();
    }
}

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

测试

package com.thoreau.protobuf;

import com.googlecode.protobuf.format.JsonFormat;
import com.thoreau.protobuf.generated.vo.User;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.io.IOException;
import java.io.InputStream;

/**
 * 2018/3/23 13:55.
 *
 * @author zhaozhou
 */
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ApplicationTest {
    @LocalServerPort
    private int port;
    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void getUserTest() {
        ResponseEntity<User> user = restTemplate.getForEntity("/user", User.class);
        // assert
    }

    @Test
    public void getUserJson() throws IOException {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url("http://127.0.0.1:" + port + "/user")
                .build();
        Response response = client.newCall(request).execute();
        InputStream inputStream = null;
        try {
            inputStream = response.body().byteStream();
            JsonFormat jsonFormat = new JsonFormat();
            User user = User.parseFrom(inputStream);
            // assert
            System.out.println(jsonFormat.printToString(user));
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
        }
    }
}

总结

对于前后端的通讯,比如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

CONTENTS