文章目录

  • 相关技术栈
  • 起因
  • 分析
    • 1. 报错位置
    • 2. 接口定义
    • 3. Kotlin编译成Java
    • 4. springfox源码分析
      • 1. 判断是否加了`@RequestBody`等参数
      • 2. 包装`RequestParameter`
      • 3. 将`RequestParameter` 添加到`HashSet`
    • 关于hash碰撞的代码片段
  • 总结
  • github

相关技术栈

Kotlin1.5 Springboot2.5 Springfox3.0

起因

最近对接支付宝的电脑网站支付,需要定义一个支持表单Post提交的接口来接收支付宝的回调。在定义完接口后发现Springfox初始化swagger时报了空指针,导致swagger api doc无法加载

分析

1. 报错位置

springfox.documentation.service.RequestParameter#equals

springfox.documentation.schema.Example#equals

2. 接口定义

首先,来看看出问题的接口定义

@ApiOperation("xxx")
@ApiResponse(code = 0,message = "ok",
)
@PostMapping("/api",consumes = [MediaType.APPLICATION_FORM_URLENCODED_VALUE]
)
fun api(dto:Dto) {//do something
}

Dto定义

@ApiModel
class Dto {@ApiModelPropertylateinit var field: String
}

3. Kotlin编译成Java

看起来似乎没啥毛病,很nice。为什么会报空指针呢?首先我们来看下Dto编译成Java代码是什么样子

public final class Dto {@ApiModelPropertypublic String field;@NotNullpublic final String getField() {String var1 = this.field;if (var1 != null) {return var1;} else {Intrinsics.throwUninitializedPropertyAccessException("field");throw null;}}public final void setField(@NotNull String var1) {Intrinsics.checkNotNullParameter(var1, var1);this.field = var1;}
}

可以发现,field访问修饰符是public。事实上这个public就是罪魁祸首

4. springfox源码分析

我们先来看一下springfox处理接口参数的一个大致过程

  1. 判断接口参数前是否加了@RequestBody等参数,如果没加则进入第二步
  2. 将Dto里的所有public属性跟public get方法包装成RequestParameter
  3. 将所有的RequestParameter 添加到HashSet

1. 判断是否加了@RequestBody等参数

先看看第一步相关的源码

