使用SpringDoc时,FastJson作为HttpMessageConverters的问题
从notion直接导出,可能存在排版问题, Notion在线文档链接:yuxs.notion.site/SpringDoc-F...
Tags: I
[BUG] 集成openapi有误,参考#387 · Issue #1256 · alibaba/fastjson2
上面链接是在FastJson2中有人给出的详细解答。
我的项目中使用的是FastJson1的最新版本,同样也存在这个问题。
问题
项目同时使用了FastJsonHttpMessageConverter
作为HTTP消息转换器,并且配置了springdoc-openapi-ui
之后,进入SpringDoc的swagger-ui/index.html
报错:
Unable to render this definition The provided definition does not specify a valid version field.
Please indicate a valid Swagger or OpenAPI version field. Supported version fields are swagger: "2.0" and those that match openapi: 3.0.n (for example, openapi: 3.0.0).
项目背景
项目中添加了springdoc-openapi-ui
依赖,用来自动生成项目的接口文档;项目本身添加了FastJsonHttpMessageConverter
作为*HttpMessageConverter
(SpringMVC处理HTTP消息转换器的集合)*
采用配置了HttpMessageConverters
的类Bean
进行注入。所以就提供了一个将FastJsonHttpMessageConverter
作为消息转换器列表的第一个元素的集合。
Debug
💡 需要注意的是,在对`org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters(T, org.springframework.core.MethodParameter, org.springframework.http.server.ServletServerHttpRequest, org.springframework.http.server.ServletServerHttpResponse)`调试过程中,进入swagger-ui页面时,会发起两次请求,第一个请求,返回的数据之前,类型是一个`TreeMap`,我们不需要关心,它会被正常接受。第二个请求在返回之前是一个`byte[]` ,此时fastjson处理之后不能被正常接受使用。
在debug后发现OpenApi的方法org.springdoc.api.AbstractOpenApiResource#writeJsonValue
是采用JackSon将所有接口的配置信息转化成了一个byte[]数组,并返回:
接着对org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters
方法调试,该方法的核心作用就是按照要返回的结果类型,匹配一个合适的*HttpMessageConverter
* 通过消息转换器的处理,返回数据。
核心代码部分:
在进入swager-ui页面时,对api-doc的请求头中,accept标识了为application/json,**/*
* 表明客户端优先想接受到json数据格式。恰好此时我们服务端的消息转换处理器集合*this*.messageConverters
的第一个元素就是可以处理该返回类型的(FastJson的消息处理转换器)
跟具体一点,会在FastJson详细转换器的canWrite方法中返回true,从而使用FastJson作为转换器:
并且还发现一个东西,FastJson消息转换器默认会接受所有媒体类型的消息处理-_-。所以将FastJson的消息转换器放在第一位,处理完之后,直接return,其它消息处理也不会再遍历。绝杀。。。。
接着再看FastJsonHttpMessageConverter
内部的关键方法是怎么处理byte[]数组转换的:
前面的判断都不会命中,因为value是byte[]类型的。那么这个JSON.*writeJSONStringWithFastJsonConfig
* 是如何处理输出值的?
看到这,需要补充点说明:
💡 在 Fastjson 中,`SerializeWriter` 和 `JSONSerializer` 是两个核心类,用于序列化 Java 对象为 JSON 字符串。
SerializeWriter
:SerializeWriter
是一个底层的字符缓冲区,用于将 Java 对象序列化为 JSON 字符串。它提供了一系列方法来操作和构建字符串缓冲区,用于存储序列化后的 JSON 数据。在序列化过程中,SerializeWriter
将会被JSONSerializer
使用来写入最终的 JSON 字符串。JSONSerializer
:JSONSerializer
是负责将 Java 对象序列化为 JSON 字符串的类。它会遍历 Java 对象的属性,并在SerializeWriter
中生成相应的 JSON 数据。JSONSerializer
同时也支持配置不同的序列化选项,例如设置日期格式、处理循环引用等。
下面给一个简短的代码示例,辅助对照理解:
java
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.JSONSerializer;
import com.alibaba.fastjson.serializer.SerializeWriter;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class Main {
public static void main(String[] args) {
MyObject obj = new MyObject();
obj.setId(1);
obj.setName("John");
// 创建 SerializeWriter 对象
SerializeWriter writer = new SerializeWriter();
// 创建 JSONSerializer 对象,并设置 SerializeWriter
JSONSerializer serializer = new JSONSerializer(writer);
// 配置序列化选项,例如设置日期格式、处理循环引用等
serializer.config(SerializerFeature.WriteDateUseDateFormat, true);
// 将 Java 对象序列化为 JSON 字符串
serializer.write(obj);
String jsonString = writer.toString();
System.out.println(jsonString);
// 关闭 SerializeWriter
writer.close();
}
}
class MyObject {
private int id;
private String name;
// 省略 getter 和 setter 方法
}
在上面的示例中,我们创建了一个 SerializeWriter
对象来存储序列化后的 JSON 数据。然后,我们创建了一个 JSONSerializer
对象,并将 SerializeWriter
对象传递给它。通过调用 serializer.write(obj)
来将 Java 对象序列化为 JSON 字符串。最后,我们通过调用 writer.toString()
获取最终的 JSON 字符串。
总结:SerializeWriter
是 Fastjson
中底层的字符缓冲区,用于存储序列化后的 JSON 数据;JSONSerializer
是负责处理 Java 对象序列化为 JSON 字符串的类,它使用 SerializeWriter
对象来写入最终的 JSON 数据。
进入write方法内部:
这里获得了一个ObjectSerializer
,实际类型是PrimitiveArraySerializer
。
💡 在 Fastjson 中,`ObjectSerializer` 是一个接口,用于自定义对象序列化的行为。通过实现 `ObjectSerializer` 接口,你可以定义自己的序列化规则,以满足特定的需求。
ObjectSerializer
接口定义了一个方法 void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features)
,该方法接收以下参数:
JSONSerializer serializer
:当前的 JSON 序列化器,你可以通过它来实现更复杂的序列化逻辑。Object object
:待序列化的对象。Object fieldName
:待序列化的对象的属性名。Type fieldType
:待序列化的对象的属性类型,可以帮助你实现更加精确的序列化。int features
:序列化特性,例如日期格式化、循环引用处理等。
自定义的序列化器需要实现 ObjectSerializer
接口,并根据自己的需求实现 write
方法。在 write
方法中,你可以根据待序列化对象的类型、属性名和属性类型,编写逻辑来生成相应的 JSON 数据。
这个PrimitiveArraySerializer
就是按照数组类型匹配,然后按匹配到的类型写到SerializeWriter
中,其中最后匹配到了byte[]
:
writeByteArray(*byte*[] bytes)
方法内部,按照base64算法将byte[]数组编码输出,有兴趣可以看一下。
到这,就明白了最后接口返回的数据是一个被base64编码后的纯文本内容,并不是结构化的json数组。
处理方法也很简单,只要让byte[]
的消息处理走默认的*ByteArrayHttpMessageConverter
* 就行。在SpringBoot默认的配置中,ByteArrayHttpMessageConverter始终是在消息列表处理的第一个,也就是0号元素,所以只需要这样处理即可:
java
@Component
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
WebMvcConfigurer.super.configureMessageConverters(converters);
}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// converters.add(0, new ByteArrayHttpMessageConverter());
// 默认第0个转换器是ByteArrayHttpMessageConverter,处理byte[]数据的转换
FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
converters.add(1, fastJsonHttpMessageConverter);
}
}
将Fastjson的消息处理筛在1号位置(第二个)。
这里还有个细节,扩展MessageConverters
的逻辑建议写在extendMessageConverters
方法中,而不是configureMessageConverters
至于为什么,可以看看Spring的源码注释。
到此,暂时Over!