SOFASTACK/SOFA-RPC

RPC

这里会去介绍一下关于我的小小的开源经历 SOFASTACK/SOFA-RPC和Apache/fury
看起来是两个,但是其实是一个,因为主要是在做如何将fury融入到sofa-rpc里面,这里做了很多工作,特别是要感谢 @EvenLjj @Lo1n @chaokunyang
下面直接开始介绍:
首先是对sofa-rpc的改造,这里最大的需求是需要读取一个黑白名单作为配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
public class BlackAndWhiteListFileLoader {

private static final Logger LOGGER = LoggerFactory
.getLogger(BlackAndWhiteListFileLoader.class);

public static final List<String> SOFA_SERIALIZE_BLACK_LIST = loadBlackListFile("/sofa-rpc/serialize_blacklist.txt");

public static final List<String> SOFA_SERIALIZER_WHITE_LIST = loadWhiteListFile("/sofa-rpc/serialize_whitelist.txt");

public static List<String> loadBlackListFile(String path) {
List<String> blackPrefixList = new ArrayList<>();
InputStream input = null;
try {
input = BlackAndWhiteListFileLoader.class.getResourceAsStream(path);
if (input != null) {
readToList(input, "UTF-8", blackPrefixList);
}
String overStr = SofaConfigs.getOrCustomDefault(SERIALIZE_BLACKLIST_OVERRIDE, "");
if (StringUtils.isNotBlank(overStr)) {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Serialize blacklist will override with configuration: {}", overStr);
}
overrideBlackList(blackPrefixList, overStr);
}
} catch (Exception e) {
if (LOGGER.isErrorEnabled()) {
LOGGER.error(e.getMessage(), e);
}
} finally {
closeQuietly(input);
}
return blackPrefixList;
}

public static List<String> loadWhiteListFile(String path) {
List<String> whitePrefixList = new ArrayList<>();
InputStream input = null;
try {
input = BlackAndWhiteListFileLoader.class.getResourceAsStream(path);
if (input != null) {
readToList(input, "UTF-8", whitePrefixList);
}
String overStr = SofaConfigs.getOrCustomDefault(SERIALIZE_WHITELIST_OVERRIDE, "");
if (StringUtils.isNotBlank(overStr)) {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Serialize whitelist will override with configuration: {}", overStr);
}
overrideWhiteList(whitePrefixList, overStr);
}
} catch (Exception e) {
if (LOGGER.isErrorEnabled()) {
LOGGER.error(e.getMessage(), e);
}
} finally {
closeQuietly(input);
}
return whitePrefixList;
}

/**
* 读文件,将结果丢入List
*
* @param input 输入流程
* @param encoding 编码
* @param blackPrefixList 保持黑名单前缀的List
*/
private static void readToList(InputStream input, String encoding, List<String> blackPrefixList) {
InputStreamReader reader = null;
BufferedReader bufferedReader = null;
try {
reader = new InputStreamReader(input, encoding);
bufferedReader = new BufferedReader(reader);
String lineText;
while ((lineText = bufferedReader.readLine()) != null) {
String pkg = lineText.trim();
if (pkg.length() > 0) {
blackPrefixList.add(pkg);
}
}
} catch (IOException e) {
if (LOGGER.isWarnEnabled()) {
LOGGER.warn(e.getMessage(), e);
}
} finally {
closeQuietly(bufferedReader);
closeQuietly(reader);
}
}

/**
* Override blacklist with override string.
*
* @param originList Origin black list
* @param overrideStr The override string
*/
public static void overrideBlackList(List<String> originList, String overrideStr) {
List<String> adds = new LinkedList<>();
String[] overrideItems = StringUtils.splitWithCommaOrSemicolon(overrideStr);
for (String overrideItem : overrideItems) {
if (StringUtils.isNotBlank(overrideItem)) {
if (overrideItem.startsWith("!") || overrideItem.startsWith("-")) {
overrideItem = overrideItem.substring(1);
if ("*".equals(overrideItem) || "default".equals(overrideItem)) {
originList.clear();
} else {
originList.remove(overrideItem);
}
} else {
if (!originList.contains(overrideItem)) {
adds.add(overrideItem);
}
}
}
}
if (adds.size() > 0) {
originList.addAll(adds);
}
}

public static void overrideWhiteList(List<String> originList, String overrideStr) {
List<String> adds = new LinkedList<>();
String[] overrideItems = StringUtils.splitWithCommaOrSemicolon(overrideStr);
for (String overrideItem : overrideItems) {
if (StringUtils.isNotBlank(overrideItem)) {
if (!originList.contains(overrideItem)) {
adds.add(overrideItem);
}
}
}
if (adds.size() > 0) {
originList.addAll(adds);
}
}
}

