项目场景:
环境
此次影响组件
springboot2.6.6
springdoc1.6.6
背景
由于项目中需要在接口中过滤某些多余或者敏感的属性。于是同事采用jsonfilter的方法
- springboot中引入了相关maven导入
<dependency>
<groupId>com.github.rkonovalov</groupId>
<artifactId>jfilter</artifactId>
<version>${jfilter.version}</version>
</dependency>
- 在application头信息上添加@EnableJsonFilter
- 在接口方法上添加@FieldFilterSetting
eg:
@FieldFilterSetting(className = DataSourceDetectionInfo.class, fields = {"id", "pass"}, behaviour = FilterBehaviour.HIDE_FIELDS)
添加完成之后发现我们想要达成的目标完成了,但是同时带来一个问题,那就是我们的springdoc生成的swagger界面打不开了,业务测试接口也出现了问题,还好在开发阶段,于是上手定位
问题描述
- swagger的页面报错如下:
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).
- 接口报错为json解析不了,发现返回值类似于这样:
<Response>
<success>true</success>
<data>true</data>
</Response>
原因分析:
看到上述现象其实问题已经很明显了,我们接口的返回值被转换成了xml格式,并不是json格式。
为了进一步确认swagger的问题,进一步调用swagger的接口
http://127.0.0.1:9091/v3/api-docs
发现返回值如下:
"{\"openapi\":\"3.0.1\",\"info\":{\"title\":\"workflow's Open API\",\"description\":\"workflow\"},\"servers\":[{\"url\":\"http://127.0.0.1:9091\",\"description\":\"Generated server url\"}],\"tags\":[{}]}"
它是个字符串,并不是json对象
那为什么会出现这种问题呢,我们在头上加了 Accept:application/json,其实是可以解决的,但是毕竟问题是新引入的,我们得知道为什么,最好的办法就是打开代码去debug。
- 启动工程,直接调用接口,如果熟悉springmvc过程,可以直接打开类
org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite
如果不熟悉可以一步步定位,例如从
org.springframework.web.servlet.FrameworkServlet
开始入手,
org.springframework.web.servlet.FrameworkServlet#processRequest
org.springframework.web.servlet.DispatcherServlet#doService
org.springframework.web.servlet.DispatcherServlet#doDispatch
org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter#handle
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#handleInternal
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#invokeHandlerMethod
org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle
org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite#handleReturnValue
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor#handleReturnValue
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters(T, org.springframework.core.MethodParameter, org.springframework.http.server.ServletServerHttpRequest, org.springframework.http.server.ServletServerHttpResponse)
关键方法如下
我们看下 producibletypes的值是什么
acceptableTypes 这个数组的集合是*/* 那也就是要自动推测了,首先我们看到第一个就是xml,再往后看怎么去取的第一个
首先进行了排序(内部实现了自定义比较器),然后如果是*/那就取第一个值,那就很明显了,最终使用了xml来序列化我们的接口返回值。
接下来要搞清楚的就是MediaType里面并没有+xml,为什么会有这个,那就要回过头来看getProducibleMediaTypes 这个方法
数组里第一个值是FilterConverter,这个类做了什么,又是那里来的的呢
看到他自己添加了这几种类型的支持。
直接看他的父类,往下找所有的子类
我们找对应jar,来通过mvn dependency:tree 来找一下是哪个pom引进来的
发现正好是我们新引入的这个类,至此罪魁祸首找到了
在往下看StringHttpMessageConverter 和 FilterConverter 的writeInternal 的实现有什么区别
FilterConverter
protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
MediaType contentType = outputMessage.getHeaders().getContentType();
if (object instanceof FilterClassWrapper) {
FilterClassWrapper wrapper = (FilterClassWrapper)object;
ObjectMapper objectMapper = this.filterConfiguration.getObjectMapperCache().findObjectMapper(wrapper.getMethodParameterDetails());
objectMapper.writeValue(outputMessage.getBody(), wrapper.getObject());
} else {
ObjectMapper objectMapper = this.filterConfiguration.getMapper(contentType);
objectMapper.writeValue(outputMessage.getBody(), object);
}
}
StringHttpMessageConverter
protected void writeInternal(String str, HttpOutputMessage outputMessage) throws IOException {
HttpHeaders headers = outputMessage.getHeaders();
if (this.writeAcceptCharset && headers.get("Accept-Charset") == null) {
headers.setAcceptCharset(this.getAcceptedCharsets());
}
Charset charset = this.getContentTypeCharset(headers.getContentType());
StreamUtils.copy(str, charset, outputMessage.getBody());
}
由上述代码看到具体的实现类上一个是用jackson去write,另一个是用StringUtils去拷贝的,而springdoc的swagger的借口返回值是string,用json去格式话就会出现将原来的String的引号做转义,测试代码如下:
public static void main(String[] args) throws IOException {
String s = "{\"openapi\":\"3.0.1\"}";
final ObjectMapper build = Jackson2ObjectMapperBuilder.json().build();
build.writeValue(new File("./openapi.text"),s);
final FileOutputStream fileOutputStream = new FileOutputStream(new File(".openapi-string.text"));
StreamUtils.copy(s, Charset.defaultCharset(),fileOutputStream);
fileOutputStream.close();
解决方案:
看到上述分析,其实问题很明显,我们尝试一下几种方案:
- 排除掉这个jar包
<dependency>
<groupId>com.github.rkonovalov</groupId>
<artifactId>jfilter</artifactId>
<version>${jfilter.version}</version>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</exclusion>
</exclusions>
</dependency>
但是排除了之后,我们过滤的功能就会报错
2. 第二种是通过实现 WebMvcConfigurer
@Configuration
@EnableWebMvc
public class WebInterceptorAdapter implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType(MediaType.APPLICATION_JSON, MediaType.TEXT_XML, MediaType.APPLICATION_XML,MediaType.ALL);
}
}
然后发现接口的问题解决了,但是springdoc的swagger的界面还是打不开。
3. 拉取工程https://github.com/rkonovalov/jfilter,修改源码如下
@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
MediaType contentType = outputMessage.getHeaders().getContentType();
//If object is FilterClassWrapper try to serialize object using filters(if configured)
if (object instanceof FilterClassWrapper) {
FilterClassWrapper wrapper = (FilterClassWrapper) object;
//Retrieving ObjectMapper from ObjectMapperCache
ObjectMapper objectMapper = filterConfiguration.getObjectMapperCache()
.findObjectMapper(wrapper.getMethodParameterDetails());
//Serialize object with ObjectMapper
objectMapper.writeValue(outputMessage.getBody(), wrapper.getObject());
} else if(object instanceof String){
HttpHeaders headers = outputMessage.getHeaders();
Charset charset = this.getContentTypeCharset(headers.getContentType());
StreamUtils.copy(String.valueOf(object), charset, outputMessage.getBody());
} else {
//Otherwise try to serialize object without filters by default ObjectMapper from filterConfiguration
ObjectMapper objectMapper = filterConfiguration.getMapper(contentType);
objectMapper.writeValue(outputMessage.getBody(), object);
}
}
打包测试,返回值正常
但是修改之后需要完整测试,所以临时将@EnableJsonFilter 拿掉,等测试没有问题之后再使用此注解