从设计者的角度出发理解源码--FastJson

2024-02-14  本文已影响0人  王_建峰

从设计者的角度出发理解源码--FastJson

引言

本篇,作为《从..理解源码》的第二篇,延续前一篇的思路,笔者视图尝试从作者的角度触发分析作者的思路历程,和上篇相比,FastJson的知识可能略多。这篇文章,对fastjson只能算是浅尝即止,我认为最厉害的未过于,作者的持续习惯,包括但不限于:

这个框架的阅读过程中,感受到作者的拼劲全力,感受到:一个极客,孜孜不倦,为当年立下的Flag,将自己空闲时间,都投入到少年时的一个梦:极致的性能

写本篇文章时,由于fastjson2已发布,本篇文章基于fastjson2

为什么要写FastJson

哎,以往的框架,Jakson,Goson,最大的问题其实是使用繁琐,和在不同环境下,兼容性不足

我需要给别人一个足够的理由,做俩个选择

  1. 选择放弃原本的实用。
  2. 选择使用我的东西。

有了

其实,做框架设计起初和写PPT有点类似,你得先有一个能引人入胜的思路,最好能有个最简陋的版本,先将人吸引过来,然后你逐步去完善它,持之以恒的迭代,再产生新的思路,再迭代。

哎,光能证明你性能好也不行呀,稳定性怎么保证,市面上相似的太多了。

有了

序列化设计(Bean->Json)

序列化流程,如果再细分的话,有如下流程:

  1. 获取所有需要提取的属性列表。

  2. 依次遍历属性列表获取属性,向一段JSON容器里面放置key,val。

  3. 在放置key,val的时候,可能涉及某一些转义符的处理,json结构的处理等等。

不同场景的优化

哎,我如何基于不同情况做出不同的处理方式呢,这样我可以针对每一种情况去做具体的优化

有了

// ObjectWriter抽象类,以及部分具体实现
com.alibaba.fastjson2.writer.ObjectWriter
com.alibaba.fastjson2.util.JodaSupport.LocalDateTimeWriter
com.alibaba.fastjson2.util.GuavaSupport.AsMapWriter
....

哎,总不能把所有情况都穷举出来,写出各自的实现,这种优化思路还是不太灵活。

有了:Bean转JSON,本质上,就是从一个Bean里面取内容,然后往一个JSON里面塞么。

我可以为每一个Bean的,读写逻辑,分别依据各自的情况生成相应的字节码读写器。

// 如下摘自:com.alibaba.fastjson2.JSON#toJSONString(java.lang.Object),部分逻辑省略

//1:获取ObjectWriter
com.alibaba.fastjson2.writer.ObjectWriterProvider#getObjectWriter
 //1.1:使用 ASM 来创建 ObjectWriter 对象   
 1.1 creator = ObjectWriterCreatorASM.INSTANCE; // 
 //1.2:通过字节码编写代码,继承ObjectWriterAdapter,创建Class。 
 1.2 com.alibaba.fastjson2.reader.ObjectWriterCreatorASM#createObjectWriter

//2:完成读序列化   
objectWriter.write(writer, object, null, null, 0); 
//3:返回
return writer.toString();

ASM生成的代码

如下我列出了FastJSON生成的ObjectReaderAdapter:

不能使用ASM的环境

哎,可能部分使用者的场景不能使用字节码,可能由于环境问题,比如Android,GraalVM环境等,或者出于安全上面考量,使用者主观不想用字节码实现

有了:

// 核心源码如下: 1.优先支持手动指定方式,2.如果不指定,默认使用ASM方式,3.如果当前环境不支持ASM,则自动转为MH,或者反射。
switch (JSONFactory.CREATOR) { 
  case "reflect":
  case "lambda":
    creator = ObjectWriterCreator.INSTANCE; 
    break;
  case "asm":
  default:
    try {
      if (!JDKUtils.ANDROID && !JDKUtils.GRAAL) { 
        creator = ObjectWriterCreatorASM.INSTANCE; 
      }
    } catch (Throwable ignored) {}
    if (creator == null) { 
      // 如果 creator 仍然为 null,则使用反射或 Lambda 表达式来创建 ObjectWriter 对象
      creator = ObjectWriterCreator.INSTANCE; 
    }
    break;
}