这段代码的主要作用是加载黑名单和白名单文件,并将文件中的内容读取到对应的列表中。
具体来说,这段代码包含以下主要功能:

  1. loadBlackListFile方法:加载黑名单文件并将文件中的内容读取到blackPrefixList列表中。首先,通过BlackAndWhiteListFileLoader.class.getResourceAsStream(path)方法获取黑名单文件的输入流。然后,使用readToList方法将输入流中的内容按行读取,并将非空的行添加到blackPrefixList列表中。最后,根据配置文件中的覆盖字符串,通过overrideBlackList方法对黑名单列表进行覆盖操作。
  2. loadWhiteListFile方法:加载白名单文件并将文件中的内容读取到whitePrefixList列表中。与loadBlackListFile方法类似,它通过相同的步骤读取白名单文件,并根据配置文件中的覆盖字符串,通过overrideWhiteList方法对白名单列表进行覆盖操作。
  3. readToList方法:将输入流中的内容按行读取,并将非空的行添加到指定的列表中。
  4. overrideBlackList方法:根据覆盖字符串对原始的黑名单列表进行覆盖操作。根据覆盖字符串中的规则,可以添加、删除或清空黑名单列表中的元素。
  5. overrideWhiteList方法:根据覆盖字符串对原始的白名单列表进行覆盖操作。根据覆盖字符串中的规则,可以添加白名单列表中不存在的元素。

总体而言,这段代码的主要作用是加载黑名单和白名单文件,并提供方法来对这些列表进行覆盖操作,以便在后续的逻辑中使用这些列表进行过滤或其他处理
这是一个最基本的需求,就是通过读取一个黑白名单文件,这里也可以展示一下文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
clojure.core$constantly
clojure.main$eval_opt
com.alibaba.citrus.springext.support.parser.AbstractNamedProxyBeanDefinitionParser$ProxyTargetFactory
com.alibaba.citrus.springext.support.parser.AbstractNamedProxyBeanDefinitionParser$ProxyTargetFactoryImpl
com.alibaba.citrus.springext.util.SpringExtUtil.AbstractProxy
com.alipay.custrelation.service.model.redress.Pair
com.caucho.hessian.test.TestCons
com.mchange.v2.c3p0.JndiRefForwardingDataSource
com.mchange.v2.c3p0.WrapperConnectionPoolDataSource
com.rometools.rome.feed.impl.EqualsBean
com.rometools.rome.feed.impl.ToStringBean
com.sun.jndi.rmi.registry.BindingEnumeration
com.sun.jndi.toolkit.dir.LazySearchEnumerationImpl
......

格式就是这样,具体就不全部放出了
然后从设计模式上,会使用一个枚举类去展示出有哪些情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author lipan
*/
public enum FurySecurityMode {

WHITELIST_MODE("whitelist"), BLACKLIST_MODE("blacklist"), NONE_MODE("none");

private final String securityMode;

FurySecurityMode(String securityMode) {
this.securityMode = securityMode;
}

public String getSecurityMode() {
return securityMode;
}
}

