踩坑日记之Springfox+Kotlin lateinit引发NullPointException

SpringBoot项目集成Swagger2 使用swagger2编写接口文档方便快捷,swagger2是为了方便前后端分离项目的交互,后端可以用来编写字段的描述,方便前端在对接口的了解每个字段的含义,不会把字段搞混,减轻前后端交互的压力,提高开发效率。 阅读详情

相关技术栈

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 {
	@ApiModelProperty
	lateinit var field: String
}

3. Kotlin编译成Java

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

public final class Dto {
   @ApiModelProperty
   public String field;

   @NotNull
   public 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修饰的字段包装成ModelAttributeField
		    List<ModelAttributeField> attributes =
		        allModelAttributes(
		            propertyLookupByGetter,
		            getters,
		            fieldsByName,
		            alternateTypeProvider,
		            context.ignorableTypes());
				//处理getter方法跟public字段,将其包装为对应的RequestParamter
				simpleFields.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 {
	@Override
  public void apply(ParameterExpansionContext context) {
  
	//1. 查找字段上的ApiModelProperty注解,context则为单个字段或者getter方法的信息集合
	//如果字段上存在ApiModelProperty注解,则返回的Optional存在相关注解包装对象
	//如果是getter方法,在context的metadataAccessor中会保留一份getter对应的字段的信息
	//所以这里字段跟getter的处理方式相同
    Optional<ApiModelProperty> apiModelPropertyOptional = context.findAnnotation(ApiModelProperty.class);
    
	//2. 如果字段上存在ApiModelProperty注解,则执行fromApiModelProperty
    apiModelPropertyOptional.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. 生成RequestParameterBuilder
	    context.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

  @Override
  public 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 {
	@Override
  public void apply(OperationContext context) {

	//触发第一步
    List<Compatibility<springfox.documentation.service.Parameter, RequestParameter>> compatibilities
        = readParameters(context);

	//拿出compatibilities#getModern返回的数据组成一个HashSet
    Collection<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

  @Override
  public 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必然会落到数组中同一个位置从而后来的元素会进入else
	        if ((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

spring boot 集成swagger 3.0 Swagger3在Swagger2的基础上进行了部分升级, 使用和Swagger2没有多少区别。一个重要的优化是依赖的引入,由之前的多个依赖变更为一个依赖,跟随springboot-starter风格,同时引入了新的开关注解 @EnableOpenApi 以代替@EnableSwagger2。一、Maven依赖二、application.yml文件配置三、添加配置。 阅读详情

相关推荐

SpringBoot 2.6. 整合springfox 3.0报错问题解决

添加下方名片,即可获取全套学习资料哦。那么为了不报空指针异常,只能把这些HandlerMethod过滤掉,这些HandlerMethod是从RequestMappingInfoHandlerMapping获取的,那么通过过滤RequestMappingInfoHandlerMapping就可以达到目标,通过判断条件getPatternParser()==null进行判断,这样过滤之后,所有的HandlerMethod的getPatternsCondition()都不是为空的了。启动项目,发现还是报错。...

begefefsef的博客 2500

如何解决这个错误Cannot invoke "springfox.documentation.service.ParameterType.getIn()" because the return val...

这个错误的意思是"无法调用'springfox.documentation.service.ParameterType.getIn()',因为'springfox.documentation.service.RequestParameter.getIn()'的返回值为空"。 通常,这个错误是由于在Swagger文档生成过程中缺少必要的请求参数导致的。要解决这个问题,您可以采取以下步骤: 1.检查S...

weixin_35749440的博客 2120

G120变频器抱闸逻辑在提升机上应用

一、前言: 对于提升机、起重机、堆垛机、升降机等提升机构,负载为位能性负载,在设备启动和停止阶段,由于重物作用,可能会出现“溜车”现象、控制变频器与电机抱闸时许可以解决此问题 使用具有矢量控制功能的变频器 二、参数设置 抱闸控制时序: 启动: 1、变频器接收到启动命令时-----输出电流对电机进行励磁,经过P346电机励磁时间(==丹佛斯 设置启动电流延时0.5S)------达到一定的转矩 2、变频器输出开抱闸命令,经过P1216延时打开抱闸时间(==PLC延时控制打开抱闸) 注意:抱闸

fenglingxicao的博客 1万+

同一代码在不同Idea版本差异下报错

最近和小伙伴们合伙做一个自己的项目,本地遇到一个MapStruct、Swagger2的关联报错,最后发现不同idea版本针对mapstruct的处理机制不同,小记。

zbw237798856的博客 2367

记录一个Swaggger错误

org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘api’ defined in class path resource [com/huacloud/common/swagger/SwaggerAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.

weixin_51509525的博客 954

整合springfox时在线调试,请求参数为null

2019独角兽企业重金招聘Python工程师标准>>> ...

weixin_34406061的博客 294

报错:Cannot invoke “springfox.documentation.service.ParameterType.getIn()

遇到报错:Cannot invoke "springfox.documentation.service.ParameterType.getIn()" because the return value of "springfox.documentation.service.RequestParameter.getIn()" is null怎么解决?

xiaodaidai的博客 1475

springBoot启动失败Caused by: java.lang.ClassNotFoundException: org.springframework.http.MediaType

异常详情: Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled. 2021-08-05 14:44:55.559 ERROR 13568 --- [ main] o.s.boot.SpringApplication : Application run failed "org.spring

lck_csdn的博客 1718

spring boot 2.2.1集成swagger3报错java.lang.NullPointerException: null

自己的spring cloud alibaba脚手架集成swagger3,运行的过程中报出null错误,项目启动没有什么问题接口可以访问,但是swagger功能不正常了。 2021-03-19 03:04:18.645 INFO 45081 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: any request, [org.springframework.security.w

3933

NullPointerException swagger

异常信息 异常关键字 空指针 NullPointerException swagger java.lang.NullPointerException: null at springfox.documentation.swagger2.mappers.RequestParameterMapper.bodyParameter(RequestParameterMapper.java:264) at springfox.documentation.swagger2.mappers.RequestPara

愤怒的菜鸟博客 3551

spring boot 2.6.0 使用swagger报 ‘documentationPluginsBootstrapper‘; java.lang.NullPointerException 错

pom 配置如下 <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-boot-starter</artifactId> <version>3.0.0</version> </dependency> swagger 配置类 @Configuration @EnableSwagger2 public cla

huwei0518的专栏 1万+

springboot2.6.7使用swagger2出现错误

Caused by: java.lang.NullPointerException: null at springfox.documentation.spi.service.contexts.Orderings$8.compare(Orderings.java:113) ~[springfox-spi-2.7.0.jar:2.7.0] at springfox.documentation.spi.service.contexts.Orderings$8.compare(Orderings.java:11

weixin_43308917的博客 2222

Failed to start bean documentationPluginsBootstrapper

Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException: Cannot invoke "org.springframework.web.servlet.mvc.condition.PatternsRequestCondition.toString()" because the return value of "springfox.documenta.

a250152的专栏 1529

Unable to scan documentation context default: null at springfox.documentation.schema.Example.equals

通过堆栈跟踪可以发现是Maven: io.springfox:springfox-core:3.0.0中的问题。

程序员养成计划 3122

项目能正常启动,但是启动过程中swagger报空指针异常(java.lang.NullPointerException)

问题描述 项目能正常启动,但是启动过程中swagger报空指针异常,swagger文档无法打开 解决方案 一、实体修饰符错误 情况:实体A和实体B相互引用(见下方代码说明) 解决方案:检查相互引用的地方,是否修饰符误写成了public 代码说明:@ApiModel("A") class A{ @ApiModelProperty("B") @Transient private B b;//如果为public则会出现上述情况 } /**********************/ @

MINGHUE 4223

对 request.getParameter(参数名字) 获取的值判空报空指针异常

  对 ? request.getParameter("参数名字") 获取的值判空报空指针异常   比如   String ?str=request.getParameter("参数名字");   str.equal("");   运行报错,空指针异常   因为此时   request....

计算机毕业论文源码,学生个人网页制作html源码。贴近用户做网络推广和互联网优化。 1017

at springfox.documentation.schema.Example.equals(Example.java:131)报错解决

swagger报空指针

litang140513的博客 1175

java.util.list 报错_【spring mvc】后台spring mvc接收List参数报错如下:org.springframework.beans.BeanInstantiationEx...

org.springframework.beans.BeanInstantiationException: Failed to instantiate [java.util.List]: Specified class is an interfaceat org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:99)a...

weixin_39918588的博客 705

RT-Thread GD32F4xx HWTIMER驱动

GD32F4xx RT-Thread 硬件定时器驱动调试使用记录

madao1234的博客 1466

mlx90614_stm32MLX90614_stm32测温_MLX90614_mlx90614驱动_MLX90614stm32

stm32驱动mlx90614红外测温,串口一输出

上一篇: Auto DevOps之gitlab CI/CD
下一篇: 单元测试之浅析Mockito mock Kotlin Object类方法
穹柏
博客等级 码龄9年 11粉丝 36原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值