fieldValues = new HashMap<>();
19 | fieldValues.put("id", 1);
20 | fieldValues.put("name", "jihite");
21 | fieldValues.put("email", "jihite@jihite.com");
22 | fieldValues.put("address", Arrays.asList("address1", "address2", "address3"));
23 |
24 | // 生成Message
25 | Message message = DynamicProtoBuilder
26 | .buildMessage("simple_person.proto", "SimplePersonMessage", fieldValues);
27 | byte[] data = message.toByteArray();
28 | }
29 | }
30 | ```
31 | 更多示例见单元测试
32 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | com.fengfshao
8 | dynamic-proto
9 | 1.1-SNAPSHOT
10 |
11 |
12 |
13 |
14 | junit
15 | junit
16 | 4.12
17 | test
18 |
19 |
20 |
21 | com.google.protobuf
22 | protobuf-java
23 | 3.17.0
24 | provided
25 |
26 |
27 |
28 | com.github.os72
29 | protobuf-dynamic
30 | 1.0.1
31 |
32 |
33 |
34 | io.protostuff
35 | protostuff-parser
36 | 2.2.27
37 |
38 |
39 |
40 |
41 |
42 | 8
43 | 8
44 |
45 |
46 |
47 |
48 |
49 |
50 | org.apache.maven.plugins
51 | maven-shade-plugin
52 | 3.2.4
53 |
54 |
55 | package
56 |
57 | shade
58 |
59 |
60 | true
61 |
62 |
63 | com.google.common
64 | shaded.dp.com.google.common
65 |
66 |
67 | com.google.inject
68 | shaded.dp.com.google.inject
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | com.github.igor-petruk.protobuf
77 | protobuf-maven-plugin
78 | 0.6.5
79 |
80 | true
81 |
82 |
83 | ${project.basedir}/src/test/resources
84 |
85 |
86 | /usr/local/bin/protoc3
87 | ${project.basedir}/src/test/java
88 | false
89 |
90 |
91 |
92 | process-test-sources
93 |
94 | run
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/src/main/java/com/fengfshao/dynamicproto/DynamicProtoBuilder.java:
--------------------------------------------------------------------------------
1 | package com.fengfshao.dynamicproto;
2 |
3 | import com.github.os72.protobuf.dynamic.DynamicSchema;
4 | import com.github.os72.protobuf.dynamic.EnumDefinition;
5 | import com.github.os72.protobuf.dynamic.MessageDefinition;
6 | import com.github.os72.protobuf.dynamic.MessageDefinition.Builder;
7 | import com.google.inject.Guice;
8 | import com.google.inject.Injector;
9 | import com.google.protobuf.Descriptors.Descriptor;
10 | import com.google.protobuf.Descriptors.FieldDescriptor;
11 | import com.google.protobuf.DynamicMessage;
12 | import io.protostuff.compiler.ParserModule;
13 | import io.protostuff.compiler.model.Enum;
14 | import io.protostuff.compiler.model.Field;
15 | import io.protostuff.compiler.model.Message;
16 | import io.protostuff.compiler.parser.FileReader;
17 | import io.protostuff.compiler.parser.Importer;
18 | import io.protostuff.compiler.parser.ProtoContext;
19 | import org.antlr.v4.runtime.CharStream;
20 | import org.antlr.v4.runtime.CharStreams;
21 | import org.slf4j.Logger;
22 | import org.slf4j.LoggerFactory;
23 |
24 | import java.io.ByteArrayInputStream;
25 | import java.io.InputStream;
26 | import java.util.List;
27 | import java.util.Map;
28 | import java.util.Objects;
29 | import java.util.concurrent.ConcurrentHashMap;
30 |
31 | /**
32 | * 运行时动态构建pb message的一种方案,无需任何protoc编译
33 | *
34 | *
35 | * 示例,对如下proto协议:
36 | * message PersonMessage {
37 | * int32 id = 1;
38 | * string name = 2;
39 | * string email = 3;
40 | * repeated string address = 4;
41 | * }
42 | *
43 | * 通过如下方式,可构造对应的DynamicMessage,得到对应的字节数组
44 | *
45 | * Map fieldValues = new HashMap<>();
46 | * fieldValues.put("id", 1);
47 | * fieldValues.put("name", "jihite");
48 | * fieldValues.put("email", "jihite@jihite.com");
49 | * fieldValues.put("address",Arrays.asList("address1", "address2"));
50 | *
51 | * Message dynamicMessage = DynamicProtoBuilder
52 | * .buildMessage("person.proto","PersonMessage",fieldValues);
53 | *
54 | *
55 | * @author fengfshao
56 | */
57 | @SuppressWarnings("unchecked")
58 | public class DynamicProtoBuilder {
59 |
60 | private static final Logger LOGGER = LoggerFactory.getLogger(DynamicProtoBuilder.class);
61 |
62 | private static class InputStreamReader implements FileReader {
63 |
64 | private final InputStream in;
65 |
66 | public InputStreamReader(InputStream in) {
67 | this.in = in;
68 | }
69 |
70 | @Override
71 | public CharStream read(String name) {
72 | try {
73 | return CharStreams.fromStream(in);
74 | } catch (Exception e) {
75 | LOGGER.error("Could not read {}", name, e);
76 | }
77 | return null;
78 | }
79 | }
80 |
81 | public static class ProtoHolder {
82 | public static final ConcurrentHashMap cache = new ConcurrentHashMap<>();
83 |
84 | public static void registerOrUpdate(byte[] protoBytes, String protoFileName)
85 | throws Exception {
86 | InputStream protoInputStream = new ByteArrayInputStream(protoBytes);
87 | DynamicSchema schema = parseProtoFile(protoInputStream);
88 | cache.put(protoFileName, schema);
89 | }
90 |
91 | public static void registerOrUpdate(InputStream protoInputStream, String protoFileName)
92 | throws Exception {
93 | DynamicSchema schema = parseProtoFile(protoInputStream);
94 | cache.put(protoFileName, schema);
95 | }
96 | }
97 |
98 | /**
99 | * 运行时解析proto文件,构造对应的DynamicSchema,用于后续构建DynamicMessage
100 | *
101 | * @param protoInputStream proto协议输入流
102 | */
103 | private static DynamicSchema parseProtoFile(InputStream protoInputStream) throws Exception {
104 | Injector injector = Guice.createInjector(new ParserModule());
105 | Importer importer = injector.getInstance(Importer.class);
106 |
107 | ProtoContext context = importer.importFile(
108 | new InputStreamReader(protoInputStream), null);
109 |
110 | DynamicSchema.Builder schemaBuilder = DynamicSchema.newBuilder();
111 |
112 | context.getProto().getMessages().forEach(e -> {
113 | MessageDefinition msgDef = createMessageDefinition(e);
114 | schemaBuilder.addMessageDefinition(msgDef);
115 | });
116 |
117 | context.getProto().getEnums().forEach(e -> {
118 | EnumDefinition enumDef = createEnumDefinition(e);
119 | schemaBuilder.addEnumDefinition(enumDef);
120 | });
121 | protoInputStream.close();
122 | return schemaBuilder.build();
123 | }
124 |
125 |
126 | /**
127 | * 按照深度优先顺序,构造含有嵌套的MessageDefinition
128 | */
129 | private static MessageDefinition createMessageDefinition(Message message) {
130 | Builder builder = MessageDefinition.newBuilder(message.getName());
131 | for (Field f : message.getFields()) {
132 | if (!f.getType().isScalar() && !f.getType().isMessage()) {
133 | throw new UnsupportedOperationException("unsupported field type in proto.");
134 | }
135 | String label = f.isRepeated() ? "repeated" : "optional";
136 | builder.addField(label, f.getType().getName(), f.getName(), f.getIndex());
137 | }
138 |
139 | for (Message nestedMessage : message.getMessages()) {
140 | MessageDefinition nestedMsgDef = createMessageDefinition(nestedMessage);
141 | builder.addMessageDefinition(nestedMsgDef);
142 | }
143 |
144 | for (Enum e : message.getEnums()) {
145 | EnumDefinition enumDef = createEnumDefinition(e);
146 | builder.addEnumDefinition(enumDef);
147 | }
148 |
149 | return builder.build();
150 | }
151 |
152 | private static EnumDefinition createEnumDefinition(Enum e) {
153 | EnumDefinition.Builder builder = EnumDefinition.newBuilder(e.getName());
154 | e.getConstants().forEach(c -> {
155 | builder.addValue(c.getName(), c.getValue());
156 | });
157 | return builder.build();
158 | }
159 |
160 | /**
161 | * 将输入的字段根据pb的字段目标类型进行适配:
162 | *
163 | * 1. 标量值的自动转换
164 | * 2. 枚举类型的提取
165 | *
166 | *
167 | * @param fieldValue 传入的字段值
168 | * @param fd pb字段引用
169 | * @return 符合pb类型的java类型字段值
170 | */
171 | private static Object getPBValue(Object fieldValue, FieldDescriptor fd, String protoName) {
172 | if (fieldValue == null) {
173 | return null;
174 | }
175 | FieldDescriptor.JavaType javaType = fd.getJavaType();
176 | switch (javaType) {
177 | case INT:
178 | if (fieldValue instanceof Integer) {
179 | return fieldValue;
180 | } else {
181 | return Integer.parseInt(String.valueOf(fieldValue));
182 | }
183 | case LONG:
184 | if (fieldValue instanceof Long) {
185 | return fieldValue;
186 | } else {
187 | return Long.parseLong(String.valueOf(fieldValue));
188 | }
189 | case FLOAT:
190 | if (fieldValue instanceof Float) {
191 | return fieldValue;
192 | } else {
193 | return Float.parseFloat(String.valueOf(fieldValue));
194 | }
195 | case DOUBLE:
196 | if (fieldValue instanceof Double) {
197 | return fieldValue;
198 | } else {
199 | return Double.parseDouble(String.valueOf(fieldValue));
200 | }
201 | case BOOLEAN:
202 | if (fieldValue instanceof Boolean) {
203 | return fieldValue;
204 | } else {
205 | return Boolean.parseBoolean(String.valueOf(fieldValue));
206 | }
207 | case STRING:
208 | if (fieldValue instanceof String) {
209 | return fieldValue;
210 | } else {
211 | return String.valueOf(fieldValue);
212 | }
213 | case ENUM:
214 | return fd.getEnumType().findValueByName(String.valueOf(fieldValue));
215 | case MESSAGE:
216 | Map fieldValues = (Map) fieldValue;
217 | return buildMessage(protoName, fd.getMessageType().getFullName(), fieldValues);
218 | default:
219 | // BYTE_STRING
220 | throw new UnsupportedOperationException(javaType.name() + " for " + fd.getName() + " not support yet!");
221 | }
222 | }
223 |
224 | /**
225 | * 动态构建proto message的接口
226 | *
227 | * @param messageName 生成的proto文件中的message名称
228 | * @param fieldValues 要填充的数据,字段名->字段值的映射
229 | * 字段名与proto协议一致,字段值的类型说明如下:
230 | *
231 | * - 标量类型与java类型对应,如int32对应Integer,支持自适应解析转换,如String转int32
232 | * - 枚举类型对应枚举值的字符串
233 | * - repeated类型对应java.util.ArrayList
234 | * - 嵌套的message字段为{@literal Map}
235 | *
236 | * @return 生成的DynamicMessage
237 | */
238 | public static DynamicMessage buildMessage(String protoName, String messageName,
239 | Map fieldValues) {
240 | DynamicMessage.Builder msgBuilder = Objects.requireNonNull(ProtoHolder.cache.get(protoName),
241 | "use ProtoHolder#registerOrUpdate register your proto first!")
242 | .newMessageBuilder(messageName);
243 | Descriptor msgDesc = msgBuilder.getDescriptorForType();
244 |
245 | List fdList = msgDesc.getFields();
246 |
247 | fdList.forEach(fd -> {
248 | String fieldName = fd.getName();
249 | Object fieldValue = fieldValues.get(fieldName);
250 | if (fd.isRepeated()) {
251 | if (fieldValue != null) {
252 | List