使用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!

相关推荐
Easonmax20 分钟前
用 Rust 打造可复现的 ASCII 艺术渲染器:从像素到字符的完整工程实践
开发语言·后端·rust
百锦再25 分钟前
选择Rust的理由:从内存管理到抛弃抽象
android·java·开发语言·后端·python·rust·go
小羊失眠啦.28 分钟前
深入解析Rust的所有权系统:告别空指针和数据竞争
开发语言·后端·rust
q***71851 小时前
Spring Boot 集成 MyBatis 全面讲解
spring boot·后端·mybatis
大象席地抽烟1 小时前
使用 Ollama 本地模型与 Spring AI Alibaba
后端
程序员小假1 小时前
SQL 语句左连接右连接内连接如何使用,区别是什么?
java·后端
小坏讲微服务1 小时前
Spring Cloud Alibaba Gateway 集成 Redis 限流的完整配置
数据库·redis·分布式·后端·spring cloud·架构·gateway
方圆想当图灵2 小时前
Nacos 源码深度畅游:Nacos 配置同步详解(下)
分布式·后端·github
方圆想当图灵2 小时前
Nacos 源码深度畅游:Nacos 配置同步详解(上)
分布式·后端·github
小羊失眠啦.2 小时前
用 Rust 实现高性能并发下载器:从原理到实战
开发语言·后端·rust