这里就只会出现三种情况,黑名单,白名单,什么都不使用
会有一个判断逻辑去判断会出现那种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Do not use any configuration
if (checkerMode.equalsIgnoreCase(FurySecurityMode.NONE_MODE.getSecurityMode())) {
AllowListChecker noChecker = new AllowListChecker(AllowListChecker.CheckLevel.DISABLE);
f.getClassResolver().setClassChecker(noChecker);
return f;
} else if (checkerMode.equalsIgnoreCase(FurySecurityMode.BLACKLIST_MODE.getSecurityMode())) {
AllowListChecker blackListChecker = new AllowListChecker(AllowListChecker.CheckLevel.WARN);
List<String> blackList = BlackAndWhiteListFileLoader.SOFA_SERIALIZE_BLACK_LIST;
// To setting checker
f.getClassResolver().setClassChecker(blackListChecker);
blackListChecker.addListener(f.getClassResolver());
// BlackList classes use wildcards
for (String key : blackList) {
blackListChecker.disallowClass(key + "*");
}
} else if (checkerMode.equalsIgnoreCase(FurySecurityMode.WHITELIST_MODE.getSecurityMode())) {
AllowListChecker blackAndWhiteListChecker = new AllowListChecker(AllowListChecker.CheckLevel.STRICT);
List<String> whiteList = BlackAndWhiteListFileLoader.SOFA_SERIALIZER_WHITE_LIST;
// To setting checker
f.getClassResolver().setClassChecker(blackAndWhiteListChecker);
blackAndWhiteListChecker.addListener(f.getClassResolver());
// WhiteList classes use wildcards
for (String key : whiteList) {
blackAndWhiteListChecker.allowClass(key + "*");
}
List<String> blackList = BlackAndWhiteListFileLoader.SOFA_SERIALIZE_BLACK_LIST;
// To setting checker
f.getClassResolver().setClassChecker(blackAndWhiteListChecker);
blackAndWhiteListChecker.addListener(f.getClassResolver());
// BlackList classes use wildcards
for (String key : blackList) {
blackAndWhiteListChecker.disallowClass(key + "*");
}

这里涉及到fury的两个能力,一个是注册过的类 进行序列化/反序列化的时候效率会更高,一个是可以通过checker去判断,当前的类是否在名单内。
然后下面进入了编码阶段,这里其实会对不同请求进行区别,将SOFA请求和普通的类区分开来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public AbstractByteBuf encode(final Object object, final Map<String, String> context) throws SofaRpcException {
if (object == null) {
throw buildSerializeError("Unsupported null message!");
}
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
try {
fury.setClassLoader(contextClassLoader);
CustomSerializer customSerializer = getObjCustomSerializer(object);
if (customSerializer != null) {
return customSerializer.encodeObject(object, context);
} else {
MemoryBuffer writeBuffer = MemoryBuffer.newHeapBuffer(32);
writeBuffer.writerIndex(0);
fury.serialize(writeBuffer, object);
return new ByteArrayWrapperByteBuf(writeBuffer.getBytes(0, writeBuffer.writerIndex()));
}
} catch (Exception e) {
throw buildSerializeError(e.getMessage(), e);
} finally {
fury.clearClassLoader(contextClassLoader);
}
}

这段代码是一个方法的实现,用于将对象编码为字节缓冲区(ByteBuf)。
具体来说,这段代码的作用如下:
首先,检查传入的对象是否为null,如果是null,则抛出一个序列化错误异常。获取当前线程的上下文类加载器(contextClassLoader),以便后续使用。然后会设置fury(一个自定义序列化框架)的类加载器为上下文类加载器,以确保在序列化过程中使用正确的类加载器加载所需的类。
获取对象的自定义序列化器(customSerializer),如果存在自定义序列化器,则使用自定义序列化器对对象进行编码,并返回编码后的字节缓冲区。
如果不存在自定义序列化器,则创建一个内存缓冲区(MemoryBuffer),并使用fury对对象进行序列化,将序列化后的数据写入内存缓冲区。
将内存缓冲区中的字节数据封装到一个ByteArrayWrapperByteBuf对象中,并返回该对象作为编码后的结果。需要注意的是,在整个过程中,如果发生任何异常,都会抛出一个序列化错误异常,并将异常信息作为错误消息进行构建。
这段代码的主要作用是根据对象的类型和上下文信息,使用自定义序列化器或默认的fury序列化框架,将对象编码为字节缓冲区。
这里可以着重讲一下:

1
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

这个设计的主要目的是为了获得当前线程最新的类加载器,因为在这里会遇到一个场景,那就是当客户端在发送rpc的时候,服务端的rpc协议或者说类变更了,这样会导致两端不一样的问题, 所以需要通过这种方式去实时更新类。动态类加载和热更新:使用动态类加载和热更新的机制,可以在运行时动态加载和更新类。这样可以避免重启应用程序或重新部署的麻烦,使得类的变更能够及时生效。
然后就是通过:

1
CustomSerializer customSerializer = getObjCustomSerializer(object);

这里实际上是,会有自己的序列化器去实现这个CustomSerializer接口,然后这里会通过判断这个object的class是否为CustomSerializer的实现类,然后CustomSerializer有自己的处理方式。
然后看看它的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public AbstractByteBuf encodeObject(SofaRequest object, Map<String, String> context) throws SofaRpcException {
try {
MemoryBuffer writeBuffer = MemoryBuffer.newHeapBuffer(32);
writeBuffer.writerIndex(0);

// 根据SerializeType信息决定序列化器
boolean genericSerialize = context != null &&
isGenericRequest(context.get(RemotingConstants.HEAD_GENERIC_TYPE));
if (genericSerialize) {
// TODO support generic call
throw new SofaRpcException("Generic call is not supported for now.");
}
fury.serialize(writeBuffer, object);
final Object[] args = object.getMethodArgs();
fury.serialize(writeBuffer, args);

return new ByteArrayWrapperByteBuf(writeBuffer.getBytes(0, writeBuffer.writerIndex()));
} catch (Exception e) {
throw new SofaRpcException(e.getMessage(), e);
}
}

这里默认会去构造一个buffer,将序列化的实例的写入buffer里面,并且同时需要将每个参数也同步写入。同时定好fury的序列化模式,然后转为byte数组传回去。这里涉及到fury的底层实现,比较难说为什么要这么做,但是做法确实比较直观和简洁。
然后,就是进行反序列化了,但在这里有一个不同的实现方式,sofa会去定义是否要将结果直接反序列化到传入的实例中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Override
public SofaRequest decodeObject(AbstractByteBuf data, Map<String, String> context) throws SofaRpcException {
MemoryBuffer readBuffer = MemoryBuffer.fromByteArray(data.array());
try {
SofaRequest sofaRequest = (SofaRequest) fury.deserialize(readBuffer);
String targetServiceName = sofaRequest.getTargetServiceUniqueName();
if (targetServiceName == null) {
throw new SofaRpcException("Target service name of request is null!");
}
String interfaceName = ConfigUniqueNameGenerator.getInterfaceName(targetServiceName);
sofaRequest.setInterfaceName(interfaceName);
final Object[] args = (Object[]) fury.deserialize(readBuffer);
sofaRequest.setMethodArgs(args);
return sofaRequest;
} catch (Exception e) {
throw new SofaRpcException(e.getMessage(), e);
}
}

@Override
public void decodeObjectByTemplate(AbstractByteBuf data, Map<String, String> context, SofaRequest template)
throws SofaRpcException {
if (data.readableBytes() <= 0) {
throw new SofaRpcException("Deserialized array is empty.");
}
try {
MemoryBuffer readBuffer = MemoryBuffer.fromByteArray(data.array());
SofaRequest tmp = (SofaRequest) fury.deserialize(readBuffer);
String targetServiceName = tmp.getTargetServiceUniqueName();
if (targetServiceName == null) {
throw new SofaRpcException("Target service name of request is null!");
}
// copy values to template
template.setMethodName(tmp.getMethodName());
template.setMethodArgSigs(tmp.getMethodArgSigs());
template.setTargetServiceUniqueName(tmp.getTargetServiceUniqueName());
template.setTargetAppName(tmp.getTargetAppName());
template.addRequestProps(tmp.getRequestProps());
String interfaceName = ConfigUniqueNameGenerator.getInterfaceName(targetServiceName);
template.setInterfaceName(interfaceName);
final Object[] args = (Object[]) fury.deserialize(readBuffer);
template.setMethodArgs(args);
} catch (Exception e) {
throw new SofaRpcException(e.getMessage(), e);
}
}

大体操作比较类似,就是读出对应的实例和参数。
然后,这个sofa-rpc也是使用扩展机制,去将序列化方式融入到整体当中的。

Fury

这里我还在fury这个项目中做了点贡献,尤其是对StringBuilder的修改:
在 JDK 8 和 JDK 11 中,StringBuilder 类的底层实现数组的方式有一些区别。
在 JDK 8 中,StringBuilder 类使用的是一个字符数组(char[])作为底层的缓冲区。初始时,该字符数组的长度为 16。当需要追加更多字符时,StringBuilder 会检查当前缓冲区是否有足够的空间,如果没有,则会创建一个新的字符数组,将原有的字符数组内容复制到新数组中,并将新的字符数组作为底层缓冲区。
在 JDK 11 中,StringBuilder 类的底层实现有所改进。它引入了一个新的类,称为 CompactStrings。CompactStrings 类使用的是一个字节数组(byte[])作为底层的缓冲区。这个字节数组中的每个字节都可以存储一个字符,而不仅仅是一个字节。这样可以节省内存空间,特别是对于包含大量 ASCII 字符的字符串。
在 JDK 11 中,默认情况下,CompactStrings 是启用的。当字符串中的字符都可以用一个字节表示时,CompactStrings 会将字符串存储在字节数组中。只有当字符串中包含无法用一个字节表示的字符时,才会使用字符数组来存储字符串。
这种改进的底层实现方式可以提供更高的内存效率,特别是对于包含大量 ASCII 字符的字符串。它可以减少内存的使用量,并提高性能。需要注意的是,CompactStrings 的启用与否可以通过 JVM 的参数进行配置。在某些情况下,可能需要手动禁用 CompactStrings,以便与旧版本的 JDK 兼容或满足特定的需求。
于是,即使是面对同样的类,也要有不一样的序列化方式,当然,这里其实可以通过启动时去获取jdk版本,然后做区分操作,现在要展示的是,如何在版本不同的StringBuilder下做出不同的序列化结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static final ToIntFunction GET_CODER;
private static final Function GET_VALUE;

static {
GET_VALUE = (Function) makeGetterFunction(StringBuilder.class.getSuperclass(), "getValue");
ToIntFunction<CharSequence> getCoder;
try {
Method getCoderMethod = StringBuilder.class.getSuperclass().getDeclaredMethod("getCoder");
getCoder = (ToIntFunction<CharSequence>) makeGetterFunction(getCoderMethod, int.class);
} catch (NoSuchMethodException e) {
getCoder = null;
}
GET_CODER = getCoder;
}

这段代码是一个静态代码块,用于初始化两个私有静态变量 GET_VALUE 和 GET_CODER。
首先,代码使用 makeGetterFunction 方法创建了一个函数对象 GET_VALUE,该函数对象用于获取 StringBuilder 类的父类的 getValue 方法。接下来,代码尝试通过反射获取 StringBuilder 类的父类的 getCoder 方法,并将其转换为 ToIntFunction 类型。如果找不到该方法,则将 GET_CODER 设置为 null。最终,将获取到的 getCoder 赋值给 GET_CODER 变量。
这段代码的目的是为了获取 StringBuilder 类的父类的 getValue 方法和 getCoder 方法,并将它们分别赋值给 GET_VALUE 和 GET_CODER 变量。这些变量可能在后续的代码中使用,用于执行相应的操作。
因为既然它们的底层实现不一样,就需要通过反射的方式去获取到实际的底层数组,这里只需要做区分了。然后,写入的代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  @Override
public void write(MemoryBuffer buffer, T value) {
if (GET_CODER != null) {
int coder = GET_CODER.applyAsInt(value);
byte[] v = (byte[]) GET_VALUE.apply(value);
buffer.writeByte(coder);
if (coder == 0) {
buffer.writePrimitiveArrayWithSizeEmbedded(v, Platform.BYTE_ARRAY_OFFSET, value.length());
} else {
if (coder != 1) {
throw new UnsupportedOperationException("Unsupported coder " + coder);
}
buffer.writePrimitiveArrayWithSizeEmbedded(
v, Platform.BYTE_ARRAY_OFFSET, value.length() << 1);
}
} else {
char[] v = (char[]) GET_VALUE.apply(value);
if (StringSerializer.isLatin(v)) {
stringSerializer.writeCharsLatin(buffer, v, value.length());
} else {
stringSerializer.writeCharsUTF16(buffer, v, value.length());
}
}
}
}

