-
Notifications
You must be signed in to change notification settings - Fork 100
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enhancements for Json support. #70
base: master
Are you sure you want to change the base?
Changes from all commits
0ef9a2f
3f17042
bea5094
6d16a40
9fb92c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package one.nio.serial; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
/** | ||
* Constructor calls are skipped on JSON deserialization unless annotated with this annotation. | ||
*/ | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@Target({ElementType.CONSTRUCTOR}) | ||
public @interface Before { | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,12 +39,15 @@ public class GeneratedSerializer extends Serializer { | |
static final AtomicInteger renamedFields = new AtomicInteger(); | ||
static final AtomicInteger unsupportedFields = new AtomicInteger(); | ||
|
||
private final boolean jsonOnlySerialization; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think about creating separate JsonSerializer with own configuration ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great idea. I'll see what I can come up with. |
||
|
||
private FieldDescriptor[] fds; | ||
private FieldDescriptor[] defaultFields; | ||
private Delegate delegate; | ||
|
||
GeneratedSerializer(Class cls) { | ||
GeneratedSerializer(Class cls, boolean jsonOnlySerialization) { | ||
super(cls); | ||
this.jsonOnlySerialization = jsonOnlySerialization; | ||
|
||
Field[] ownFields = getSerializableFields(); | ||
this.fds = new FieldDescriptor[ownFields.length / 2]; | ||
|
@@ -108,7 +111,7 @@ public void skipExternal(ObjectInput in) throws IOException, ClassNotFoundExcept | |
|
||
@Override | ||
public byte[] code() { | ||
return DelegateGenerator.generate(cls, fds, defaultFields); | ||
return DelegateGenerator.generate(cls, fds, defaultFields, jsonOnlySerialization); | ||
} | ||
|
||
@Override | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,11 @@ | |
|
||
public class Json { | ||
|
||
|
||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_SAFE_INTEGER | ||
public static final long JS_MIN_SAFE_INTEGER = -9007199254740991L; | ||
public static final long JS_MAX_SAFE_INTEGER = 9007199254740991L; | ||
|
||
public static void appendChar(StringBuilder builder, char c) { | ||
builder.append('"'); | ||
if (c == '"' || c == '\\') { | ||
|
@@ -70,6 +75,18 @@ public static void appendObject(StringBuilder builder, Object obj) throws IOExce | |
} | ||
} | ||
|
||
public static void appendLong(StringBuilder builder, long value) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe writing long as string should be configurable (as in JACKSON/GSON) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fine with me. |
||
if (isJsSafeInteger(value)) { | ||
builder.append(value); | ||
} else { | ||
builder.append('"').append(value).append('"'); | ||
} | ||
} | ||
|
||
public static boolean isJsSafeInteger(long value) { | ||
return JS_MIN_SAFE_INTEGER <= value && value <= JS_MAX_SAFE_INTEGER; | ||
} | ||
|
||
@SuppressWarnings("unchecked") | ||
public static String toJson(Object obj) throws IOException { | ||
if (obj == null) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -372,14 +372,13 @@ private static Serializer generateFor(Class<?> cls) { | |
serializer = new CollectionSerializer(cls); | ||
} else if (Map.class.isAssignableFrom(cls) && !hasOptions(cls, FIELD_SERIALIZATION)) { | ||
serializer = new MapSerializer(cls); | ||
} else if (Serializable.class.isAssignableFrom(cls)) { | ||
if (cls.getName().startsWith("java.time.") && JavaInternals.findMethod(cls, "writeReplace") != null) { | ||
} else { | ||
boolean extendsSerializable = Serializable.class.isAssignableFrom(cls); | ||
if (extendsSerializable && cls.getName().startsWith("java.time.") && JavaInternals.findMethod(cls, "writeReplace") != null) { | ||
serializer = new JavaTimeSerializer(cls); | ||
} else { | ||
serializer = new GeneratedSerializer(cls); | ||
serializer = new GeneratedSerializer(cls, !extendsSerializable); | ||
} | ||
} else { | ||
serializer = new InvalidSerializer(cls); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replacement of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I think as suggested by @atimofeyev maybe it makes sense to make a separete JsonSerializer with a bit different behavior. Such as not needing to implement Serializable. |
||
} | ||
serializer.generateUid(); | ||
} catch (Throwable e) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ | |
package one.nio.serial.gen; | ||
|
||
import one.nio.gen.BytecodeGenerator; | ||
import one.nio.serial.Before; | ||
import one.nio.serial.Default; | ||
import one.nio.serial.FieldDescriptor; | ||
import one.nio.serial.JsonName; | ||
|
@@ -38,6 +39,7 @@ | |
import java.io.ObjectOutputStream; | ||
import java.lang.invoke.MethodHandleInfo; | ||
import java.lang.invoke.MethodType; | ||
import java.lang.reflect.Constructor; | ||
import java.lang.reflect.Field; | ||
import java.lang.reflect.Method; | ||
import java.lang.reflect.Modifier; | ||
|
@@ -113,21 +115,30 @@ public static Delegate instantiate(Class cls, FieldDescriptor[] fds, byte[] code | |
} | ||
|
||
public static Delegate instantiate(Class cls, FieldDescriptor[] fds, FieldDescriptor[] defaultFields) { | ||
return instantiate(cls, fds, generate(cls, fds, defaultFields)); | ||
return instantiate(cls, fds, generate(cls, fds, defaultFields, false)); | ||
} | ||
|
||
public static byte[] generate(Class cls, FieldDescriptor[] fds, FieldDescriptor[] defaultFields) { | ||
public static byte[] generate(Class cls, FieldDescriptor[] fds, FieldDescriptor[] defaultFields, boolean jsonOnly) { | ||
String className = "sun/reflect/Delegate" + index.getAndIncrement() + '_' + cls.getSimpleName(); | ||
|
||
ClassWriter cv = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); | ||
cv.visit(V1_6, ACC_PUBLIC | ACC_FINAL, className, null, MAGIC_CLASS, | ||
new String[]{"one/nio/serial/gen/Delegate"}); | ||
|
||
generateConstructor(cv, className); | ||
generateCalcSize(cv, cls, fds); | ||
generateWrite(cv, cls, fds); | ||
generateRead(cv, cls, fds, defaultFields, className); | ||
generateSkip(cv, fds); | ||
|
||
if (!jsonOnly) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
generateCalcSize(cv, cls, fds); | ||
generateWrite(cv, cls, fds); | ||
generateRead(cv, cls, fds, defaultFields, className); | ||
generateSkip(cv, fds); | ||
} else { | ||
generateThrowCalcSize(cv, cls); | ||
generateThrowWrite(cv, cls); | ||
generateThrowRead(cv, cls); | ||
generateThrowSkip(cv, cls); | ||
} | ||
|
||
generateToJson(cv, cls, fds); | ||
generateFromJson(cv, cls, fds, defaultFields, className); | ||
|
||
|
@@ -360,6 +371,30 @@ private static void generateSkip(ClassVisitor cv, FieldDescriptor[] fds) { | |
mv.visitEnd(); | ||
} | ||
|
||
private static void generateThrowNonSerializableMethod(ClassWriter cv, String methodName, String descriptor, Class cls) { | ||
MethodVisitor mv = cv.visitMethod(ACC_PUBLIC | ACC_FINAL, methodName, descriptor, | ||
null, new String[]{"java/io/NotSerializableException"}); | ||
mv.visitCode(); | ||
emitThrow(mv, "java/io/NotSerializableException", cls.getName()); | ||
mv.visitEnd(); | ||
} | ||
|
||
private static void generateThrowCalcSize(ClassWriter cv, Class cls) { | ||
generateThrowNonSerializableMethod(cv, "calcSize", "(Ljava/lang/Object;Lone/nio/serial/CalcSizeStream;)V", cls); | ||
} | ||
|
||
private static void generateThrowWrite(ClassWriter cv, Class cls) { | ||
generateThrowNonSerializableMethod(cv, "write", "(Ljava/lang/Object;Lone/nio/serial/DataStream;)V", cls); | ||
} | ||
|
||
private static void generateThrowRead(ClassWriter cv, Class cls) { | ||
generateThrowNonSerializableMethod(cv, "read", "(Lone/nio/serial/DataStream;)Ljava/lang/Object;", cls); | ||
} | ||
|
||
private static void generateThrowSkip(ClassWriter cv, Class cls) { | ||
generateThrowNonSerializableMethod(cv, "skip", "(Lone/nio/serial/DataStream;)V", cls); | ||
} | ||
|
||
private static void generateToJson(ClassVisitor cv, Class cls, FieldDescriptor[] fds) { | ||
MethodVisitor mv = cv.visitMethod(ACC_PUBLIC | ACC_FINAL, "toJson", "(Ljava/lang/Object;Ljava/lang/StringBuilder;)V", | ||
null, new String[]{"java/io/IOException"}); | ||
|
@@ -401,6 +436,10 @@ private static void generateToJson(ClassVisitor cv, Class cls, FieldDescriptor[] | |
mv.visitMethodInsn(INVOKESTATIC, "one/nio/serial/Json", "appendChar", "(Ljava/lang/StringBuilder;C)V", false); | ||
mv.visitVarInsn(ALOAD, 2); | ||
break; | ||
case Long: | ||
mv.visitMethodInsn(INVOKESTATIC, "one/nio/serial/Json", "appendLong", "(Ljava/lang/StringBuilder;J)V", false); | ||
mv.visitVarInsn(ALOAD, 2); | ||
break; | ||
default: | ||
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", srcType.appendSignature(), false); | ||
} | ||
|
@@ -433,6 +472,19 @@ private static void generateFromJson(ClassVisitor cv, Class cls, FieldDescriptor | |
// Create instance | ||
mv.visitTypeInsn(NEW, Type.getInternalName(cls)); | ||
|
||
// support for calling constructor annotated with @Before | ||
for (Class searchClass = cls; searchClass != null; searchClass = searchClass.getSuperclass()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The cycle might look tricky to a seasoned reader (I've spent a minute or two to parse the meaning), but I have no suggestions how to simplify that, unfortunately. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I am not a fan of constructor call either. Will go trough a few use cases and see if |
||
Constructor constructor = JavaInternals.findConstructor(searchClass); | ||
if (constructor != null && constructor.getAnnotation(Before.class) != null) { | ||
if (searchClass != cls) { | ||
throw new IllegalArgumentException("To avoid unexpected behavior it is required to add @Before to no-arg constructor in " + cls + " because parent class does the same."); | ||
} | ||
mv.visitInsn(DUP); | ||
mv.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(cls), "<init>", "()V", false); | ||
break; | ||
} | ||
} | ||
|
||
// Prepare a multimap (fieldHash -> fds) for lookupswitch | ||
TreeMap<Integer, FieldDescriptor> fieldHashes = new TreeMap<>(); | ||
boolean isRecord = JavaFeatures.isRecord(cls); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it not enough @default annotation on fields?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, I'll get back to you on this one.