protobuf+netty自定义编
项目背景
protobuf+netty自定义编码解码
比如心跳协议,客户端请求的协议是10001,在java端如何解码,心跳返回协议如何编码,将协议号带过去
// 心跳包
//10001
message c2s_heartbeat {
}//10002
message s2c_heartbeat {int64 timestamp = 1; // 时间戳 ms
}
解决方案
1.每个协议id换个生成的class类名关联起来,使用的时候使用读取文件
2.使用jprotobuf 把注释上面的协议id带入到生成文件里面
使用protoc生成java文件的时候带上自定注解
<dependency><groupId>com.baidu</groupId><artifactId>jprotobuf</artifactId><version>2.4.15</version></dependency>
重写根据proto文件生成java代码的方法百度版本的核心文件在ProtobufIDLProxy类
重写核心方法 createCodeByType 生成代码的核心方法
private static CodeDependent createCodeByType(ProtoFile protoFile, MessageElement type, Set<String> enumNames,boolean topLevelClass, List<TypeElement> parentNestedTypes, List<CodeDependent> cds, Set<String> packages,Map<String, String> mappedUniName, boolean isUniName) {//...省略if (topLevelClass) {// define packageif (!StringUtils.isEmpty(packageName)) {code.append("package ").append(packageName).append(CODE_END);code.append("\n");}// add import;code.append("import com.baidu.bjf.remoting.protobuf.FieldType;\n");code.append("import com.baidu.bjf.remoting.protobuf.EnumReadable;\n");code.append("import com.baidu.bjf.remoting.protobuf.annotation.Protobuf;\n");}//添加自定义操作generateCommentsForClass(code,type,protoFile);// define classString clsName;if (topLevelClass) {clsName = "public class ";} else {clsName = "public static class ";}
/*** 生成class注释* @param code 当前代码* @param type 当前类型* @param protoFile 所有类型* @return 是否返回协议码*/private static void generateCommentsForClass(StringBuilder code, MessageElement type, ProtoFile protoFile) {TypeElement typeElement = protoFile.typeElements().stream().filter(i -> i.name().equals(type.name())).findFirst().orElse(null);if(typeElement==null){return;}String documentation = typeElement.documentation();if(StringUtils.isEmpty(documentation)){documentation = "";}else {documentation = documentation.trim();}String[] split = documentation.split("\n");Integer protoId = null;try{protoId = Integer.parseInt(split[split.length-1]);String collect = Arrays.stream(split).collect(Collectors.toList()).subList(0, split.length - 1).stream().collect(Collectors.joining());//code.append("import com.baidu.bjf.remoting.protobuf.annotation.ProtobufClass;\n");String comment = """/*** %d* %s * @author authorZhao* @since %s*/""";comment = String.format(comment,protoId,collect,DATE);code.append(comment);code.append("@com.git.ProtoId("+protoId+")";}catch (Exception e){String comment = """/*** %s* @author authorZhao* @since %s*/""";comment = String.format(comment,documentation,DATE);code.append(comment);}/*code.append(" /**").append(ClassCode.LINE_BREAK);code.append(" * ").append(documentation).append(ClassCode.LINE_BREAK);code.append(" * ").append(ClassCode.LINE_BREAK);*///code.append(" */").append(ClassCode.LINE_BREAK);}
用法
public static void main(String[] args) {File javaOutPath = new File("E:\\java\\workspace\\proto\\src\\main\\java");javaOutPath = new File("C:\\Users\\Admin\\Desktop\\工作文档\\worknote\\java");File protoDir = new File("E:\\project\\git\\test_proto");//protoDir = copy(protoDir);//filterFile(protoDir);File protoFile = new File(protoDir.getAbsolutePath()+"/activity.proto");MyProtobufIDLProxy.setFormatJavaField(true);try {//这里改写之后可以根据一个proto文件生成所有的文件MyProtobufIDLProxy.createAll(protoFile,protoDir, javaOutPath);System.out.println("create success. input file="+protoFile.getName()+"\toutput path=" + javaOutPath.getAbsolutePath());} catch (IOException var5) {System.out.println("create failed: " + var5.getMessage());}System.exit(0);}
3.重写protobuf的核心文件protoc
以windows为例
git clone https://github.com/protocolbuffers/protobuf.git
cd protobuf
git submodule update --init --recursive
本文使用clion开发环境,找到核心代码
SourceLocation location;if (descriptor->GetSourceLocation(&location)) {WriteDocCommentBodyForLocation(printer, location, kdoc);}
std::string comments = location.leading_comments.empty()? location.trailing_comments: location.leading_comments;if (!comments.empty()) {if (kdoc) {comments = EscapeKdoc(comments);} else {comments = EscapeJavadoc(comments);}std::vector<std::string> lines = absl::StrSplit(comments, "\n");while (!lines.empty() && lines.back().empty()) {lines.pop_back();}if (kdoc) {printer->Print(" * ```\n");} else {printer->Print(" * <pre>\n");}for (int i = 0; i < lines.size(); i++) {// Most lines should start with a space. Watch out for lines that start// with a /, since putting that right after the leading asterisk will// close the comment.if (!lines[i].empty() && lines[i][0] == '/') {printer->Print(" * $line$\n", "line", lines[i]);} else {printer->Print(" *$line$\n", "line", lines[i]);}}if (kdoc) {printer->Print(" * ```\n");} else {printer->Print(" * </pre>\n");}printer->Print(" *\n");}
重写方法 WriteMessageDocComment 把注释的最后一行协议号提取出来增加一个协议id
void WriteMessageDocComment(io::Printer* printer, const Descriptor* message,const bool kdoc) {printer->Print("/**\n");WriteDocCommentBody(printer, message, kdoc);if (kdoc) {printer->Print(" * Protobuf type `$fullname$`\n"" */\n","fullname", EscapeKdoc(message->full_name()));} else {printer->Print(" * Protobuf type {@code $fullname$}\n"" */\n","fullname", EscapeJavadoc(message->full_name()));}
}
简单改写一下
//网上抄袭的bool isNum(const std::string& str){std::stringstream sin(str);double t;char p;if(!(sin >> t))/*解释:sin>>t表示把sin转换成double的变量(其实对于int和float型的都会接收),如果转换成功,则值为非0,如果转换不成功就返回为0*/return false;if(sin >> p)/*解释:此部分用于检测错误输入中,数字加字符串的输入形式(例如:34.f),在上面的的部分(sin>>t)已经接收并转换了输入的数字部分,在stringstream中相应也会把那一部分给清除,如果此时传入字符串是数字加字符串的输入形式,则此部分可以识别并接收字符部分,例如上面所说的,接收的是.f这部分,所以条件成立,返回false;如果剩下的部分不是字符,那么则sin>>p就为0,则进行到下一步else里面*/return false;elsereturn true;}/*** 生成自定义代码* @param printer* @param message* @param kdoc* */void writeWithProtoId(io::Printer *printer, const Descriptor *message) {SourceLocation location;bool hasComments = message->GetSourceLocation(&location);if (!hasComments) {return;}std::string comments = location.leading_comments.empty()? location.trailing_comments: location.leading_comments;if (comments.empty()) {return;}//这里当做非kdoccomments = EscapeJavadoc(comments);//根据换行分割std::vector<std::string> lines = absl::StrSplit(comments, "\n");while (!lines.empty() && lines.back().empty()) {lines.pop_back();}if(lines.empty()){return;}std::string protoId = lines[lines.size()-1];if(!isNum(protoId)){return;}printer->Print("@com.git.protoId($line$)\n","line",protoId);}void WriteMessageDocComment(io::Printer* printer, const Descriptor* message,const bool kdoc) {printer->Print("/**\n");WriteDocCommentBody(printer, message, kdoc);if (kdoc) {printer->Print(" * Protobuf type `$fullname$`\n"" */\n","fullname", EscapeKdoc(message->full_name()));} else {printer->Print(" * Protobuf type {@code $fullname$}\n"" */\n","fullname", EscapeJavadoc(message->full_name()));writeWithProtoId(printer,message);}
}
protoc.exe --plugin=protoc-gen-grpc-java=./protoc-gen-grpc-java-1.57.1-windows-x86_64.exe --proto_path=./proto ./proto*.proto --java_out=./test --grpc-java_out=./test
最后生成的代码
/*** <pre>*身份验证c2s*10007* </pre>** Protobuf type {@code login.c2s_auth}*/@com.git.protoId(10007)public static final class c2s_auth extendscom.google.protobuf.GeneratedMessageV3 implements// @@protoc_insertion_point(message_implements:login.c2s_auth)c2s_authOrBuilder {
使用方式
本文结合spring扫描,
/*** 这个类并不注册什么bean,仅仅扫描protoBuf* ProtoScan类似于mybatis的scan,表示proto生成的java文件所在目录* 扫描处理protoId*/
@Slf4j
public class BeanMapperSelector implements ImportBeanDefinitionRegistrar {/*** 扫描的包路径*/private String[] basePackage;/*** 需要扫描的类*/private Class[] classes;@Overridepublic void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(ProtoScan.class.getName());this.basePackage = (String[])annotationAttributes.get("basePackages");this.classes = (Class[])annotationAttributes.get("classes");List<Class> classList = new ArrayList<>();for (Class aClass : classes) {if(aClass.isAnnotationPresent(ProtoId.class) && com.google.protobuf.GeneratedMessageV3.class.isAssignableFrom(aClass)){classList.add(aClass);}}if(basePackage.length>0){List<String> list = List.of(basePackage).stream().map(this::resolveBasePackage).toList();List<Class> classes1 = ClassScanUtil.scanPackageClass(list, null, clazz -> clazz.isAnnotationPresent(ProtoId.class) && com.google.protobuf.GeneratedMessageV3.class.isAssignableFrom(clazz));classList.addAll(classes1);}for (Class aClass : classList) {try {ProtoId protoId = AnnotationUtils.getAnnotation(aClass, ProtoId.class);if(aClass.getSimpleName().startsWith("c2s")){//将byte[]转化为对象的方法缓存//com.google.protobuf.GeneratedMessageV3 protoObject = (com.google.protobuf.GeneratedMessageV3) method.invoke(null, bytes);Method m = aClass.getMethod("parseFrom", byte[].class);AppProtocolManager.putProtoIdC2SMethod(protoId.value(),m);}else {//class->protoId映射缓存AppProtocolManager.putOldProtoIdByClass(protoId.value(),aClass);}}catch (Exception e){log.error("protoId 注册失败",e);}}//AppProtocolManager.info();}protected String resolveBasePackage(String basePackage) {String replace = basePackage.replace(".", "/");return ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX+replace+"/*.class";}}
本文原创,转载请申明