哎,反射好像性能不太好,我如何在这个基础之上优化一点呢?

有了:

invokedynamic指令与MethodHandle的功能简单说:就是之前的动态转发的逻辑都是在字节码层面去完成的,某个方法调用,转发给哪个具体实现类去执行,都是字节码自己安排好了,基于一段固定逻辑自己走下来

而invokedynamic指令与MethodHandle则可以将“转发给谁”,这个谁,在应用代码层面去指定。

Lambda表达式主要就是基于如下几点

1.通过ASM字节码技术,生成最轻量代码(比如static方法)

2.基于指令invokedynamic与MethodHandle特性实现的,将转发能力赋予了业务代码。

// P1:创建set函数的Lambda对象
BiConsumer function = (BiConsumer) lambdaSetter(objectClass, fieldClass, method); 
return createFieldReader(objectClass, objectType, fieldName, fieldType, fieldClass, ordinal, features, format, locale, defaultValue, jsonSchema, method, function, initReader);

// P2:将set函数的Lambda对象放到缓存fieldReaders中
putIfAbsent(fieldReaders, fieldName, fieldReader, objectClass);

// P3:使用function读取值
public void readFieldValue(JSONReader jsonReader, T object) {  
   function.accept(object, (V) fieldValue);
}

知识补习:

安全性方面

哎,如何防止JSON注入呢?毕竟用户输入什么我又不可控制?

有了:

知识补习:

哎,自己通过字节码生成Class,比如如果生成的Class里面有while循环的行为呢?安全性方面如何保证呢?

有了:

//P1 DynamicClassLoader
// ProtectionDomain表示代码源和权限的集合,里面封装了代码源(即代码从哪里来,来源是哪里)和权限(即代码有什么权限,能干嘛)的信息。
public class DynamicClassLoader extends ClassLoader {
    private static final DynamicClassLoader instance = new DynamicClassLoader();
    private static final java.security.ProtectionDomain DOMAIN;
    static {
      DOMAIN = (java.security.ProtectionDomain) java.security.AccessController.doPrivileged(
        (PrivilegedAction<Object>) DynamicClassLoader.class::getProtectionDomain
      );
    }
}

//P2 ObjectReaderCreatorASM
//使用DynamicClassLoader,初始化ObjectReaderCreatorASM的classLoader属性。
public ObjectReaderCreatorASM(ClassLoader classLoader) {
    this.classLoader = classLoader instanceof DynamicClassLoader ? (DynamicClassLoader) classLoader : new DynamicClassLoader(classLoader);
  }

//P3 ObjectReaderCreatorASM.jitObjectReader
//调用classLoader的defineClassPublic方法,将code中的数据定义为一个新的类,并返回这个类的Class对象,赋值给readerClass。
byte[] code = cw.toByteArray();
Class<?> readerClass = classLoader.defineClassPublic(classNameFull, code, 0, code.length);

//P4 com.alibaba.fastjson2.util.DynamicClassLoader#defineClassPublic
// 定义类时候使用DOMAIN作为限制。
public Class<?> defineClassPublic(String name, byte[] b, int off, int len) throws ClassFormatError {
  return defineClass(name, b, off, len, DOMAIN);
}

知识补习:

属性读写顺序

哎,读取和写入Key的顺序如何保证呢?先写哪些属性,后写哪些属性?如何将性能最大化?

有了:我可以在生成字节码阶段做一些事情

// P1 com.alibaba.fastjson2.writer.ObjectWriterCreatorASM#createObjectWriter
    // 1.遍历JavaBean的属性,获取fieldWriters
  BeanUtils.declaredFields(objectClass, field -> { 
    FieldWriter fieldWriter = creteFieldWriter(objectClass, writerFieldFeatures, provider, beanInfo, fieldInfo, field); 
    ...
    fieldWriterMap.put(fieldWriter.fieldName, fieldWriter);
  }); 
  fieldWriters = new ArrayList<>(fieldWriterMap.values()); 