package springfox.documentation.spring.web.readers.operation;public class OperationParameterReader implements OperationBuilderPlugin {private List<Compatibility<springfox.documentation.service.Parameter, RequestParameter>>readParameters(OperationContext context) {List<ResolvedMethodParameter> methodParameters = context.getParameters();List<Compatibility<springfox.documentation.service.Parameter, RequestParameter>> parameters = new ArrayList<>();int index = 0;//1. 遍历方法所有参数for (ResolvedMethodParameter methodParameter : methodParameters) {//2. 判断是否需要扩展。if (shouldExpand(methodParameter, alternate)) {parameters.addAll(expander.expand(new ExpansionContext("", alternate, context)));} else {//...}}return parameters.stream().filter(hiddenParameter().negate()).collect(toList());}private boolean shouldExpand(final ResolvedMethodParameter parameter, ResolvedType resolvedParamType) {return !parameter.hasParameterAnnotation(RequestBody.class)&& !parameter.hasParameterAnnotation(RequestPart.class)&& !parameter.hasParameterAnnotation(RequestParam.class)&& !parameter.hasParameterAnnotation(PathVariable.class)&& !builtInScalarType(resolvedParamType.getErasedType()).isPresent()&& !enumTypeDeterminer.isEnum(resolvedParamType.getErasedType())&& !isContainerType(resolvedParamType)&& !isMapType(resolvedParamType);}}

这里可以看到shouldExpand会判断我们的参数是否被@RequestBody这类注解标注,而我们定义的接口是一个接收form表单的post接口,其前面的注解应该是@ModelAttribute(不加也可以)。所以这里就会进到expander.expand这里会将类拆解开来,对每个字段逐一解析。 然后进入到如下代码:

public class ModelAttributeParameterExpander {public List<Compatibility<springfox.documentation.service.Parameter, RequestParameter>> expand(ExpansionContext context) {//...//将model里所有的getter方法跟public修饰的字段包装成ModelAttributeFieldList<ModelAttributeField> attributes =allModelAttributes(propertyLookupByGetter,getters,fieldsByName,alternateTypeProvider,context.ignorableTypes());//处理getter方法跟public字段,将其包装为对应的RequestParamtersimpleFields.forEach(each -> parameters.add(simpleFields(context.getParentName(), context, each)));return parameters.stream().filter(hiddenParameter().negate()).filter(voidParameters().negate()).collect(toList());}private List<ModelAttributeField> allModelAttributes(Map<Method, PropertyDescriptor> propertyLookupByGetter,Iterable<ResolvedMethod> getters,Map<String, ResolvedField> fieldsByName,AlternateTypeProvider alternateTypeProvider,Collection<Class> ignorables) {//所有getter方法Stream<ModelAttributeField> modelAttributesFromGetters =StreamSupport.stream(getters.spliterator(), false).filter(method -> !ignored(alternateTypeProvider, method, ignorables)).map(toModelAttributeField(fieldsByName, propertyLookupByGetter, alternateTypeProvider));//所有public修饰的字段Stream<ModelAttributeField> modelAttributesFromFields =fieldsByName.values().stream().filter(ResolvedMember::isPublic).filter(ResolvedMember::isPublic).map(toModelAttributeField(alternateTypeProvider));return Stream.concat(modelAttributesFromFields,modelAttributesFromGetters).collect(toList());}}

接下来通过ModelAttributeParameterExpander.simpleFields进入如下代码

package springfox.documentation.swagger.readers.parameter;public class SwaggerExpandedParameterBuilder implements ExpandedParameterBuilderPlugin {@Overridepublic void apply(ParameterExpansionContext context) {//1. 查找字段上的ApiModelProperty注解,context则为单个字段或者getter方法的信息集合//如果字段上存在ApiModelProperty注解,则返回的Optional存在相关注解包装对象//如果是getter方法,在context的metadataAccessor中会保留一份getter对应的字段的信息//所以这里字段跟getter的处理方式相同Optional<ApiModelProperty> apiModelPropertyOptional = context.findAnnotation(ApiModelProperty.class);//2. 如果字段上存在ApiModelProperty注解,则执行fromApiModelPropertyapiModelPropertyOptional.ifPresent(apiModelProperty -> fromApiModelProperty(context, apiModelProperty));}
}

显然,我们的Dtofield字段上是有ApiModelProperty注解的。所以接下来进入fromApiModelProperty

2. 包装RequestParameter

package springfox.documentation.swagger.readers.parameter;public class SwaggerExpandedParameterBuilder implements ExpandedParameterBuilderPlugin {private void fromApiModelProperty(ParameterExpansionContext context,ApiModelProperty apiModelProperty) {//...//1. 生成RequestParameterBuildercontext.getRequestParameterBuilder().description(descriptions.resolve(apiModelProperty.value())).required(apiModelProperty.required()).hidden(apiModelProperty.hidden())//2. apiModelProperty.example()默认返回空字符串。//所以这里会生成一个除了value其他字段都为空的Example实例.example(new ExampleBuilder().value(apiModelProperty.example()).build()).precedence(SWAGGER_PLUGIN_ORDER).query(q -> q.enumerationFacet(e -> e.allowedValues(allowable)));}
}

所以这里就会生成一个跟我们字段或者getter对应的RequestParameterBuilder,且其字段scalarExample除了value以外其他字段都为null。同时可以看出来,字段跟与字段对应的getter生成的RequestParameterBuilder应该是一模一样的,因为取的都是字段注解上的信息.

所以,其build()出来的RequestParameter的字段值也是一模一样的。因为是RequestParameter#equals报错,我们先来看看其equals方法

public boolean equals(Object o) {if (this == o) {return true;}if (o == null || getClass() != o.getClass()) {return false;}RequestParameter that = (RequestParameter) o;return parameterIndex == that.parameterIndex &&Objects.equals(scalarExample, that.scalarExample);}

可以看到最终会对RequestParameter里的scalarExample进行equals比较。所以如果scalarExample不为空则必然进入进入Example#equals

  @Overridepublic boolean equals(Object o) {if (this == o) {return true;}if (o == null || getClass() != o.getClass()) {return false;}Example example = (Example) o;return id.equals(example.id) &&Objects.equals(summary, example.summary) &&Objects.equals(description, example.description) &&value.equals(example.value) &&externalValue.equals(example.externalValue) &&mediaType.equals(example.mediaType) &&extensions.equals(example.extensions);}

还记得前面提到的RequestParameterBuilder只为Example的value字段赋了值吗?所以,只要触发Example#equals ,则必然会报出NullPointException

所以接下来这个RequestParameterBuilder在哪完成build()其实已经不需要关心了,我们只需要找到是哪里触发了这个equals即可。

3. 将RequestParameter 添加到HashSet

我们进入第一步所展示的代码的调用方,代码片段如下:

package springfox.documentation.spring.web.readers.operation;public class OperationParameterReader implements OperationBuilderPlugin {@Overridepublic void apply(OperationContext context) {//触发第一步List<Compatibility<springfox.documentation.service.Parameter, RequestParameter>> compatibilities= readParameters(context);//拿出compatibilities#getModern返回的数据组成一个HashSetCollection<RequestParameter> requestParameters = compatibilities.stream().map(Compatibility::getModern).filter(Optional::isPresent).map(Optional::get).collect(toSet());context.operationBuilder().requestParameters(aggregator.aggregate(requestParameters));}
}

看到HashSet是不是突然想到了什么?没错,HashCode相同导致Hash碰撞进而触发equals。所以我们先来看看compatibilities#getModern究竟返回了什么。

package springfox.documentation.spring.web.plugins;//OperationParameterReader.readParameters
//  -> ModelAttributeParameterExpander.expand
//    -> ModelAttributeParameterExpander.simpleFields
//      -> DocumentationPluginsManager.expandParameter
public class DocumentationPluginsManager {public Compatibility<springfox.documentation.service.Parameter, RequestParameter> expandParameter(ParameterExpansionContext context) {for (ExpandedParameterBuilderPlugin each : parameterExpanderPlugins.getPluginsFor(context.getDocumentationType())) {each.apply(context);}return new Compatibility<>(context.getParameterBuilder().build(),context.getRequestParameterBuilder().build());}
}

我在上面列出了调用链,可以看到,compatibilities#getModern返回的就是我们之前说的RequestParameter。好家伙,赶紧去看RequestParameter#hashCode

  @Overridepublic int hashCode() {return Objects.hash(name,parameterIndex,in,description,required,deprecated,hidden,parameterSpecification,precedence,scalarExample,examples,extensions);}

这里可以看出,如果存在两个字段值相同的RequestParameter,则势必会在因为hash碰撞而触发equals,从而最终导致NullPointException

关于hash碰撞的代码片段

package java.util;public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;//为空则初始化if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//hash值与长度-1按位与。//hash值相同的key必然会落到数组中同一个位置从而后来的元素会进入elseif ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))//......}
}

总结

这次问题很奇葩,一方面是我对Kotlin还是不够熟,对lateinit的了解仅仅停留在很浅的层次。事实上我觉得这应该是Kotlin的编译不合理之处。因为正常的像var定义的属性,默认编译成java代码后,会生成一个私有的字段跟对应的getter&setter方法。同时,对于lateinit想要实现的功能(如果尝试访问没赋值的属性,会抛出异常),我觉得完全没必要把字段用public来修饰。

另一方面,我觉得springfox的设计也有不合理之处,既然有RequestParameter#equals的存在,为什么要允许前面这种默认只赋值一个Example#value的代码存在呢?且从表现上来看,一个public修饰的字段跟一个对应的getter方法,如果字段上不加@ApiModelProperty,则表现正常,加了,则直接导致NullpointException。这不合理,且容易令人困惑。

github

https://github.com/scientificCommunity/blog-sample/blob/main/src/main/kotlin/org/baichuan/example/spring/springfox/Application.kt

踩坑日记之Springfox+Kotlin lateinit引发NullPointException相关推荐

  1. 全志哪吒D1-H Tina Linux Ubuntu 22.04入门踩坑日记

    哪吒D1-H Tina Linux入门踩坑日记 系统环境 源码编译 mklibs-readelf的C++标准问题 m4的SIGSTKSZ问题 libfakeroot的_STAT_VER问题 read_ ...

  2. Win11 + Ubuntu18.04 双系统踩坑日记

    Win11 + Ubuntu18.04 双系统踩坑日记 前言 准备工作 硬件配置 镜像下载 Win11镜像下载 Ubuntu镜像下载 启动盘准备 Win11启动盘 Ubuntu启动盘 Win11安装 ...

  3. 【Flutter混合开发踩坑日记之‘applicationVariants‘ for extension ‘android‘】

    Flutter混合开发踩坑日记之'applicationVariants' for extension 'android' 正文 坑一:Could not get unknown property ' ...

  4. Swarm-BZZ踩坑日记之 如何让METMASK小狐狸显示gbzz

    刚入门bzz的新手还不知道小狐狸是什么的请移步上一章节:Swarm-BZZ踩坑日记之 如何在METMASK小狐狸导入节点地址 在浏览器安装好小狐狸,并添加自己的钱包地址后 会发现只显示ETH,并不显示 ...

  5. ReactNative 在丁香医生项目中引入的踩坑日记

    ReactNative 在丁香医生项目中引入的踩坑日记 this没绑定到函数导致空指针 参考 React-Native 踩坑第二弹-undefined is not a function(evalua ...

  6. springboot踩坑日记—nacos: Error watching Nacos Service change

    springboot踩坑日记-nacos: Error watching Nacos Service change Spring Boot :: (v2.1.5.RELEASE) 错误代码: 07-3 ...

  7. 微信小程序踩坑日记-微信小程序首次加载样式错乱问题

    微信小程序踩坑日记-微信小程序首次加载样式错乱问题 在实际开发项目中,遇到了个棘手的问题,就是在某些因素下,进入小程序发现有些样式发生偏移.错乱等问题 问题原因:-未知(估计是组件的问题) ↓ 解决办 ...

  8. c++字符串操作之std::ostringstream踩坑日记

    c++字符串操作之std::ostringstream踩坑日记 在开发过程中经常会遇到字符串操作,而std::string又没有format操作,这就很难受了. 于是我找到了std::ostrings ...

  9. Antd Pro V4 protable详解(ps:踩坑日记)

    Antd Pro V4 protable详解(ps:踩坑日记) 写在前面: 在这篇文章中,你会了解到: protable 中的cloumns属性详解 protable数据加载和处理(两种方法,直接使用 ...

最新文章

  1. eclipse 重启/打开内置浏览器
  2. 近期活动盘点:2018数据与媒介发展论坛、大数据应用中日交流论坛(11.04-11.15)...
  3. excel文件数据导入mysql数据库中_将excel里面的数据导入mysql数据库中
  4. 容器set和multiset
  5. js实现下拉框多选_bootstrap基础快速入门-10 dropdown下拉框
  6. Dbvisualizer9.0.6 解决中文乱码
  7. 如何构建基于.NET Core和云环境下的微服务技术体系?
  8. partproble在RHEL 6下无法更新分区信息
  9. 《Puppet实战手册》——导读
  10. WinCE应用程序产生Data Abort 错误分析
  11. 谷歌有情怀!谷歌开放大规模音频数据集 AudioSet
  12. php如何接受用户邮箱发送信息,怎么将用户购物车的产品发送到邮箱
  13. 编译单元为什么只能有一个public类
  14. CodeForces 877E DFS序+线段树
  15. MATLAB绘制对数幅频特性
  16. 工程测量(地形图测量)
  17. 从零开始,把Raspberry Pi打造成双栈11n无线路由器,支持教育网原生IPv6
  18. unity3d 获取 Advertising ID
  19. python3爬虫数据清洗与可视化实战pdf百度云_Python 3爬虫、数据清洗与可视化实战_PDF电子书...
  20. 李嘉璇:技术人如何深入人工智能

热门文章

  1. 数字电路/涉及电路/常见芯片简介/74系列的等
  2. MATLAB数学建模之排列图和柱状图
  3. 【论文写作】Endnote插入参考文献对应的英文期刊名全称如何修改为缩写形式(内附最新Endnote参考文献期刊名26627种全称和对应缩写表)
  4. flash中导入音乐出现“读取文件时出…
  5. CMTS Internal Forwarding Model
  6. NFC smart tag竟然有四种 Type 1 Tag Type 2 Tag Type 3 Tag Type
  7. TYPE-C接口引脚详解
  8. html、css、js实现普通计算器
  9. win7系统定时删除数据的批处理命令_win7系统使用批处理删除文件详细教程
  10. 【ChatGPT】ChatGPT 能否取代程序员?