如何伪造注解防止Swagger和SpringMVC的@Requestbody冲突暨swagger-ui接口入参json显示
起因:目的:解决方案:使用方法:想法实现过程中的出现了这些问题:这篇blog的技术少不了薛凌康的支持与点拨( https://me.csdn.net/qq_35433926 ),谢谢。起因:这篇博客发生在后端SpringBoot+Spring+Mybatis架构、前端VUE的工程集成Swagger(version 2.9.2)后。据我所知,目前前、后端联调...
这篇blog的技术少不了薛凌康的支持与点拨( https://me.csdn.net/qq_35433926 ),谢谢。
起因:
这篇博客发生在后端SpringBoot+Spring+Mybatis架构、前端VUE的工程集成Swagger(version 2.9.2)后。
据我所知,目前前、后端联调需要比较完整的接口文档,Swagger很好的做到这一点,但对数据请求方式提出了难以理解的要求
举例来说,Postman通过form-data、x-www-form-urlencode、raw(application/json)三种方式进行数据请求时的不同表现如下:
当入参为单入参对象 public String findByEntity(@RequestBody Batch entity) {} 时还好,但我想要
像 public String findPage(String pageNo, String pageSize, @RequestBody Batch entity, String isCurrentBatch) {} 时就会出现如上问题(restful接口参数javabean自动映射偷懒而作,表中的正常和非json指下图中的【swagger-ui】结构),又不想再封装什么Model包装对象什么的,于是我在想,如果能做到单入参对象时就好了。
目的:
目的就是解放Swagger和SpringMVC中对@RequestBody的死锁绑定,让Swagger接口文档在使用form-data时也有直观的json视图,方便前端核对参数的类型、结构、注释等。
解决方案:
1、首先要对swagger的基础知识有个了解才能开始搞这个哦,这里丢两个链接做下记录
(https://www.jianshu.com/p/349e130e40d5 、 https://blog.csdn.net/qq_25615395/article/details/70229139)
2、通过eclipse的Java Search搜索包含“requestbody”的文件,发现springfox包下的OperationModelsProvider、OperationParameterReader两个类中包含了对@RequestBody的逻辑处理,重写这两个类,并放置在@ComponentScan====扫描包外====,通过@Import({OperationModelsProviderSub.class, OperationParameterReaderSub.class})在启动类引入。
主要修改了:加入自定义注解@SwaggerFormData,重写两个构造方法、collectParameters()、shouldExpand()。如下:
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface SwaggerFormData {
}
@Import({OperationModelsProviderSub.class, OperationParameterReaderSub.class})
public class Application {}
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
@DependsOn({"springContextHolder"}) // 获取spring上下文,彻底剔除掉其中的原始文件bean
public class OperationModelsProviderSub extends OperationModelsProvider {
private static final Logger LOG = LoggerFactory.getLogger(OperationModelsProviderSub.class);
private final TypeResolver typeResolver;
@Autowired
public OperationModelsProviderSub(
TypeResolver typeResolver) {
super(typeResolver);
this.typeResolver = typeResolver;
/*
* 删除父类bean定义
* DocumentationPluginsManager中使用OperationModelsProviderPlugin类型获取bean
* 所以@Primary注解并不能覆盖父类
*/
DefaultListableBeanFactory defaultListableBeanFactory =
(DefaultListableBeanFactory) SpringContextHolder.getApplicationContext()
.getAutowireCapableBeanFactory();
defaultListableBeanFactory.removeBeanDefinition("operationModelsProvider");
}
//省略!其他方法需要从父类拷贝
private void collectParameters(RequestMappingContext context) {
LOG.debug("Reading parameters models for handlerMethod |{}|", context.getName());
List<ResolvedMethodParameter> parameterTypes = context.getParameters();
for (ResolvedMethodParameter parameterType : parameterTypes) {
if (parameterType.hasParameterAnnotation(RequestBody.class)
|| parameterType.hasParameterAnnotation(SwaggerFormData.class) // 加上自定义的注解
|| parameterType.hasParameterAnnotation(RequestPart.class)) {
ResolvedType modelType = context.alternateFor(parameterType.getParameterType());
LOG.debug("Adding input parameter of type {}", resolvedTypeSignature(modelType).or("<null>"));
context.operationModelsBuilder().addInputParam(modelType);
}
}
LOG.debug("Finished reading parameters models for handlerMethod |{}|", context.getName());
}
}
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
@DependsOn("springContextHolder") // 获取spring上下文,彻底剔除掉其中的原始文件bean
public class OperationParameterReaderSub extends OperationParameterReader {
private final ModelAttributeParameterExpander expander;
private final EnumTypeDeterminer enumTypeDeterminer;
@Autowired
private DocumentationPluginsManager pluginsManager;
@Autowired
public OperationParameterReaderSub(
ModelAttributeParameterExpander expander,
EnumTypeDeterminer enumTypeDeterminer) {
super(expander, enumTypeDeterminer);
this.expander = expander;
this.enumTypeDeterminer = enumTypeDeterminer;
/*
* 删除父类bean定义
* DocumentationPluginsManager中使用ParameterBuilderPlugin类型获取bean
* 所以@Primary注解并不能覆盖父类
*/
DefaultListableBeanFactory defaultListableBeanFactory =
(DefaultListableBeanFactory) SpringContextHolder.getApplicationContext()
.getAutowireCapableBeanFactory();
defaultListableBeanFactory.removeBeanDefinition("operationParameterReader");
}
//省略!需从父类拷贝其他方法过来
private boolean shouldExpand(final ResolvedMethodParameter parameter, ResolvedType resolvedParamType) {
// 加上自定义的注解,防止swagger与SpringMVC中的@RequestBody冲突
return !parameter.hasParameterAnnotation(RequestBody.class)
&& !parameter.hasParameterAnnotation(SwaggerFormData.class)
&& !parameter.hasParameterAnnotation(RequestPart.class)
&& !parameter.hasParameterAnnotation(RequestParam.class)
&& !parameter.hasParameterAnnotation(PathVariable.class)
&& !isBaseType(typeNameFor(resolvedParamType.getErasedType()))
&& !enumTypeDeterminer.isEnum(resolvedParamType.getErasedType())
&& !isContainerType(resolvedParamType)
&& !isMapType(resolvedParamType);
}
}
3、就这些咯,
使用方法:
1、Controller用@Api(tags = { "批次操作接口" } )标注;
2、入参对象自定义注解在参数内用@ApiParam标注,其他方法照常用@ApiImplicitParams参数组标注
3、入参对象的实体对象类可用原生:
@ApiModel(value = "Batch", description = "**实体类", parent = Base**.class)
4、入参对象的实体对象类de属性可用原生:
@ApiModelProperty(value="column_description")
@Controller
@RequestMapping("batch")
@Slf4j
@Api(tags = { "Batch操作接口" } )
public class BatchController {
@ApiOperation(value = "Batch", notes = "batch的findpage方法")
@ApiImplicitParams({
@ApiImplicitParam(name = "pageSize", value = "每页条数", paramType = "query", required = false),
@ApiImplicitParam(name = "pageNo", value = "当前页", paramType = "query", required = false)
})
@PostMapping("findPage")
@ResponseBody
public String findPage(String pageNo, String pageSize,
@ApiParam(name = "Batch", value= "Batch对象") @SwaggerFormData Batch entity) {
}
}
}
嗯,就是这个样子
想法实现过程中的出现了这些问题:
当我有这样一个想法的时候,我充分考虑了它的可行性,最初还以为很简单,结果花了不少时间,所以Mark以下几点
1、查询项目工程下(包括jar包)是否存在待查询的字符串时,使用Java_Search搜索,否则会疏漏部分区域。
2、这个地方的自定义注解声明时要放置在Swagger2Config配置里扫描到的地方。(有待考究)
根据swagger的运行原理可知,它是在项目启动时根据配置的包扫描所有的@Api修饰类,并且扫描其中的接口方法,当扫描到自定义注解时,可能这个注解还没有被加载,从而导致试验出错。
3、两个改造类和原始类bean都生效了,swagger-ui共存了json和非json(入参对象属性及其子对象属性全部铺开)
我们发现加了两个改造类,并且以@Primary指定了优先级后,页面共存了两个显示方式,经打印所有的bean到控制台,发现原始类bean还存在,这应该就是两者共存的原因了,@Primary并不管用,所以先解决再说:
swagger中的DocumentationPluginsManager在加载时会自动扫描所有的bean,所以我们需要通过加载两个改造类时remove两个原始注解,就如上面代码中的两个remove,之所以通过AutowireCapableBeanFactory移除,是因为它在BeanFactory基础上实现了对存在实例的管理。可以拿到并不由Spring管理生命周期并已存在的实例。
4、上下文空指针和两个改造类的加载顺序问题。
在我们处理上下文中原始bean的时候,遇到了获取上下文空指针的问题,一想到Spring,就想到不在同一个上下文,所以通过自定义工具SpringContextHolder获取上下文,并且需要在类上添加@DependsOn({springContextHolder}),空指针解除。
5、启动过程中application failed to start。
在启动项目过程中,抛出===application failed to start===:a component required a bean named 'operationModelsProvider' that could not be found,检查几次后发现,极有可能是第二次remove原始注解的时候为空,所以报错,而第二次扫描后面分析为在启动类application.java的@ComponentScan下包含了两个改造类,所以将他们俩移出@ComponentScan路径,并使用@Import引入。OK了,PS:application.java所在工程下的包可以不用添加进@ComponentScan路径。
更多推荐
所有评论(0)