使用SpringDoc时,FastJson作为HttpMessageConverters的问题

使用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 字符串。

  1. SerializeWriter: SerializeWriter 是一个底层的字符缓冲区,用于将 Java 对象序列化为 JSON 字符串。它提供了一系列方法来操作和构建字符串缓冲区,用于存储序列化后的 JSON 数据。在序列化过程中,SerializeWriter 将会被 JSONSerializer 使用来写入最终的 JSON 字符串。
  2. 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 字符串。

总结:SerializeWriterFastjson 中底层的字符缓冲区,用于存储序列化后的 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!

相关推荐
Rverdoser23 分钟前
【SpringBoot3】Spring Boot 3.0 集成 Mybatis Plus
spring boot·后端·mybatis
等一场春雨33 分钟前
Spring Boot 3 文件上传、多文件上传、大文件分片上传、文件流处理以及批量操作
java·spring boot·后端
骑着王八撵玉兔1 小时前
【非关系型数据库Redis 】 入门
java·数据库·spring boot·redis·后端·缓存·nosql
Just_Paranoid1 小时前
API 设计:从基础到最佳实践
后端·架构设计·系统设计·api设计
007php0075 小时前
gozero项目接入elk的配置与实战
运维·开发语言·后端·elk·golang·jenkins·ai编程
xiaosannihaiyl245 小时前
Lua语言的计算机基础
开发语言·后端·golang
hnmpf5 小时前
flask-admin 在modelview 视图中重写on_model_change 与after_model_change
后端·python·flask
hnmpf5 小时前
flask-admin 框架下添加menu_links 菜单
后端·python·flask
卷福同学6 小时前
2024年终总结:选择错误、加班三月、降薪、面试无果...
后端·面试·程序员
m0_748248776 小时前
Spring Boot 集成 MyBatis 全面讲解
spring boot·后端·mybatis