首先,代码检查 GET_CODER 是否为 null,如果不为 null,则说明 StringBuilder 对象使用了编码器(coder)。在这种情况下,代码通过调用 GET_CODER.applyAsInt(value) 获取编码器的值,并将其写入 buffer 中。
接下来,根据编码器的值进行不同的处理。如果编码器的值为 0,表示使用 Latin 编码,此时将 byte[] 类型的值 v 写入 buffer,并使用 buffer.writePrimitiveArrayWithSizeEmbedded 方法将数组的内容写入 buffer。如果编码器的值不为 0 或 1,则抛出 UnsupportedOperationException 异常。如果编码器的值为 1,表示使用 UTF-16 编码,此时将 byte[] 类型的值 v 写入 buffer,并将数组的长度乘以 2,然后使用 buffer.writePrimitiveArrayWithSizeEmbedded 方法将数组的内容写入 buffer。
如果 GET_CODER 为 null,则说明 StringBuilder 对象没有使用编码器。在这种情况下,代码通过调用 GET_VALUE.apply(value) 获取 char[] 类型的值 v,然后根据 v 是否为 Latin 编码来决定使用 stringSerializer 的 writeCharsLatin 方法还是 writeCharsUTF16 方法将字符数组写入 buffer。
为什么要区分Latin 和UTF-16呢?Latin 编码和 UTF-16 编码所占的位数是不同的!
Latin 编码是一种字符编码方式,它使用一个字节(8位)来表示一个字符。它主要用于表示拉丁字母字符集,包括英文字母和一些特殊字符。由于使用一个字节表示一个字符,Latin 编码可以节省存储空间,但它只能表示有限的字符集。
UTF-16 编码是一种可变长度的字符编码方式,它使用 16 位(2个字节)来表示一个字符。它可以表示几乎所有的字符,包括拉丁字母、非拉丁字母、符号、表情符号等。UTF-16 编码可以表示更广泛的字符集,但相对于 Latin 编码,它在存储空间上需要更多的字节。
因此,当将字符数组序列化时,如果使用 Latin 编码,每个字符只需要一个字节来表示;而如果使用 UTF-16 编码,每个字符需要两个字节来表示。在代码中,根据编码器的值选择适当的序列化方式,可以根据编码器的值来确定使用 Latin 编码还是 UTF-16 编码,并相应地调整序列化的字节数。

