起因:

目的:

解决方案:

使用方法:

想法实现过程中的出现了这些问题:


这篇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路径。

 

 

 

 

 

 

 

 

Logo

前往低代码交流专区

更多推荐