springdoc-v2中在不配置@ParameterObject的情况下实现Pageable以及Sort对象到API参数的转换
springdoc
是一个可以快速生成API
文档的第三方公共库, 并提供了UI
页面以供访问.
同时它也提供了spring-webmvc
中的handler
中的参数对象到API
参数的转换.
对于spring-data-commons
中的Pageable
和Sort
, springdoc
提供了开箱即用的功能,
需要在配置文件中启用以及在参数中声明@ParameterObject
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
中的MethodParameter
的parameterAnnotations
进行修改以实现追加@ParameterObject
注解
解决方案
新增AppendSpringdocAnnotationBeanPostProcessor
用于实现增加@ParameterObject
注解的能力
/**
* 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
/**
* 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
添加到容器中
/**
* @author Xiangcheng.Kuo
*/
@Configuration
internal class ApidocAutoConfiguration {
@Bean
@ConditionalOnProperty(Constants.SPRINGDOC_ENABLED, matchIfMissing = true)
fun appendSpringdocAnnotationBeanPostProcessor(): BeanPostProcessor =
AppendSpringdocAnnotationBeanPostProcessor()
}
备注
反射
该解决方法是基于反射的, 并且在spring
的BeanPostProcessor
中修改了MethodParameter
的parameterAnnotations
属性.
该属性可能会在spring
的其他地方被使用, 因此可能会引起一些不可预知的问题.
springdoc
应该在生产环境中关闭
在生产环境中, 不应该开启springdoc
, 因为这会暴露swagger
的api
文档, 从而导致api
文档泄露,
使用spring
中提供的profile
功能以实现在开发模式下开启springdoc
, 生产模式下关闭springdoc
springdoc:
api-docs:
enabled: false
model-converters:
pageable-converter:
enabled: true
springdoc:
api-docs:
enabled: true
model-converters:
pageable-converter:
enabled: true
参考
springdoc-v2
- springdoc v2
- 13.22. How can I map Pageable (spring-data-commons) object to correct URL-Parameter in Swagger UI?
- 13.61. How can I extract fields from parameter object ?