# grpc-demo **Repository Path**: flysa/grpc-demo ## Basic Information - **Project Name**: grpc-demo - **Description**: gprc 统一请求响应 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-06-06 - **Last Updated**: 2025-08-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Protobuf 之泛型化和http传输类型 ## 一、泛型化 ### 1.1 、 message 的烦恼 #### 1.1.1 、 常用报文格式 在项目中讲究传输数据规范统一,发送方和接收方都方便监视解析处理。常常使用json来定义数据格式,数据头和数据体两部分如: ```json { "header":{ "userCode":"u001", "reqId":"123456" }, "body":{ ... 业务数据 } } { "header":{ "code":200, "msg":"处理成功" }, "body":{ ... 业务数据 } } ``` > 开发过程只需要处理业务数据,请求统一封装请求头,响应统一判断响应头状态,只有成功时才进一步处理业务数据。 #### 1.1.2、 protoful 常用报文格式 protobuf 官方文档和博客教程中 无一例外使用的下面的格式 ```protobuf //公用请求头 message HeaderReq { string traceId = 1; // 用户ID string traceCode = 2; // 消息类型 } //公用请求体 message HeaderRes { string code = 1; // 响应码 optional string msg = 2; // 响应消息 } //业务报文 message QueryUserReq{ HeaderReq header = 1; //报文头 string userCode = 2; //业务数据 } message QueryUserRes{ HeaderRes header = 1;//报文头 string userName = 2; //业务数据 string age = 3; } ``` > 每一个业务虽然都包含了请求头,但是报文体都是独立报文件,不方便解析处理 ### 1.2 报文泛型化 ``` message HeaderReq { string traceId = 1; // 用户ID string traceCode = 2; // 消息类型 } message HeaderRes { string code = 1; // 响应码 optional string msg = 2; // 响应消息 } message Request { HeaderReq head = 1; optional google.protobuf.Any body = 2; // 使用bytes类型支持泛型数据 } message Response { HeaderRes head = 1; optional google.protobuf.Any body = 2; // 使用bytes类型支持泛型数据 } ``` > body 为Any 类型,其实就是byte数组类型 ``` message HelloRequest { string name = 1; Aa aa = 2; } message HelloResponse { string message = 1; Bb bb = 2; } message Aa{ string a1 = 1; string a2 = 2; } message Bb{ string b1 = 1; string b2 = 2; } ``` #### 1.2.1 、 报文处理工具类 ##### 1.2.1.1 、报文构建器 ```java import com.example.demo.HeaderReq; import com.example.demo.HeaderRes; import com.example.demo.Request; import com.example.demo.Response; import com.google.protobuf.Any; import com.google.protobuf.Message; import java.util.Objects; import java.util.UUID; /** * 消息构建器 * @author ls */ public class MsgBuilder { /** * 构建请求报文 * @param data 业务报文 * @param userId 用户id * @return 请求 */ public static Request buildReq(Message data, String userId) { return Request.newBuilder() .setHead(buildHeaderReq(userId)) .setBody(Any.pack(data)) .build(); } public static Request buildReq(Message data) { return buildReq(data, null); } /** * 构建成功响应 * @param data 业务报文 * @param code 状态码 * @param msg 状态描述 * @return 响应 */ public static Response buildRes(Message data, String code, String msg) { Response.Builder builder = Response.newBuilder() .setHead(buildHeaderRes(code, msg)); if (!Objects.isNull(data)) { builder.setBody(Any.pack(data)); } return builder.build(); } public static Response success(Message message) { return buildRes(message, "200", "success"); } public static Response fail(String code, String msg) { return buildRes(null, code, msg); } public static Response fail(String msg) { return buildRes(null, "500", msg); } /** * 构建响应头 * @param code 状态码 * @param msg 状态描述 * @return 响应头 */ private static HeaderRes.Builder buildHeaderRes(String code, String msg) { HeaderRes.Builder builder = HeaderRes.newBuilder().setCode(code); if (!Objects.isNull(msg)) { builder.setMsg(msg); } return builder; } /** * 构建请求头 * @param traceId * @return 请求头 */ private static HeaderReq.Builder buildHeaderReq(String traceId) { HeaderReq.Builder builder = HeaderReq.newBuilder() .setTraceId(UUID.randomUUID().toString()); if (!Objects.isNull(traceId)) { builder.setTraceId(traceId); } return builder; } } ``` ##### 1.2.1.2 、报文解析器 ```java package com.example.demo.grpc.utils; import com.example.demo.Request; import com.example.demo.Response; import com.google.protobuf.Any; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * @author ls * @description 报文解析工具 * @date 2025/6/5 18:20 */ public class MsgParse { //报文类型class 缓存 private static final Map> TYPE_REGISTRY = new ConcurrentHashMap<>(); /** * 解析请求体 * @param request 请求 * @return 请求体 * @throws InvalidProtocolBufferException 解析异常 * @throws ClassNotFoundException 类不存在异常 */ public static T parseReqBody(Request request) throws InvalidProtocolBufferException, ClassNotFoundException { if (request.hasBody()) { Any body = request.getBody(); return body.unpack(getType(body)); } return null; } /** * 解析响应体 * @param response 响应 * @return 响应体 * @throws InvalidProtocolBufferException 解析异常 * @throws ClassNotFoundException 类不存在异常 */ public static T parseResBody(Response response) throws InvalidProtocolBufferException, ClassNotFoundException { if (response.hasBody()) { Any body = response.getBody(); return body.unpack(getType(body)); } return null; } /** * 获取报文类型 * @param data 报文 * @return 报文类型 * @throws ClassNotFoundException 类不存在异常 */ private static Class getType(Any data) throws ClassNotFoundException { String fullType = data.getTypeUrl(); String className = fullType.substring(fullType.lastIndexOf("/") + 1); Class clazz = TYPE_REGISTRY.get(className); if (clazz != null) { return (Class) clazz; } Class newClazz = (Class) Class.forName(className); TYPE_REGISTRY.put(className,newClazz ); return newClazz; } } ``` ##### 1.2.1.4 、报文处理示例 客户端代码: ```java HelloRequest hello = HelloRequest.newBuilder() .setName("lis") .setAa(Aa.newBuilder().setA1("a1").setA2("a2")) .build(); Request request = MsgBuilder.buildReq(hello); Response response = helloService.sayHello(request); HelloResponse helloResponse = MsgParse.parseResBody(response); ``` 服务端代码: ```java HelloRequest data = MsgParse.parseReqBody(request); log.info("receive request: {}", data.getName()); HelloResponse helloResponse = HelloResponse.newBuilder().setMessage(data.getName()) .setBb(Bb.newBuilder().setB1(data.getAa().getA1()).setB2(data.getAa().getA2()).build()) .build(); ``` ##### 1.2.1.5 、 Grpc log拦截器 ```java import com.example.demo.HeaderReq; import com.example.demo.Request; import com.example.demo.Response; import com.google.protobuf.Message; import com.google.protobuf.MessageOrBuilder; import com.google.protobuf.util.JsonFormat; import io.grpc.*; import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; /** * 接收和响应日志打印 */ @GrpcGlobalServerInterceptor public class ServerLogInterceptor implements ServerInterceptor { private static final Logger log= LoggerFactory.getLogger(ServerLogInterceptor.class); public static final int MAX_LENGTH = 10000; private String truncateLog(String str){ if(str.length()> MAX_LENGTH){ return str.substring(0,MAX_LENGTH)+"..."; } return str; } @Override public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { ServerCall listener = new ForwardingServerCall.SimpleForwardingServerCall<>(call) { @Override public void sendMessage(RespT message) { try { Response response = (Response) message; String print = printer().print(MsgParse.parseResBody(response)); var r=truncateLog(print); log.info("响应: {} [{}] => {}",response.getHead().getCode(),response.getHead().getMsg(),print); } catch (Exception e) { log.error("protobuf转换json失败",e); } super.sendMessage(message); } }; return new ForwardingServerCallListener.SimpleForwardingServerCallListener<>(next.startCall(listener, headers)) { @Override public void onMessage(ReqT message) { try { Request request = (Request) message; if(request.hasHead()){ HeaderReq head = request.getHead(); MDC.put("traceId",head.getTraceId()); MDC.put("traceCode",head.getTraceCode()); } String print = printer().print(MsgParse.parseReqBody(request)); var r=truncateLog(print); log.info("方法名:{},接收:{}",call.getMethodDescriptor().getFullMethodName(), r); } catch (Exception e) { log.error("protobuf转换json失败",e); } super.onMessage(message); } }; } private JsonFormat.Printer printer(){ return JsonFormat.printer().omittingInsignificantWhitespace(); } } ``` ## 二、http请求响应protobuf spring web 默认支持常用响应报文类型MediaType,APPLICATION_JSON="application/json",TEXT_HTML_VALUE="text/html",TEXT_PLAIN_VALUE="text/plain" 响应protobuf 类型为:APPLICATION_PROTOBUF_VALUE="application/x-protobuf" ## 2.1、依赖jar ```groovy implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'com.google.protobuf:protobuf-java-util:3.25.0' implementation 'com.googlecode.protobuf-java-format:protobuf-java-format:1.2' ``` ## 2.2、 webMvc 配置添加message换换器 ```java import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.List; @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void configureMessageConverters(List> converters) { converters.add(new ProtobufHttpMessageConverter()); } } ``` ## 2.3、Controller 接口 ```java import com.example.demo.Request; import com.example.demo.grpc.utils.MsgBuilder; import com.example.demo.hello.Bb; import com.example.demo.hello.HelloRequest; import com.example.demo.hello.HelloResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @Slf4j @RestController @RequestMapping("/") public class IndexCtl { @GetMapping(value = "/protobuf", produces = MediaType.APPLICATION_PROTOBUF_VALUE) public HelloResponse grpc() { return getBuild(); } @GetMapping(value = "/sendProto" ,consumes = MediaType.APPLICATION_PROTOBUF_VALUE,produces = MediaType.APPLICATION_PROTOBUF_VALUE) public HelloResponse hello(HelloRequest helloRequest) { return getBuild(); } @GetMapping(value = "/protobufBuild", produces = MediaType.APPLICATION_PROTOBUF_VALUE) public Request grpcBuild() { return MsgBuilder.buildReq(getBuild()); } @GetMapping("text") public String text() { return "hello world"; } private static HelloResponse getBuild() { return HelloResponse.newBuilder() .setMessage("INFO c.e.d.g.u.ServerLogInterceptor1 - [onMessage,63] - 方法名:com.example.demo.HelloService/sayHello,接收:{\"name\":\"lis\",\"aa\":{\"a1\":\"a1\",\"a2\":\"a2\"}}") .setBb(Bb.newBuilder() .setB1("Registered gRPC service: com.example.demo.HelloService, bean: helloServiceGrpc, class: com.example.demo.grpc.service.HelloServiceGrpc") .setB2("Registered gRPC service: com.example.demo.TerminalService, bean: terminalSeriveGrpc, class: com.example.demo.grpc.service.TerminalSeriveGrpc") .build()) .build(); } } ```