// P2 com.alibaba.fastjson2.writer.ObjectWriterCreatorASM#genMethodWrite
  // 2.遍历fieldWriters,按先后次序生成字节码
  for (int i = 0; i < fieldWriters.size(); i++) {
    FieldWriter fieldWriter = fieldWriters.get(i);
    gwFieldValue(mwc, fieldWriter, OBJECT, i);
  }

// P3 https://note.youdao.com/s/W9gi9fRD,(生成的WriterAdapter字节码)
  // 3.如下为生成的字节码的编码顺序,
  if ((var14 = ((ExcelTransformReq)var2).getColumn1()) != null)
  ...  
  if ((var14 = ((ExcelTransformReq)var2).getColumn10()) != null) 
  
// P4 设置byte数值
  // 4.写入数据
  com.wang.track.analysis.OWG_1_25_ExcelTransformReq#write  
    // 4.2 具体filed写入,其中JSONWriter里面维护全局作用域,以及全局Byte[]
    com.alibaba.fastjson2.writer.FieldWriter#writeFieldName
        com.alibaba.fastjson2.JSONWriter#writeName 
        // 4.2.1 使用unsafe直接操作数组
        UNSAFE.putLong(bytes, ARRAY_BYTE_BASE_OFFSET + off, name);

知识补习:

多种编码格式的适配

哎,字符串的编码格式种类,我如何适配到多种编码格式呢?

有了:其实不同编码格式的区别无非是,存储容器,拼接与位移策略,在不同的情况下的不同。

// 如下列出了JSONWriter,已经它的具体实现
com.alibaba.fastjson2.JSONWriter
com.alibaba.fastjson2.JSONWriterUTF16
com.alibaba.fastjson2.JSONWriterUTF8
com.alibaba.fastjson2.JSONWriterJSONB  

// 根据配置项,或者环境情况,初始化对应的实现。
// com.alibaba.fastjson2.JSONWriter#of
...  
if (FIELD_STRING_VALUE != null && !ANDROID && !OPENJ9) {
  jsonWriter = new JSONWriterUTF16JDK8UF(context);
} else {
  jsonWriter = new JSONWriterUTF16JDK8(context);
}  
...

// 将JSONWriter作为入参,传入WriterAdapter,使用桥接的思路。
com.wang.track.analysis.OWG_1_25_ExcelTransformReq#write(JSONWriter var1, Object var2, Object var3, Type var4, long var5)
this.fieldWriter0.writeFieldName(var1);  

知识补习:

哎,复杂对象(一个class里有另一个class),该如何处理?

有了:

代码结构图

如下是最终的架构图:

[图片上传失败...(image-dc35c6-1707987987703)]

1.入口

JSON.toJSONString(testBean);

2.初始化ObjectWriterProvider(默认ASM实现),全局上下文(context)

final ObjectWriterProvider provider = defaultObjectWriterProvider;
final JSONWriter.Context context = new JSONWriter.Context(provider);

3.初始化JSONWriter(判断当前所处环境,以及参数,自动选择合适的JSONWriter)

JSONWriter writer = JSONWriter.of(context)

3.初始化getObjectWriter

// 初始化getObjectWriter
ObjectWriter<?> objectWriter = provider.getObjectWriter(valueClass,valueClass,(defaultWriterFeatures & JSONWriter.Feature.FieldBased.mask) != 0); 

4.解析

// 此时objectWriter默认的类型为....OWG_1_25_ExcelTransformReq,为ASM生成的class
objectWriter.write(writer, object, null, null, 0);

5.具体的字符串输出

return writer.toString();

反序列化设计(Json->Bean)

由于parseObject方法的代表性,如下均以com.alibaba.fastjson2.JSON#parseObject方法作为思路解析的依据

