parent
1534386fbc
commit
46b8c5b43f
@ -0,0 +1,198 @@
|
||||
package org.redisson.codec;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.ser.BasicSerializerFactory;
|
||||
import com.google.protobuf.MessageLite;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufAllocator;
|
||||
import io.netty.buffer.ByteBufInputStream;
|
||||
import io.netty.buffer.ByteBufOutputStream;
|
||||
import io.protostuff.LinkedBuffer;
|
||||
import io.protostuff.ProtostuffIOUtil;
|
||||
import io.protostuff.Schema;
|
||||
import io.protostuff.runtime.RuntimeSchema;
|
||||
import org.redisson.client.codec.BaseCodec;
|
||||
import org.redisson.client.handler.State;
|
||||
import org.redisson.client.protocol.Decoder;
|
||||
import org.redisson.client.protocol.Encoder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class ProtobufCodec extends BaseCodec {
|
||||
private static final Logger log = LoggerFactory.getLogger(ProtobufCodec.class);
|
||||
private final Class<?> mapKeyClass;
|
||||
private final Class<?> mapValueClass;
|
||||
private final Class<?> valueClass;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
//classes in blacklist will not be serialized using protobuf ,but instead will use jackson
|
||||
private final Set<String> protobufBlacklist;
|
||||
|
||||
private final static Set<String> CLASSES_OWN_JACKSON_SERIALIZER = new HashSet<>();
|
||||
|
||||
static {
|
||||
try {
|
||||
Field concreteField = BasicSerializerFactory.class.getDeclaredField("_concrete");
|
||||
concreteField.setAccessible(true);
|
||||
CLASSES_OWN_JACKSON_SERIALIZER.addAll(((Map) concreteField.get(BasicSerializerFactory.class)).keySet());
|
||||
Field _concreteLazyField = BasicSerializerFactory.class.getDeclaredField("_concreteLazy");
|
||||
_concreteLazyField.setAccessible(true);
|
||||
CLASSES_OWN_JACKSON_SERIALIZER.addAll(((Map) concreteField.get(BasicSerializerFactory.class)).keySet());
|
||||
} catch (NoSuchFieldException | IllegalAccessException ignored) {
|
||||
log.warn("ProtobufCodec failed to retrieve Jackson serializers. Maybe some objects (like String which using StringSerializer is better) will be serialized with protobuf unless the protobuf blacklist is explicitly set.");
|
||||
}
|
||||
}
|
||||
|
||||
public ProtobufCodec(Class<?> mapKeyClass, Class<?> mapValueClass) {
|
||||
this(mapKeyClass, mapValueClass, null);
|
||||
}
|
||||
|
||||
public ProtobufCodec(Class<?> valueClass) {
|
||||
this(null, null, valueClass);
|
||||
}
|
||||
|
||||
private ProtobufCodec(Class<?> mapKeyClass, Class<?> mapValueClass, Class<?> valueClass) {
|
||||
this.mapKeyClass = mapKeyClass;
|
||||
this.mapValueClass = mapValueClass;
|
||||
this.valueClass = valueClass;
|
||||
|
||||
protobufBlacklist = new HashSet<>();
|
||||
protobufBlacklist.addAll(CLASSES_OWN_JACKSON_SERIALIZER);
|
||||
protobufBlacklist.add("java.util.ArrayList");
|
||||
protobufBlacklist.add("java.util.HashSet");
|
||||
protobufBlacklist.add("java.util.HashMap");
|
||||
}
|
||||
|
||||
public void addBlacklist(Class<?> clazz) {
|
||||
protobufBlacklist.add(clazz.getName());
|
||||
}
|
||||
|
||||
public void removeBlacklist(Class<?> clazz) {
|
||||
protobufBlacklist.remove(clazz.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Decoder<Object> getValueDecoder() {
|
||||
return createDecoder(valueClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Encoder getValueEncoder() {
|
||||
return createEncoder(valueClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Decoder<Object> getMapValueDecoder() {
|
||||
return createDecoder(mapValueClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Encoder getMapValueEncoder() {
|
||||
return createEncoder(mapValueClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Decoder<Object> getMapKeyDecoder() {
|
||||
return createDecoder(mapKeyClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Encoder getMapKeyEncoder() {
|
||||
return createEncoder(mapKeyClass);
|
||||
}
|
||||
|
||||
private Decoder<Object> createDecoder(Class<?> clazz) {
|
||||
if (clazz == null) {
|
||||
throw new IllegalArgumentException("class to create protobuf decoder can not be null");
|
||||
}
|
||||
|
||||
return new Decoder<Object>() {
|
||||
@Override
|
||||
public Object decode(ByteBuf buf, State state) throws IOException {
|
||||
//use jackson deserializer
|
||||
if (protobufBlacklist.contains(clazz.getName())) {
|
||||
return objectMapper.readValue((InputStream) new ByteBufInputStream(buf), clazz);
|
||||
}
|
||||
|
||||
byte[] bytes = new byte[buf.readableBytes()];
|
||||
buf.readBytes(bytes);
|
||||
if (MessageLite.class.isAssignableFrom(clazz)) {
|
||||
//native deserialize
|
||||
try {
|
||||
return clazz.getDeclaredMethod("parseFrom", byte[].class).invoke(clazz, bytes);
|
||||
} catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} else {
|
||||
//protostuff
|
||||
return ProtostuffUtils.deserialize(bytes, clazz);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Encoder createEncoder(Class<?> clazz) {
|
||||
if (clazz == null) {
|
||||
throw new IllegalArgumentException("class to create protobuf encoder can not be null");
|
||||
}
|
||||
return new Encoder() {
|
||||
@Override
|
||||
public ByteBuf encode(Object in) throws IOException {
|
||||
//use jackson serializer
|
||||
ByteBuf out = ByteBufAllocator.DEFAULT.buffer();
|
||||
if (protobufBlacklist.contains(clazz.getName())) {
|
||||
try {
|
||||
ByteBufOutputStream os = new ByteBufOutputStream(out);
|
||||
objectMapper.writeValue((OutputStream) os, in);
|
||||
return os.buffer();
|
||||
} catch (IOException e) {
|
||||
out.release();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (MessageLite.class.isAssignableFrom(clazz)) {
|
||||
//native serialize
|
||||
out.writeBytes(((MessageLite) in).toByteArray());
|
||||
} else {
|
||||
//protostuff
|
||||
out.writeBytes(ProtostuffUtils.serialize(in));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static class ProtostuffUtils {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> byte[] serialize(T obj) {
|
||||
LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
|
||||
try {
|
||||
return ProtostuffIOUtil.toByteArray(obj, RuntimeSchema.getSchema((Class<T>) obj.getClass()), buffer);
|
||||
} finally {
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T deserialize(byte[] data, Class<T> clazz) {
|
||||
Schema<T> schema = RuntimeSchema.getSchema(clazz);
|
||||
T obj = schema.newMessage();
|
||||
ProtostuffIOUtil.mergeFrom(data, obj, schema);
|
||||
return obj;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
package org.redisson.codec.protobuf;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.protostuff.ProtostuffIOUtil;
|
||||
import io.protostuff.Schema;
|
||||
import io.protostuff.runtime.RuntimeSchema;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.redisson.client.protocol.Encoder;
|
||||
import org.redisson.codec.ProtobufCodec;
|
||||
import org.redisson.codec.protobuf.protostuffData.StuffData;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ProtobufCodecTest {
|
||||
|
||||
@Test
|
||||
public void testBlacklist() throws IOException {
|
||||
Class<StuffData> stuffDataClass = StuffData.class;
|
||||
ProtobufCodec protobufCodec = new ProtobufCodec(stuffDataClass);
|
||||
Encoder valueEncoder = protobufCodec.getValueEncoder();
|
||||
|
||||
//classes in blacklist will not be serialized using protobuf ,but instead will use jackson
|
||||
protobufCodec.addBlacklist(stuffDataClass);
|
||||
final StuffData stuffData = getStuffData();
|
||||
ByteBuf buf = valueEncoder.encode(stuffData);
|
||||
byte[] jsonBytes = new byte[buf.readableBytes()];
|
||||
buf.readBytes(jsonBytes);
|
||||
Assertions.assertTrue(isValidJson(new String(jsonBytes)));
|
||||
|
||||
//classes not in blacklist will be serialized using protobuf
|
||||
protobufCodec.removeBlacklist(stuffDataClass);
|
||||
buf = valueEncoder.encode(stuffData);
|
||||
byte[] protobufBytes = new byte[buf.readableBytes()];
|
||||
buf.readBytes(protobufBytes);
|
||||
StuffData desStuffData = deserializeProtobufBytes(protobufBytes, StuffData.class);
|
||||
Assertions.assertEquals(stuffData, desStuffData);
|
||||
}
|
||||
|
||||
private <T> T deserializeProtobufBytes(byte[] data, Class<T> clazz) {
|
||||
Schema<T> schema = RuntimeSchema.getSchema(clazz);
|
||||
T obj = schema.newMessage();
|
||||
ProtostuffIOUtil.mergeFrom(data, obj, schema);
|
||||
return obj;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private StuffData getStuffData() {
|
||||
final StuffData stuffData = new StuffData();
|
||||
stuffData.setAge(18);
|
||||
List<String> hobbies = new ArrayList<>();
|
||||
hobbies.add("game");
|
||||
hobbies.add("game");
|
||||
stuffData.setHobbies(hobbies);
|
||||
stuffData.setName("ccc");
|
||||
return stuffData;
|
||||
}
|
||||
|
||||
private boolean isValidJson(String jsonString) {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
try {
|
||||
objectMapper.readTree(jsonString);
|
||||
return true;
|
||||
} catch (JsonProcessingException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,27 @@
|
||||
syntax = "proto2";
|
||||
|
||||
package org.redisson.codec.protobuf.raw;
|
||||
|
||||
option java_package = "org.redisson.codec.protobuf.raw";
|
||||
|
||||
option java_outer_classname = "Proto2AllTypes";
|
||||
|
||||
message AllTypes2{
|
||||
//types from https://protobuf.dev/programming-guides/proto2/
|
||||
repeated double doubleType = 1;
|
||||
optional float floatType = 2;
|
||||
required int32 int32Type = 3;
|
||||
optional int64 int64Type = 4;
|
||||
optional uint32 uint32Type = 5;
|
||||
optional uint64 uint64Type = 6;
|
||||
optional sint32 sint32Type = 7;
|
||||
optional sint64 sint64Type = 8;
|
||||
optional fixed32 fixed32Type = 9;
|
||||
optional fixed64 fixed64Type = 10;
|
||||
optional sfixed32 sfixed32Type = 11;
|
||||
optional sfixed64 sfixed64Type = 12;
|
||||
optional bool boolType = 13;
|
||||
optional string stringType = 14;
|
||||
optional bytes bytesType = 15 ;
|
||||
}
|
||||
|
@ -0,0 +1,27 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package org.redisson.codec.protobuf.raw;
|
||||
|
||||
option java_package = "org.redisson.codec.protobuf.raw";
|
||||
|
||||
option java_outer_classname = "Proto3AllTypes";
|
||||
|
||||
message AllTypes3{
|
||||
//types from https://protobuf.dev/programming-guides/proto3/
|
||||
repeated double doubleType = 1;
|
||||
optional float floatType = 2;
|
||||
int32 int32Type = 3;
|
||||
int64 int64Type = 4;
|
||||
uint32 uint32Type = 5;
|
||||
uint64 uint64Type = 6;
|
||||
sint32 sint32Type = 7;
|
||||
sint64 sint64Type = 8;
|
||||
fixed32 fixed32Type = 9;
|
||||
fixed64 fixed64Type = 10;
|
||||
sfixed32 sfixed32Type = 11;
|
||||
sfixed64 sfixed64Type = 12;
|
||||
bool boolType = 13;
|
||||
string stringType = 14;
|
||||
bytes bytesType = 15 ;
|
||||
}
|
||||
|
@ -0,0 +1,51 @@
|
||||
package org.redisson.codec.protobuf.protostuffData;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class StuffData {
|
||||
private String name;
|
||||
private int age;
|
||||
private List<String> hobbies;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public int getAge() {
|
||||
return age;
|
||||
}
|
||||
|
||||
public void setAge(int age) {
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
public List<String> getHobbies() {
|
||||
return hobbies;
|
||||
}
|
||||
|
||||
public void setHobbies(List<String> hobbies) {
|
||||
this.hobbies = hobbies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
StuffData stuffData = (StuffData) o;
|
||||
|
||||
if (age != stuffData.age) return false;
|
||||
if (!Objects.equals(name, stuffData.name)) return false;
|
||||
return Objects.equals(hobbies, stuffData.hobbies);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, age, hobbies);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue