refactor(extension): 重构 FastJsonHttpMessageConverter 以提高性能 (#3754)

* refactor(extension): 重构 FastJsonHttpMessageConverter 以提高性能
- 新增 fastRead 方法,用于高效读取请求体
- 优化内存分配,避免不必要的数组拷贝
- 使用 JSONReader.Context 和 JSONWriter.Context 缓存避免重复构造 Context 的性能开销
- 增加对大数组长度的处理逻辑,防止内存溢出

* style(extension): 删除 FastJsonConfig 类中的冗余空行

* refactor(extension-spring5): 调整请求体初始化容量的变量命名和访问修饰符,避免非预期的修改

* refactor(extension-spring5): 调整部分内部方法为 private,限制外部访问,加强封装性

* 调整:JSON 请求主体数据初始容量为 8KB
This commit is contained in:
CodePlayer 2025-09-14 19:32:21 +08:00 committed by GitHub
parent 007746de07
commit e99c4ad2b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 155 additions and 18 deletions

View File

@ -1699,7 +1699,6 @@ public interface JSON {
* @return {@link T} or {@code null}
* @throws JSONException If a parsing error occurs
*/
@SuppressWarnings("unchecked")
static <T> T parseObject(
byte[] bytes,
Type type,
@ -1719,6 +1718,26 @@ public interface JSON {
);
context.setDateFormat(format);
return parseObject(bytes, type, context);
}
/**
* Parses the json string as {@link T}. Returns
* {@code null} if received {@link String} is {@code null} or empty.
*
* @param bytes the specified UTF8 text to be parsed
* @param type the specified actual type
* @param context the specified custom context
* @return {@link T} or {@code null}
* @throws JSONException If a parsing error occurs
* @throws NullPointerException If received context is null
*/
@SuppressWarnings("unchecked")
static <T> T parseObject(byte[] bytes, Type type, JSONReader.Context context) {
if (bytes == null || bytes.length == 0) {
return null;
}
boolean fieldBased = (context.features & JSONReader.Feature.FieldBased.mask) != 0;
ObjectReader<T> objectReader = context.provider.getObjectReader(type, fieldBased);

View File

@ -21,6 +21,7 @@ import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
/**
* Fastjson for Spring MVC Converter.
@ -93,24 +94,96 @@ public class FastJsonHttpMessageConverter
return readType(getType(clazz, null), inputMessage);
}
private Object readType(Type type, HttpInputMessage inputMessage) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
InputStream in = inputMessage.getBody();
/** Default initialization capacity when content-length is not specified */
private static int REQUEST_BODY_INITIAL_CAPACITY = 8192;
byte[] buf = new byte[1024 * 64];
for (; ; ) {
int len = in.read(buf);
if (len == -1) {
break;
}
public static void setRequestBodyInitialCapacity(int initialCapacity) {
if (initialCapacity < 128 || initialCapacity > 1024 * 1024) {
throw new IllegalArgumentException("invalid initialCapacity: " + initialCapacity);
}
REQUEST_BODY_INITIAL_CAPACITY = initialCapacity;
}
if (len > 0) {
baos.write(buf, 0, len);
/**
* @param contentLength The content length of the request message. If -1 is passed, it means unknown.
*/
protected static int calcInitialCapacity(long contentLength) {
return contentLength == -1 || contentLength > Integer.MAX_VALUE
? REQUEST_BODY_INITIAL_CAPACITY
// The maximum limit is 1MB to prevent fake request headers
: (int) Math.min(contentLength, 1024 * 1024);
}
/**
* @param in the specified input stream
* @param contentLength -1 means unknown
*/
protected static byte[] fastRead(final InputStream in, final long contentLength) throws IOException {
final int expectSize = calcInitialCapacity(contentLength);
byte[] body = new byte[expectSize];
int offset = in.read(body, 0, body.length);
if (offset == -1) {
body = new byte[0];
} else if (contentLength == -1 || offset != contentLength) {
final byte[] buf = new byte[1024];
int len = in.read(buf);
while (len != -1) { // Refer to the implementation of ByteArrayOutputStream
final int minRequired = offset + len;
final int oldLength = body.length;
if (minRequired > oldLength) {
int newLength = newLength(oldLength, minRequired - oldLength, oldLength);
byte[] newBody = Arrays.copyOf(body, newLength);
System.arraycopy(buf, 0, newBody, offset, len);
body = newBody;
} else {
System.arraycopy(buf, 0, body, offset, len);
}
offset = minRequired;
len = in.read(buf);
}
byte[] bytes = baos.toByteArray();
if (offset != body.length) {
body = Arrays.copyOf(body, offset);
}
}
return body;
}
return JSON.parseObject(bytes, type, config.getDateFormat(), config.getReaderFilters(), config.getReaderFeatures());
// see jdk.internal.util.ArraysSupport.SOFT_MAX_ARRAY_LENGTH
private static final int SOFT_MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8;
// see jdk.internal.util.ArraysSupport.newLength( )
private static int newLength(int oldLength, int minGrowth, int prefGrowth) {
// preconditions not checked because of inlining
// assert oldLength >= 0
// assert minGrowth > 0
int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow
if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
return prefLength;
} else {
// put code cold in a separate method
return hugeLength(oldLength, minGrowth);
}
}
// see jdk.internal.util.ArraysSupport.hugeLength( )
private static int hugeLength(int oldLength, int minGrowth) {
int minLength = oldLength + minGrowth;
if (minLength < 0) { // overflow
throw new OutOfMemoryError("Required array length " + oldLength + " + " + minGrowth + " is too large");
} else if (minLength <= SOFT_MAX_ARRAY_LENGTH) {
return SOFT_MAX_ARRAY_LENGTH;
} else {
return minLength;
}
}
protected Object readType(Type type, HttpInputMessage inputMessage) {
final long contentLength = inputMessage.getHeaders().getContentLength(); // -1 表示未知
try {
final byte[] body = fastRead(inputMessage.getBody(), contentLength);
return JSON.parseObject(body, type, config.readerContext());
} catch (JSONException ex) {
throw new HttpMessageNotReadableException("JSON parse error: " + ex.getMessage(), ex, inputMessage);
} catch (IOException ex) {
@ -138,10 +211,7 @@ public class FastJsonHttpMessageConverter
}
contentLength = JSON.writeTo(
baos, object,
config.getDateFormat(),
config.getWriterFilters(),
config.getWriterFeatures()
baos, object, config.writerContext()
);
}

View File

@ -1,6 +1,7 @@
package com.alibaba.fastjson2.support.config;
import com.alibaba.fastjson2.JSONB;
import com.alibaba.fastjson2.JSONFactory;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.SymbolTable;
@ -64,6 +65,11 @@ public class FastJsonConfig {
*/
private SymbolTable symbolTable;
/** internal cache for JSONReader.Context, avoid repeatedly constructing new objects */
private transient JSONReader.Context readerContext;
/** internal cache for JSONWriter.Context, avoid repeatedly constructing new objects */
private transient JSONWriter.Context writerContext;
/**
* init param.
*/
@ -96,6 +102,7 @@ public class FastJsonConfig {
*/
public void setCharset(Charset charset) {
this.charset = charset;
this.clearContext();
}
/**
@ -114,6 +121,7 @@ public class FastJsonConfig {
*/
public void setDateFormat(String dateFormat) {
this.dateFormat = dateFormat;
this.clearContext();
}
/**
@ -132,6 +140,7 @@ public class FastJsonConfig {
*/
public void setReaderFeatures(JSONReader.Feature... readerFeatures) {
this.readerFeatures = readerFeatures;
this.readerContext = null;
}
/**
@ -150,6 +159,7 @@ public class FastJsonConfig {
*/
public void setWriterFeatures(JSONWriter.Feature... writerFeatures) {
this.writerFeatures = writerFeatures;
this.writerContext = null;
}
/**
@ -168,6 +178,7 @@ public class FastJsonConfig {
*/
public void setReaderFilters(Filter... readerFilters) {
this.readerFilters = readerFilters;
this.readerContext = null;
}
/**
@ -186,6 +197,7 @@ public class FastJsonConfig {
*/
public void setWriterFilters(Filter... writerFilters) {
this.writerFilters = writerFilters;
this.writerContext = null;
}
/**
@ -224,6 +236,7 @@ public class FastJsonConfig {
*/
public void setJSONB(boolean jsonb) {
this.jsonb = jsonb;
this.clearContext();
}
/**
@ -244,5 +257,40 @@ public class FastJsonConfig {
*/
public void setSymbolTable(String... names) {
this.symbolTable = JSONB.symbolTable(names);
this.clearContext();
}
/**
* Clear internal caches of JSONReader.Context and JSONWriter.Context
*/
public void clearContext() {
readerContext = null;
writerContext = null;
}
public JSONReader.Context readerContext() {
JSONReader.Context context = readerContext;
if (context == null) { // Concurrency may occur, but it will not cause any problems
context = new JSONReader.Context(JSONFactory.getDefaultObjectReaderProvider(), jsonb ? symbolTable : null, readerFilters, readerFeatures);
context.setDateFormat(dateFormat);
this.readerContext = context;
}
return context;
}
public JSONWriter.Context writerContext() {
JSONWriter.Context context = writerContext;
if (context == null) {
context = new JSONWriter.Context(dateFormat, writerFeatures);
if (dateFormat != null && !dateFormat.isEmpty()) {
context.setDateFormat(dateFormat);
}
// symbolTable is only in JSONWriter class
if (writerFilters != null && writerFilters.length > 0) {
context.configFilter(writerFilters);
}
this.writerContext = context;
}
return context;
}
}