高效的判断该字符串是否纯Latin字符串(SIMD)

这里是使用了向量化操作去快速的判断,主要还是使用了UNSAFE方法去加速判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static boolean isLatin(char[] chars) {
int numChars = chars.length;
int vectorizedLen = numChars >> 2;
int vectorizedChars = vectorizedLen << 2;
int endOffset = Platform.CHAR_ARRAY_OFFSET + (vectorizedChars << 1);
boolean isLatin = true;
for (int offset = Platform.CHAR_ARRAY_OFFSET; offset < endOffset; offset += 8) {
// check 4 chars in a vectorized way, 4 times faster than scalar check loop.
// See benchmark in CompressStringSuite.latinSuperWordCheck.
long multiChars = Platform.getLong(chars, offset);
if ((multiChars & MULTI_CHARS_NON_LATIN_MASK) != 0) {
isLatin = false;
break;
}
}
if (isLatin) {
for (int i = vectorizedChars; i < numChars; i++) {
if (chars[i] > 0xFF) {
isLatin = false;
break;
}
}
}
return isLatin;
}

函数首先获取字符数组的长度 numChars,然后计算出向量化长度 vectorizedLen,即将字符数组长度右移两位得到的结果。这里的len是指需要向量化检测的次数,比如numChars=81,vectorizedLen=20。接着,根据向量化长度计算出向量化字符数 vectorizedChars,即将向量化长度左移两位得到的结果。既,检测的字符数量。

然后,根据向量化字符数计算出结束偏移量 endOffset,即将字符数组的偏移量 Platform.CHAR_ARRAY_OFFSET 加上向量化字符数左移一位得到的结果。Platform.CHAR_ARRAY_OFFSET默认数值为16。在Java中,数组对象在内存中的布局通常包含一些元数据,比如数组的长度、类型信息等。Platform.CHAR_ARRAY_OFFSET 指定了字符数组数据部分的起始偏移量,表示从数组对象的起始位置到实际字符数据开始的距离。在很多JVM实现中,这个偏移量是16字节。这是因为:

  1. JVM实现细节:不同的JVM实现可能有不同的对象头结构。通常,对象头包含一些元数据,如对象的类型信息、标记信息、数组长度等。对于数组对象,这些元数据会占用一定的空间,具体值可能会因JVM版本和实现的不同而有所差异。
  2. 内存对齐:为了性能优化,JVM可能会对内存进行对齐操作,使数据起始位置是某个倍数(如8字节或16字节)的地址。

