一、创建springboot项目
1、创建proto文件目录 和 proto文件生成的java对象包
2、pom 中添加依赖包
## 版本
<grpc-version>1.6.1</grpc-version>
<protobuf-version>3.12.4</protobuf-version>
## 依赖
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>${protobuf-version}</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>${protobuf-version}</version>
</dependency>
<dependency>
<groupId>com.googlecode.protobuf-java-format</groupId>
<artifactId>protobuf-java-format</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty</artifactId>
<version>${grpc-version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>${grpc-version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>${grpc-version}</version>
</dependency>
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>1.7.4</version>
</dependency>
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>1.7.4</version>
</dependency>
3、编译proto文件,会自动生成class文件放到 proto包下
二、protobuf文件定义
1、proto3 对应各语言类型
.proto Type | Notes | C++ Type | Java Type | Python Type[2] | Go Type |
---|---|---|---|---|---|
double | double | double | float | *float64 | |
float | float | float | float | *float32 | |
int32 | 使用可变长度编码。编码负数的效率低 - 如果你的字段可能有负值,请改用 sint32 | int32 | int | int | *int32 |
int64 | 使用可变长度编码。编码负数的效率低 - 如果你的字段可能有负值,请改用 sint64 | int64 | long | int/long[3] | *int64 |
uint32 | 使用可变长度编码 | uint32 | int[1] | int/long[3] | *uint32 |
uint64 | 使用可变长度编码 | uint64 | long[1] | int/long[3] | *uint64 |
sint32 | 使用可变长度编码。有符号的 int 值。这些比常规 int32 对负数能更有效地编码 | int32 | int | int | *int32 |
sint64 | 使用可变长度编码。有符号的 int 值。这些比常规 int64 对负数能更有效地编码 | int64 | long | int/long[3] | *int64 |
fixed32 | 总是四个字节。如果值通常大于 228,则比 uint32 更有效。 | uint32 | int[1] | int/long[3] | *uint32 |
fixed64 | 总是八个字节。如果值通常大于 256,则比 uint64 更有效。 | uint64 | long[1] | int/long[3] | *uint64 |
sfixed32 | 总是四个字节 | int32 | int | int | *int32 |
sfixed64 | 总是八个字节 | int64 | long | int/long[3] | *int64 |
bool | bool | boolean | bool | *bool | |
string | 字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本 | string | String | str/unicode[4] | *string |
bytes | 可以包含任意字节序列 | string | ByteString | str | []byte |
2、定义proto文件
# 统一返回报文对象定义
syntax = "proto3";
option java_outer_classname = "ResultVo";
option java_package = "com.demo.proto";
import "google/protobuf/any.proto";
message ResultResponse {
int32 code = 1;
string msg = 2;
google.protobuf.Any data = 3;# Any类型可代表proto对象
}
#返回业务Vo对象定义
syntax = "proto3";
option java_outer_classname = "AccountVo"; # java_outer_classname 的名字 要与 message 名字不同不然会编译不过
option java_package = "com.demo.proto";
message Account {
//账号
uint64 ID = 1;
//名字
string name = 2;
//密码
string password = 3;
//临时
string test = 4;
}
syntax = "proto3";
option java_outer_classname = "UserVo";
option java_package = "com.demo.proto";
import "Account.proto"; # 引入依赖对象
message User {
Account user = 1;
}
三、自定义消息转换器HttpMessageConverter
转换器
package com.demo.configs.converter;
import com.demo.common.util.ZlibUtil;
import com.google.common.base.Stopwatch;
import com.google.protobuf.CodedOutputStream;
import com.google.protobuf.Descriptors;
import com.google.protobuf.Message;
import net.jpountz.lz4.LZ4BlockOutputStream;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConversionException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.xerial.snappy.SnappyOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.util.Map;
public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<Message> {
private Logger logger = LoggerFactory.getLogger(getClass());
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
public static final MediaType MEDIA_TYPE = new MediaType("application", "x-protobuf", DEFAULT_CHARSET);
public static final MediaType MEDIA_TYPE_GZIP = new MediaType("application", "x-protobuf-zip", DEFAULT_CHARSET);
private static final Map<Class<?>, Method> methodCache = new ConcurrentReferenceHashMap();
/**
* Construct a new instance.
*/
public ProtobufHttpMessageConverter() {
super(MEDIA_TYPE, MEDIA_TYPE_LZ4, MEDIA_TYPE_GZIP, MEDIA_TYPE_SNAPPY);
}
/**
* {@inheritDoc}
*/
@Override
public boolean canRead(final Class<?> clazz, final MediaType mediaType) {
if(MEDIA_TYPE.isCompatibleWith(mediaType)
||MEDIA_TYPE_GZIP.isCompatibleWith(mediaType)){
return true;
}
return false;
}
/**
* {@inheritDoc}
* 根据 mediaType判断需不需要走protobuf的转换
*/
@Override
public boolean canWrite(final Class<?> clazz, final MediaType mediaType) {
if(MEDIA_TYPE.isCompatibleWith(mediaType)
||MEDIA_TYPE_GZIP.isCompatibleWith(mediaType)){
return true;
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
protected Message readInternal(final Class<? extends Message> clazz, final HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
Message.Builder builder = this.getMessageBuilder(clazz);
// protobuf 序列化方式
if (MEDIA_TYPE.isCompatibleWith(inputMessage.getHeaders().getContentType())) {
builder.mergeFrom(inputMessage.getBody());
//protobuf 序列化 + zlib 压缩
}else if(MEDIA_TYPE_GZIP.isCompatibleWith(inputMessage.getHeaders().getContentType())){
ByteArrayOutputStream outputStream = null;
try {
long time = System.currentTimeMillis();
InputStream inputStream = inputMessage.getBody();
HttpHeaders httpHeaders = inputMessage.getHeaders();
Descriptors.Descriptor descriptor = builder.getDescriptorForType();
Descriptors.FieldDescriptor fieldDescriptorLength = descriptor.findFieldByNumber(25);
Descriptors.FieldDescriptor fieldDescriptorTime = descriptor.findFieldByNumber(26);
Long length = httpHeaders.getContentLength();
byte[] bytes = new byte[1024];
outputStream = new ByteArrayOutputStream(length.intValue());
int i =0;
while ( (i = inputStream.read(bytes)) > 0){
outputStream.write(bytes,0,i);
}
byte[] data = ZlibUtil.uncompress(outputStream.toByteArray(),false);
logger.info("压缩前的数据长度={}",data.length);
//httpHeaders.add("uncompressLength",data.length+"");
builder.setField(fieldDescriptorLength,data.length+"");
builder.mergeFrom(data);
long time2 = System.currentTimeMillis();
logger.info("解压数据耗时---{}--长度={}",time2-time,length);
//httpHeaders.add("uncompressTime",(time2-time)+"");
builder.setField(fieldDescriptorTime,((time2-time)==0?1:(time2-time))+"");
} catch (Exception e) {
e.printStackTrace();
}finally {
if(outputStream != null){
outputStream.close();
}
}
}
return builder.build();
}
private Message.Builder getMessageBuilder(Class<? extends Message> clazz) {
try {
Method method = (Method)methodCache.get(clazz);
if (method == null) {
method = clazz.getMethod("newBuilder");
methodCache.put(clazz, method);
}
return (Message.Builder)method.invoke(clazz);
} catch (Exception var3) {
throw new HttpMessageConversionException("Invalid Protobuf Message type: no invocable newBuilder() method on " + clazz, var3);
}
}
/**
* {@inheritDoc}
*/
@Override
protected boolean supports(final Class<?> clazz) {
// Should not be called, since we override canRead/canWrite.
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*/
@Override
protected void writeInternal(Message message, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
if (logger.isDebugEnabled()) {
logger.info("Current type: {}", outputMessage.getHeaders().getContentType());
}
Stopwatch stopwatch = Stopwatch.createStarted();
OutputStream stream = null;
try {
stream = outputMessage.getBody();
if (MEDIA_TYPE.isCompatibleWith(outputMessage.getHeaders().getContentType())) {
this.setProtoHeader(outputMessage, message);
CodedOutputStream codedOutputStream = CodedOutputStream.newInstance(outputMessage.getBody());
message.writeTo(codedOutputStream);
codedOutputStream.flush();
return;
} else if (MEDIA_TYPE_GZIP.isCompatibleWith(outputMessage.getHeaders().getContentType())) {
byte[] bytes = message.toByteArray();
stream.write(ZlibUtil.compress(bytes,false));
stream.flush();
} else if (MEDIA_TYPE_LZ4.isCompatibleWith(outputMessage.getHeaders().getContentType())) {
stream = new LZ4BlockOutputStream(stream);
} else if (MEDIA_TYPE_SNAPPY.isCompatibleWith(outputMessage.getHeaders().getContentType())) {
stream = new SnappyOutputStream(stream);
} else {
return;
}
message.writeTo(stream);
} finally {
IOUtils.closeQuietly(stream);
}
if (logger.isDebugEnabled()) {
logger.info("Output spend {}", stopwatch.toString());
}
}
private void setProtoHeader(HttpOutputMessage response, Message message) {
response.getHeaders().set("X-Protobuf-Schema", message.getDescriptorForType().getFile().getName());
response.getHeaders().set("X-Protobuf-Message", message.getDescriptorForType().getFullName());
}
}
zlib解压缩工具
public class ZlibUtil {
public static byte[] uncompress(byte[] input, boolean nowrap) throws IOException {
Inflater inflater = new Inflater(nowrap);
inflater.setInput(input);
ByteArrayOutputStream baos = new ByteArrayOutputStream(input.length);
try {
byte[] buff = new byte[1024];
while (!inflater.finished()) {
int count = inflater.inflate(buff);
/*if(count == 0){
break;
}*/
baos.write(buff, 0, count);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
baos.close();
}
inflater.end();
byte[] output = baos.toByteArray();
return output;
}
public static byte[] compress(byte[] data, boolean nowrap) throws IOException {
byte[] output;
Deflater compress = new Deflater(Deflater.BEST_COMPRESSION, nowrap);
compress.reset();
compress.setInput(data);
compress.finish();
ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length);
try {
byte[] buf = new byte[1024];
while (!compress.finished()) {
int i = compress.deflate(buf);
bos.write(buf, 0, i);
}
output = bos.toByteArray();
} catch (Exception e) {
output = data;
e.printStackTrace();
} finally {
bos.close();
}
compress.end();
return output;
}
}
四、Controller 层定义
/**
* 查询数据接口 ,请求接口时 ,content-type = 'application/x-protobuf-zip'
* produces = "application/x-protobuf-zip" 自定义消息转换器会根据 这个mediaType类型判断,是否将返回信息封装成protobuf系列化 +zlib压缩
* @param request
* @param calResultQuery
* @return
*/
@PostMapping(value = "queryResult", produces = "application/x-protobuf-zip" )
@ResponseBody
public ResultVo.ResultResponse queryResult(HttpServletRequest request,@RequestBody CalResultQueryVO calResultQuery){
ResultVo.ResultResponse.Builder resultResponse = ResultVo.ResultResponse.newBuilder();
try {
String globalId = (String) request.getAttribute("globalId");
CalResultVO.CalResult calResult = parseProtoDataManager.parseResultData(calResultQuery,globalId);
resultResponse.setData(Any.pack(calResult));
resultResponse.setCode(Integer.valueOf(ErrorCode.SYS_CODE_200));
resultResponse.setMsg("下载数据成功");
}catch (Exception e){
e.printStackTrace();
resultResponse.setCode(Integer.valueOf(ErrorCode.SYS_CODE_500));
resultResponse.setMsg("下载数据失败"+e.getMessage());
}
return resultResponse.build();
}