其实作者对序列化,反序列化的思路是差不多的。只不过反序列化有一些区别,这部分只针对有区分的部分做推倒。

key无序问题

哎,JSON的key的顺序是不可预测的,我该如何处理?

有了:相同字符串的Hash值是一样的,我可以基于这个规律,在写字节码的时候,按顺序写不就行了,通过Hash来作为IF的判断依据。

可能有同学有疑问,使用Map结构足够了,为什么还有生成这样的代码

因为在key的数量相当有限的情况下,逻辑分支的性能(经过JVM指令优化)远比Map寻址的性能高。

// 根据Name的Hash值,使用switch编写逻辑分支代码。
var9 = var1.readFieldNameHashCode()
int var11 = (int)(var9 ^ var9 >>> 32);  
switch(var11) {
  case 1079836942:
    if (var9 == 3832966174269534051L) {
      var10001 = var1.readString();
      ((ExcelTransformReq)var6).setColumn15(var10001);
      continue;
    }
    break;
  case 1079902478:
    if (var9 == 3833247649246244707L) {
      var10001 = var1.readString();
      ((ExcelTransformReq)var6).setColumn25(var10001);
      continue;
    }
    break;
    ...

代码结构图

[图片上传失败...(image-c9fc78-1707987987703)]

1.入口

com.alibaba.fastjson2.JSON#parseObject

2.初始化ObjectReaderProvider,与Context(全局上下文)

ObjectReaderProvider provider = JSONFactory.getDefaultObjectReaderProvider();
JSONReader.Context context = new JSONReader.Context(provider);

3.初始化objectReader

ObjectReader<T> objectReader = provider.getObjectReader(clazz,(defaultReaderFeatures & JSONReader.Feature.FieldBased.mask) != 0);

4.初始化JSONReader

JSONReader reader = JSONReader.of(text, context);

5.解析

T object = objectReader.readObject(reader, clazz, null, 0);

环境变量设计

环境变量信息搜集

哎,我需要面对不同的环境,如果有一个全局视野知道当前环境的情况那些功能可用?

有了,我可以使用一个JDKUtils,所有当前环境的信息,都通过各种方式取出来,然后放到它里面统一维护,用这个类来解决环境变量问题。

// P1
// 环境工具类
com.alibaba.fastjson2.util.JDKUtils
// static阶段获取
public static final Unsafe UNSAFE;
public static final long ARRAY_BYTE_BASE_OFFSET;
public static final long ARRAY_CHAR_BASE_OFFSET;
public static final int JVM_VERSION;
public static final Field FIELD_STRING_VALUE;  
// 如下是通过BiFunction(函数式编程),将不同环境中的能力统一出口
public static final ToIntFunction<String> STRING_CODER;
public static final Function<String, byte[]> STRING_VALUE;

// P2,如下是获取安卓SDK版本号
android_sdk_int = Class.forName("android.os.Build$VERSION").getField("SDK_INT").getInt(null);
// p3,获取Java版本号
String javaSpecVer = System.getProperty("java.specification.version");
// P4,判断当前环境是否有sql的jar包
dataSourceClass = Class.forName("javax.sql.DataSource");

// P5,根据不同版本通过Lambda表达式封装实现
if (JVM_VERSION >= 17) {
  handle = trustedLookup.findStatic(classStringCoding = 
                                    String.class,"isASCII",MethodType.methodType(boolean.class,byte[].class);
}
....
if (handle == null && JVM_VERSION >= 11) {
  classStringCoding = Class.forName("java.lang.StringCoding");
  handle = trustedLookup.findStatic(classStringCoding,"isASCII",MethodType.methodType(boolean.class, byte[].class)
}                               
MethodHandles.Lookup lookup = trustedLookup(classStringCoding);
CallSite callSite = LambdaMetafactory.metafactory(..);
isAscii = (Predicate<byte[]>) callSite.getTarget().invokeExact();
PREDICATE_IS_ASCII = isAscii;

流程数据维护和流程能力扩展

哎,全局视野如何保证?比如现在多层嵌套的JSON,由于我使用Byte[]维护。

有了:

在解析开始时候,设置一个全局Context,这个Context贯穿流程始终。

哎,功能参数和扩展性如何设计呢?

有了:

// 分别是序列化,反序列化,的Context
com.alibaba.fastjson2.JSONWriter.Context
com.alibaba.fastjson2.JSONReader.Context
  
// 如下是JSONReader.Context的部分定义,其中objectSupplier,arraySupplier为
public static final class com.alibaba.fastjson2.JSONReader.Context {
   long features;
   // 扩展点:第1种
   Supplier<Map> objectSupplier;
   Supplier<List> arraySupplier;
   // 扩展点:第2种
   AutoTypeBeforeHandler autoTypeBeforeHandler;
   ExtraProcessor extraProcessor;   
}
// 如下是JSONWriter.Context的部分定义,
public static final class com.alibaba.fastjson2.JSONWriter.Context {
    long features;
    boolean hasFilter;
    // 扩展点:第2种
    PropertyPreFilter propertyPreFilter;
    PropertyFilter propertyFilter;
    NameFilter nameFilter;
    ValueFilter valueFilter;
    BeforeFilter beforeFilter;
    AfterFilter afterFilter;
    ...
}

哎,这些扩展点,需要通过不同的入参传入,体验不太友好,因为这些配置项到底是哪个层面的,没有办法客观区分出来?

答:有了,使用参数注解,属性注解天生就具备一个能力:该注解生效在哪个属性上!

高低版本JDK如何兼容

哎,不同环境可能有一些能力的实现,或者方法名称不一样?我是否可以借力其他框架的某一些能力?

有了:上面好像用过 Lambda表达式的一种能力,将不同环境的实现,统一入口。

CallSite callSite = LambdaMetafactory.metafactory(..);

我可以基于这个思路,通过Class.forName,寻找出当前环境能提供的内容,甚至可以查找一些三方包。

只要符合要求的,我都包装进来

方法的传递,其实使用MethodHandle已经足够了,是否再将MethodHandle包装为Lambda,取决于是否要统一调用入口。

所以如下,既有MethodHandle,也有Lambda(BiFunction,ToIntFunction)

public static final BiFunction<char[], Boolean, String> STRING_CREATOR_JDK8; // JDK 8 的 String 创建函数
public static final BiFunction<byte[], Byte, String> STRING_CREATOR_JDK11; // JDK 11 的 String 创建函数
public static final ToIntFunction<String> STRING_CODER; // String 的编码函数
public static final Function<String, byte[]> STRING_VALUE; // String 的值函数

public static final MethodHandle METHOD_HANDLE_HAS_NEGATIVE; // 是否有负数的方法句柄
public static final Predicate<byte[]> PREDICATE_IS_ASCII; // 是否为 ASCII 的谓词

如下则是通过其他框架来扩展自身能力:

// com.alibaba.fastjson2.writer.ObjectWriterProvider#getObjectWriterInternal

switch (className) {
  case "com.google.common.collect.HashMultimap":
  case "com.google.common.collect.LinkedListMultimap":
  case "com.google.common.collect.LinkedHashMultimap":
  case "com.google.common.collect.ArrayListMultimap":
  case "com.google.common.collect.TreeMultimap":
    objectWriter = GuavaSupport.createAsMapWriter(objectClass);
    break;
  case "com.google.common.collect.AbstractMapBasedMultimap$RandomAccessWrappedList":
    objectWriter = ObjectWriterImplList.INSTANCE;
    break;
  case "com.alibaba.fastjson.JSONObject":
    objectWriter = ObjectWriterImplMap.of(objectClass);
    break;
  case "android.net.Uri$OpaqueUri":
  case "android.net.Uri$HierarchicalUri":
  case "android.net.Uri$StringUri":
    objectWriter = ObjectWriterImplToString.INSTANCE;
    break;
  default:
    break;
}

性能提升的思路

上一篇 下一篇

猜你喜欢

热点阅读