接下来,函数使用一个布尔变量 isLatin 来表示字符数组是否全部由 Latin 字符组成,初始值为 true。然后,函数通过一个循环,从字符数组的偏移量开始,每次增加 8,以向量化方式检查字符数组中的四个字符。具体地,函数使用 Platform.getLong(chars, offset) 获取一个长整型数,一次读取8个字节(即4个字符)。然后,函数将这个长整型数与 MULTI_CHARS_NON_LATIN_MASK 进行按位与操作,如果结果不为 0,说明存在非 Latin 字符,此时将 isLatin 设置为 false 并跳出循环。

怎么知道掩码是什么数值的呢? 掩码 MULTI_CHARS_NON_LATIN_MASK= -71777214294589696 (即 0xFF80FF80FF80FF80) 是通过位运算来检查字符是否是拉丁字符的。这是通过以下方式实现的:

  1. 掩码计算:

    0xFF80是一个16位掩码,表示非拉丁字符的范围。0xFF80的二进制表示为 11111111 10000000,即前8位为1,后8位为0,用于标识非拉丁字符。

    为了检查4个字符,我们需要将这个16位掩码扩展到64位,即重复4次 0xFF80,得到 0xFF80FF80FF80FF80,表示 11111111 10000000 11111111 10000000 11111111 10000000 11111111 10000000。

  2. 使用掩码进行位运算:

    UNSAFE.getLong(chars, offset) 读取8字节(即4个字符)的数据。

    将这些数据与 0xFF80FF80FF80FF80 进行按位与运算。如果结果不为0,说明这4个字符中至少有一个字符的高字节部分非零,即不是拉丁字符。

如果在向量化检查中没有发现非 Latin 字符,函数会继续进行后续的检查。去检测费向量化的部分,函数使用一个循环,从向量化字符数开始,逐个检查字符数组中的字符。如果发现字符的值大于 0xFF(即超过一个字节的范围),则说明存在非 Latin 字符,此时将 isLatin 设置为 false 并跳出循环。
最后,函数返回 isLatin 的值,表示字符数组是否全部由 Latin 字符组成。

在Unicode标准中,0xFF(255)并不是最大的字符。事实上,Unicode字符的范围远超过255。具体解释如下:

字符范围:

  1. 在ASCII码表中,字符范围是0到127。
  2. 在扩展ASCII码表中,字符范围是0到255。
  3. Unicode字符的范围从 0x0000 到 0x10FFFF,支持大量的字符,包括各种语言的字符、符号、表情符号等。

0xFF的意义:

  1. 0xFF表示255,主要用于表示扩展ASCII字符的范围。
  2. 在检查拉丁字符时,判断条件 chars[i] > 0xFF 用于检查字符是否超出了扩展ASCII范围,即是否包含非拉丁字符(Unicode字符)。

因此,0xFF并不是最大的字符。在这个方法中,chars[i] > 0xFF 用来检测字符数组中是否有字符的值大于255,以判断是否包含非拉丁字符。

这个函数的目的是通过向量化方式快速检查字符数组中是否存在非 Latin 字符,以及通过逐个检查剩余字符的方式进一步确认是否全部为 Latin 字符。这样可以提高判断的效率,并且可以在序列化过程中根据判断结果选择合适的序列化方式。

零拷贝StringBuilder/StringBuffer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public void writeCharsLatin(MemoryBuffer buffer, char[] chars, final int strLen) {
int writerIndex = buffer.writerIndex();
// The `ensure` ensure next operations are safe without bound checks,
// and inner heap buffer doesn't change.
buffer.ensure(writerIndex + 9 + strLen);
final byte[] targetArray = buffer.getHeapMemory();
if (targetArray != null) {
final int targetIndex = buffer.unsafeHeapWriterIndex();
int arrIndex = targetIndex;
targetArray[arrIndex++] = LATIN1;
arrIndex += MemoryUtils.writePositiveVarInt(targetArray, arrIndex, strLen);
writerIndex += arrIndex - targetIndex + strLen;
for (int i = 0; i < strLen; i++) {
targetArray[arrIndex + i] = (byte) chars[i];
}
buffer.unsafeWriterIndex(writerIndex);
} else {
buffer.unsafePut(writerIndex++, LATIN1);
writerIndex += buffer.unsafePutPositiveVarInt(writerIndex, strLen);
final byte[] tmpArray = getByteArray(strLen);
// Write to heap memory then copy is 60% faster than unsafe write to direct memory.
for (int i = 0; i < strLen; i++) {
tmpArray[i] = (byte) chars[i];
}
buffer.put(writerIndex, tmpArray, 0, strLen);
writerIndex += strLen;
buffer.unsafeWriterIndex(writerIndex);
}
}

