Skip to main content

springdoc-v2中在不配置@ParameterObject的情况下实现Pageable以及Sort对象到API参数的转换

· 5 min read
orange
programmer on jvm platform

springdoc是一个可以快速生成API文档的第三方公共库, 并提供了UI页面以供访问.
同时它也提供了spring-webmvc中的handler中的参数对象到API参数的转换.
对于spring-data-commons中的PageableSort, springdoc提供了开箱即用的功能, 需要在配置文件中启用以及在参数中声明@ParameterObject

application.yml
springdoc:
model-converters:
pageable-converter:
enabled: true

对于已经存在的项目, 当刚引入springdoc时, 需要配置大量的@ParameterObject注解以实现参数转换功能.
这个过程通常比较繁琐, 因为对于一个具有一定规模的项目而言, 其对外提供的API往往会非常多, 需要进行大量的修改功能才能实现该功能
那么此时我们需要一种解决方案, 即如何在不配置@ParameterObject的情况下实现Pageable以及Sort对象到API参数的转换

思路

@ParameterObject

springdoc中, @ParameterObject注解的获取是来自于spring-webmvc中的HandlerMethod对象

HandlerMethod

这个对象是我们在Controller中声明的Handler的抽象, 它主要保存了我们声明的Handler的一些元信息.
它的MethodParameter[] parameters属性包含了该方法的所有参数的信息.

MethodParameter

MethodParameter对象的Annotation[] parameterAnnotations属性包含了该参数的所有注解.
我们可以尝试修改Annotation[] parameterAnnotations属性以实现动态添加@ParameterObject注解的功能.

HandlerMapping

我们不能直接从spring容器中获取到MethodHandler, 因为其是保存在HandlerMapping中的, HandlerMapping 可以从容器中获取到.

BeanPostProcessor

spring中提供了BeanPostProcess机制, 主要实现了对Bean的创建进行拦截处理.
我们可以实现BeanPostProcessor来对HandlerMapping中的MethodHandler中的MethodParameterparameterAnnotations 进行修改以实现追加@ParameterObject注解

解决方案

新增AppendSpringdocAnnotationBeanPostProcessor用于实现增加@ParameterObject注解的能力

AppendSpringdocAnnotationBeanPostProcessor.kt
/**
* We must add @ParameterObject annotation to Pageable and Sort parameter to ensure that springdoc can generate correct.
* But declaring @ParameterObject in the controller method is not a good idea, because it will take some time to.
* So we can use this class to avoid declaring @ParameterObject on method parameter which type is Pageable or Sort.
*
* @author Xiangcheng.Kuo
* @see org.springdoc.core.annotations.ParameterObject
* @see <a href="https://springdoc.org/v2/#how-can-i-map-pageable-spring-data-commons-object-to-correct-url-parameter-in-swagger-ui">13.22. How can I map Pageable (spring-data-commons) object to correct URL-Parameter in Swagger UI?</a>
*/
internal class AppendSpringdocAnnotationBeanPostProcessor : BeanPostProcessor {

override fun postProcessAfterInitialization(bean: Any, beanName: String): Any {
if (bean !is AbstractHandlerMethodMapping<*>) {
return bean
}
bean.handlerMethods?.forEach { (_: Any, handlerMethod: HandlerMethod) ->
handlerMethod
.methodParameters
.filter {
Pageable::class.java.isAssignableFrom(it.parameterType) || Sort::class.java.isAssignableFrom(it.parameterType)
}.forEach { methodParameter: MethodParameter ->
addAnnotationForParameter(methodParameter)
}
}
return bean
}

private fun addAnnotationForParameter(methodParameter: MethodParameter) {
val parameterAnnotationsField =
FieldUtils.getDeclaredField(MethodParameter::class.java, "parameterAnnotations", true)
val annotations: MutableList<Annotation> =
((parameterAnnotationsField[methodParameter] as Array<Annotation>?)
?: emptyArray<Annotation>()).toMutableList()

if (annotations.stream().anyMatch(ParameterObject::class::isInstance)) {
return
}

annotations.add(FakeParameterObject.create())
parameterAnnotationsField[methodParameter] = annotations.toTypedArray()
}

}

新增@ParameterObject的实现类, 由于kotlin不支持继承annotation, 需要新建java类来继承annotation

FakeParameterObject.java
/**
* Avoid the following kotlin compile error
* This type has a constructor, and thus must be initialized here
* This type is final, so it cannot be inherited from
*
* @author Xiangcheng.Kuo
* @see <a href="https://stackoverflow.com/questions/51608924/implement-inherit-extend-annotation-in-kotlin">Implement (/inherit/~extend) annotation in Kotlin</a>
*/
public class FakeParameterObject implements ParameterObject {

@Override
public Class<? extends Annotation> annotationType() {
return FakeParameterObject.class;
}

public static ParameterObject create() {
return new FakeParameterObject();
}

}

新增Configuration, 将AppendSpringdocAnnotationBeanPostProcessor添加到容器中

ApidocAutoConfiguration.kt
/**
* @author Xiangcheng.Kuo
*/
@Configuration
internal class ApidocAutoConfiguration {

@Bean
@ConditionalOnProperty(Constants.SPRINGDOC_ENABLED, matchIfMissing = true)
fun appendSpringdocAnnotationBeanPostProcessor(): BeanPostProcessor =
AppendSpringdocAnnotationBeanPostProcessor()

}

备注

反射

该解决方法是基于反射的, 并且在springBeanPostProcessor中修改了MethodParameterparameterAnnotations属性. 该属性可能会在spring的其他地方被使用, 因此可能会引起一些不可预知的问题.

springdoc应该在生产环境中关闭

在生产环境中, 不应该开启springdoc, 因为这会暴露swaggerapi文档, 从而导致api文档泄露,

使用spring中提供的profile功能以实现在开发模式下开启springdoc, 生产模式下关闭springdoc

application.yml
springdoc:
api-docs:
enabled: false
model-converters:
pageable-converter:
enabled: true
application-dev.yml
springdoc:
api-docs:
enabled: true
model-converters:
pageable-converter:
enabled: true

参考

springdoc-v2

spring

kotlin