protobuf与spring boot整合使用

4,862 阅读3分钟

因为工作原因,需要在websocket通信中传输protobuf格式的数据,所以了解了一下protobuf与spring boot的整合使用

是什么

官网

一种与语言无关,与平台无关的可扩展机制,用于序列化结构化数据

使用

protobuf基本使用

从官网获取的基本使用方法

  1. 转换工具安装

为了将通用配置文件转换为特定的语言实现,需要在本地安装工具

推荐使用scoop快速安装

scoop install protobuf
  1. 文件转换

示例:在项目根目录执行下述命令:

protoc -I=proto --java_out=src/main/java proto/LineProto.proto

命令格式

protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto

$SRC_DIR为proto文件放置的目录,$DST_DIR为类文件根目录

生成类的包名、类名在proto文件中指定

option java_package = "com.sayyi.proto";
option java_outer_classname = "MsgProtoClass";

与spring boot整合

源码地址,spring boot版本为1.5.10.RELEASE

  • master: http代码
  • feature/socket: websocket代码

http

参考spring boot官方提供的案例实现

  1. 添加依赖
        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>3.9.1</version>
        </dependency>
        <dependency>
            <groupId>com.googlecode.protobuf-java-format</groupId>
            <artifactId>protobuf-java-format</artifactId>
            <version>1.4</version>
        </dependency>
  1. 配置数据转换器

    /**
     * 启用protobuf类型数据转换
     * @return
     */
    @Bean
    ProtobufHttpMessageConverter protobufHttpMessageConverter() {
        return new ProtobufHttpMessageConverter();
    }

之后即可处理protobuf数据

如果想要返回protobuf数据,只要声明响应类型即可

@PostMapping(value = "/msgProtos", produces="application/x-protobuf;charset=UTF-8")

websocket

原生

仿照ProtobufHttpMessageConverter实现二进制数据的处理方法即可

package com.sayyi.socket;

import com.google.protobuf.ExtensionRegistry;
import com.google.protobuf.Message;
import org.springframework.util.FileCopyUtils;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;

public class ProtobufHelper {

    /**
     * 转换信息缓存
     */
    private static final ConcurrentHashMap<Class<?>, Method> methodCache = new ConcurrentHashMap<>();

    private final ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance();

    public ProtobufHelper() {
    }

    public <T extends Message> byte[] toBytes(T message) throws Exception {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        FileCopyUtils.copy(message.toByteArray(), out);
        return out.toByteArray();
    }

    @SuppressWarnings("unchecked")
    public <T extends Message> T toObj(Class<T> targetClass, byte[] data) throws Exception {
        Message.Builder builder = getMessageBuilder(targetClass);
        builder.mergeFrom(new ByteArrayInputStream(data), this.extensionRegistry);
        return (T) builder.build();
    }

    private static Message.Builder getMessageBuilder(Class<? extends Message> clazz) throws Exception {
        Method method = methodCache.get(clazz);
        if (method == null) {
            method = clazz.getMethod("newBuilder");
            methodCache.put(clazz, method);
        }
        return (Message.Builder) method.invoke(clazz);
    }
}

stomp

最好不要使用stomp协议传输。stomp仅支持string类型的数据,后台可以通过自定义MessageConverter来实现二进制转对象,但是前端不能很好的处理二进制数据,只能通过String转换。这里提供参考org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter实现后端转换器

import com.google.protobuf.ExtensionRegistry;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.protobuf.ExtensionRegistryInitializer;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.converter.AbstractMessageConverter;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.MimeType;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @ClassName: ProtobufMessageConverter
 * @Description:
 * @author: XC
 * @date: 2019-10-24 17:51:26
 */
public class ProtobufMessageConverter extends AbstractMessageConverter {

    public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

    public static final MediaType PROTOBUF = new MediaType("application", "x-protobuf", DEFAULT_CHARSET);

    private static final ConcurrentHashMap<Class<?>, Method> methodCache = new ConcurrentHashMap<>();

    private final ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance();

    /**
     * Construct a new instance.
     */
    public ProtobufMessageConverter() {
        this(null);
    }

    /**
     * Construct a new instance with an {@link ExtensionRegistryInitializer}
     * that allows the registration of message extensions.
     */
    public ProtobufMessageConverter(ExtensionRegistryInitializer registryInitializer) {
        super(PROTOBUF);
        if (registryInitializer != null) {
            registryInitializer.initializeExtensionRegistry(this.extensionRegistry);
        }
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return com.google.protobuf.Message.class.isAssignableFrom(clazz);
    }

    @Override
    protected Object convertFromInternal(Message<?> message, Class<?> targetClass, Object conversionHint) {
        MediaType contentType = getMediaType(getMimeType(message.getHeaders()));
        Charset charset = contentType.getCharset();
        if (charset == null) {
            charset = DEFAULT_CHARSET;
        }
        try {
            @SuppressWarnings("unchecked") com.google.protobuf.Message.Builder builder = getMessageBuilder((Class<? extends com.google.protobuf.Message>) targetClass);
            Object payload = message.getPayload();
            byte[] data;
            if (payload instanceof byte[]) {
                data = (byte[]) payload;
            } else {
                data = ((String) payload).getBytes(charset);
            }

            builder.mergeFrom(new ByteArrayInputStream(data), this.extensionRegistry);
            return builder.build();
        } catch (Exception ex) {
            throw new HttpMessageNotReadableException("Could not read Protobuf message: " + ex.getMessage(), ex);
        }
    }

    @Override
    protected Object convertToInternal(Object payload, MessageHeaders headers, Object conversionHint) {
        com.google.protobuf.Message message = (com.google.protobuf.Message) payload;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try {
            FileCopyUtils.copy(message.toByteArray(), out);
        } catch (IOException e) {
            e.printStackTrace();
        }
        payload = out.toByteArray();
        return payload;
    }

    private MediaType getMediaType(MimeType type) {
        return type == null ?
                PROTOBUF : new MediaType(type.getType(), type.getSubtype(), type.getCharset());
    }

    private static com.google.protobuf.Message.Builder getMessageBuilder(Class<? extends com.google.protobuf.Message> clazz) throws Exception {
        Method method = methodCache.get(clazz);
        if (method == null) {
            method = clazz.getMethod("newBuilder");
            methodCache.put(clazz, method);
        }
        return (com.google.protobuf.Message.Builder) method.invoke(clazz);
    }

}