public void writeCharsUTF16(MemoryBuffer buffer, char[] chars, int strLen) {
int numBytes = MathUtils.doubleExact(strLen);
if (Platform.IS_LITTLE_ENDIAN) {
buffer.writeByte(UTF16);
// FIXME JDK11 utf16 string uses little-endian order.
buffer.writePrimitiveArrayWithSizeEmbedded(chars, Platform.CHAR_ARRAY_OFFSET, numBytes);
} else {
// The `ensure` ensure next operations are safe without bound checks,
// and inner heap buffer doesn't change.
int writerIndex = buffer.writerIndex();
buffer.ensure(writerIndex + 9 + numBytes);
byte[] targetArray = buffer.getHeapMemory();
if (targetArray != null) {
final int targetIndex = buffer.unsafeHeapWriterIndex();
int arrIndex = targetIndex;
targetArray[arrIndex++] = UTF16;
arrIndex += MemoryUtils.writePositiveVarInt(targetArray, arrIndex, strLen);
// Write to heap memory then copy is 250% faster than unsafe write to direct memory.
int charIndex = 0;
for (int i = arrIndex, end = i + numBytes; i < end; i += 2) {
char c = chars[charIndex++];
targetArray[i] = (byte) (c >> StringUTF16.HI_BYTE_SHIFT);
targetArray[i + 1] = (byte) (c >> StringUTF16.LO_BYTE_SHIFT);
}
writerIndex += arrIndex - targetIndex + numBytes;
} else {
buffer.unsafePut(writerIndex++, UTF16);
writerIndex += buffer.unsafePutPositiveVarInt(writerIndex, numBytes);
byte[] tmpArray = getByteArray(strLen);
int charIndex = 0;
for (int i = 0; i < numBytes; i += 2) {
char c = chars[charIndex++];
tmpArray[i] = (byte) (c >> StringUTF16.HI_BYTE_SHIFT);
tmpArray[i + 1] = (byte) (c >> StringUTF16.LO_BYTE_SHIFT);
}
buffer.put(writerIndex, tmpArray, 0, numBytes);
writerIndex += numBytes;
}
buffer.unsafeWriterIndex(writerIndex);
}
}

这段代码包含了两个函数:writeCharsLatin 和 writeCharsUTF16,用于将字符数组按照 Latin 编码或 UTF-16 编码写入到内存缓冲区 buffer 中。
writeCharsLatin 函数首先获取当前写入位置 writerIndex,然后通过 buffer.ensure 方法确保内存缓冲区有足够的空间来写入字符数组。接下来,函数检查 buffer 是否是基于堆内存的,如果是,则使用堆内存的方式进行写入。
在堆内存方式下,函数将 Latin 编码的标识符 LATIN1 写入到目标数组中,并使用 MemoryUtils.writePositiveVarInt 方法将字符数组的长度写入到目标数组中。然后,函数使用一个循环,将字符数组中的每个字符转换为字节,并写入到目标数组中。最后,函数更新写入位置 writerIndex,并将其设置为新的值。
如果 buffer 不是基于堆内存的,函数使用直接内存的方式进行写入。函数先将 Latin 编码的标识符 LATIN1 写入到 buffer 中,然后使用 buffer.unsafePutPositiveVarInt 方法将字符数组的长度写入到 buffer 中。接着,函数创建一个临时的字节数组 tmpArray,并使用一个循环,将字符数组中的每个字符转换为字节,并写入到 tmpArray 中。最后,函数使用 buffer.put 方法将 tmpArray 中的字节写入到 buffer 中,并更新写入位置 writerIndex。
writeCharsUTF16 函数首先根据字符数组的长度计算出需要的字节数 numBytes。如果当前平台是小端序(little-endian),函数先将 UTF-16 编码的标识符 UTF16 写入到 buffer 中,然后使用 buffer.writePrimitiveArrayWithSizeEmbedded 方法将字符数组按照 UTF-16 编码写入到 buffer 中。
如果当前平台不是小端序,函数使用与 writeCharsLatin 函数类似的逻辑进行写入。函数首先获取当前写入位置 writerIndex,然后通过 buffer.ensure 方法确保内存缓冲区有足够的空间来写入字符数组。接下来,函数检查 buffer 是否是基于堆内存的,如果是,则使用堆内存的方式进行写入。
在堆内存方式下,函数将 UTF-16 编码的标识符 UTF16 写入到目标数组中,并使用 MemoryUtils.writePositiveVarInt 方法将字符数组的长度写入到目标数组中。然后,函数使用一个循环,将字符数组中的每个字符转换为字节,并按照 UTF-16 编码的规则写入到目标数组中。最后,函数更新写入位置 writerIndex。
如果 buffer 不是基于堆内存的,函数使用直接内存的方式进行写入。函数先将 UTF-16 编码的标识符 UTF16 写入到 buffer 中,然后使用 buffer.unsafePutPositiveVarInt 方法将字符数组的长度写入到 buffer 中。接着,函数创建一个临时的字节数组 tmpArray,并使用一个循环,将字符数组中的每个字符按照 UTF-16 编码的规则转换为字节,并写入到 tmpArray 中。最后,函数使用 buffer.put 方法将 tmpArray 中的字节写入到 buffer 中,并更新写入位置 writerIndex。
这两个函数的目的是将字符数组按照 Latin 编码或 UTF-16 编码写入到内存缓冲区中,以便进行后续的序列化操作。具体的写入方式根据 buffer 是否基于堆内存以及当前平台的字节序来确定。

获取器函数

1
2
3
4
5
6
7
8
9
10
11
12
public static Object makeGetterFunction(Method method, Class<?> returnType) {
MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(method.getDeclaringClass());
try {
// Why `lookup.findGetter` doesn't work?
// MethodHandle handle = lookup.findGetter(field.getDeclaringClass(), field.getName(),
// field.getType());
MethodHandle handle = lookup.unreflect(method);
return _JDKAccess.makeGetterFunction(lookup, handle, returnType);
} catch (IllegalAccessException ex) {
throw new RuntimeException(ex);
}
}

这段代码定义了一个静态方法 makeGetterFunction,用于创建一个获取器函数(getter function)。该方法接受两个参数:method 和 returnType。method 是一个方法对象,表示要创建获取器函数的方法;returnType 是一个 Class 对象,表示方法的返回类型。
首先,方法通过 _JDKAccess._trustedLookup 方法获取一个 MethodHandles.Lookup 对象,用于执行方法句柄的查找操作。然后,使用 lookup.unreflect 方法将 method 转换为一个方法句柄(MethodHandle)对象,以便后续的操作。
接下来,方法调用 _JDKAccess.makeGetterFunction 方法,传递 lookup、handle 和 returnType 作为参数,以创建获取器函数。具体的实现细节在该方法内部。如果在获取方法句柄或创建获取器函数的过程中发生了 IllegalAccessException 异常,方法将捕获该异常并抛出一个 RuntimeException,将原始异常作为其原因。
总体而言,该方法的目的是通过反射和方法句柄机制,创建一个获取器函数,用于获取指定方法的返回值。

  1. writeCharsLatin 函数用于将字符数组按照 Latin 编码写入到内存缓冲区中。
  2. writeCharsUTF16 函数用于将字符数组按照 UTF-16 编码写入到内存缓冲区中。
  3. 这两个函数根据内存缓冲区是否基于堆内存以及当前平台的字节序来确定写入方式。
  4. makeGetterFunction 方法用于创建获取器函数,通过反射和方法句柄机制获取指定方法的返回值。
  5. makeGetterFunction 方法使用 _JDKAccess._trustedLookup 方法获取方法句柄的查找对象。
  6. 方法句柄通过 lookup.unreflect 方法将方法对象转换为方法句柄。
  7. _JDKAccess.makeGetterFunction 方法使用方法句柄和返回类型创建获取器函数。
  8. 如果在获取方法句柄或创建获取器函数的过程中发生 IllegalAccessException 异常,将抛出 RuntimeException。
  9. 这些代码涉及了字节转换、内存缓冲区操作、反射和方法句柄等底层操作。
  10. 目的是实现字符编码的写入和获取器函数的创建,用于后续的序列化和数据访问操作。

热加载能力

其实这个能力并非是本人设计的,是根据原理去利用的:

在Java中,类加载器(ClassLoader)用于加载类和资源。每个类加载器都有其特定的加载范围和优先级。线程的上下文类加载器(Context ClassLoader)是一个特殊的类加载器,它与当前线程相关联,并且可以在加载类和资源时被使用。

上下文类加载器的设置可以由应用程序自行决定,通常用于解决类加载器层次结构中的资源查找问题。在某些情况下,应用程序可能需要使用第三方库或框架,而这些库或框架使用了自定义的类加载器。在这种情况下,上下文类加载器可以被设置为第三方类加载器,以确保正确加载所需的类和资源。

因此,Thread.currentThread().getContextClassLoader() 方法返回的类加载器可以是第三方类加载器,它可能是应用程序中使用的自定义类加载器或其他第三方库中的类加载器。

于是可以使用:

1
2
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
//获取第三方加载器

ref: https://www.sofastack.tech/projects/sofa-boot/sofa-ark-readme/

总结

这次的开源经历更多的是提升了自己的创造力和动手能力,更多的时候是去编写一些从未有过的代码逻辑,没有多少参考代码是可以直接复用的。从一方面说,需要根据这个项目的需求,去从零到一的去实现这个逻辑,有时候会感觉每一行代码都写的很生涩很不好,但是写的代码多了,再回过去去思考,又可以发现自己不足的地方,然后继续去优化原有的代码块,提高代码质量,我想很多项目都是通过反复思考,推导,才会让自己的项目更加的完善。另一方面,这其实也是挺有乐趣的一件事,这其实也让我找到了当初学习数据结构的时候的那种乐趣,老师给你一个二叉树或者是图的逻辑,然后一点一点的去完成它,中途可能会碰到各种问题,但是将这个逻辑彻底实现为你的代码中,也会感到一些成就感,这给你的反馈也是一种特殊的礼物。虽然,开源看起来也是一件比较困难的事情,但是每一点的努力都会让你有所提升,这也是很值得去投入的事情,相信,道阻且长,行则将至。