Swagger2关于Map参数在API⽂档中展⽰详细参数以及参数说
明
前⾔
本⽂主要解决的问题是 Swagger2 (SpringFox)关于Map参数⽣成的API⽂档中没有详细Json结构说明,问题如下图所⽰:
此种⽅式⽣成的Api⽂档中的请求参数如下:
如果是这样的参数类型的会让查看API的⼈员⽆法清晰的知道如何请求API⽂档。当然Swagger2 根据这种情况也给出了解决⽅案:
@ApiOperation(value = "not use")
@ApiImplicitParam(name = "params" , paramType = "body",examples = @Example({
@ExampleProperty(value = "{'user':'id'}", mediaType = "application/json")
}))
@PostMapping("/xxx")
public void test(Map<String,String> params){}
但是这种写法在SpringFox版本2.8.0⾄2.9.0之间好像没有实现@ApiImplicitParam的examples的⽤法,还是属于issue的状态,下⾯是关于这两个issue的说明:
springfox.github.io/springfox/docs/current/#changing-how-generic-types-are-named
stackoverflow/questions/41861164/how-can-i-manually-describe-an-example-input-for-a-j
ava-requestbody-mapstring
解决⽅法
SpringFox 提供给我们了⼀个ParameterBuilderPlugin接⼝,通过这个接⼝我们可以在SpringFox构造Map参数映射的ModelRef时使⽤javassist动态的⽣成类,并把这个map参数的modelRef对象指向我们动态⽣成的具体Class对象(通过⾃定义注解在Map参数上⽣成可表⽰JSON结构的类),具体实现如下(求⽅便的同学可以把下⾯3个类直接Copy到⾃⼰的代码中即可):
ller.agent;
import com.fasterxml.classmate.TypeResolver;
lemon.base.Optional;
ller.agent.annotation.ApiJsonObject;
ller.agent.annotation.ApiJsonProperty;
import javassist.*;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.ConstPool;
import javassist.bytecode.annotation.Annotation;
import javassist.bytecode.annotation.IntegerMemberValue;
import javassist.bytecode.annotation.IntegerMemberValue;
import javassist.bytecode.annotation.StringMemberValue;
import org.springframework.beans.factory.annotation.Autowired;
import annotation.Order;
import org.springframework.stereotype.Component;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ResolvedMethodParameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.ParameterBuilderPlugin;
import springfox.documentation.ts.ParameterContext;
import java.util.Map;
@Component
@Order //plugin加载顺序,默认是最后加载
public class MapApiReader implements ParameterBuilderPlugin {
@Autowired
private TypeResolver typeResolver;
@Override
public void apply(ParameterContext parameterContext) {
ResolvedMethodParameter methodParameter = solvedMethodParameter();
if (ParameterType().canCreateSubtype(Map.class) || ParameterType().canCreateSubtype(String.class)) { //判断是 Optional<ApiJsonObject> optional = methodParameter.findAnnotation(ApiJsonObject.class); //根据参数上的ApiJsonObject注解中的参数动态⽣成Class
if (optional.isPresent()) {
String name = ().name(); //model 名称
ApiJsonProperty[] properties = ().value();
parameterContext.parameterBuilder() //修改Map参数的ModelRef为我们动态⽣成的class
.parameterType("body")
.modelRef(new ModelRef(name))
.name(name);
}
}
}
private final static String basePackage = "del."; //动态⽣成的Class名
/**
* 根据propertys中的值动态⽣成含有Swagger注解的javaBeen
*/
private Class createRefModel(ApiJsonProperty[] propertys, String name) {
ClassPool pool = Default();
CtClass ctClass = pool.makeClass(basePackage + name);
try {
for (ApiJsonProperty property : propertys) {
ctClass.addField(createField(property, ctClass));
}
Class();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据property的值⽣成含有swagger apiModelProperty注解的属性
*/
private CtField createField(ApiJsonProperty property, CtClass ctClass) throws NotFoundException, CannotCompileException {
CtField ctField = new CtField(pe()), property.key(), ctClass);
ctField.setModifiers(Modifier.PUBLIC);
ctField.setModifiers(Modifier.PUBLIC);
ConstPool constPool = ClassFile().getConstPool();
AnnotationsAttribute attr = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
Annotation ann = new Annotation("io.swagger.annotations.ApiModelProperty", constPool);
ann.addMemberValue("value", new StringMemberValue(property.description(), constPool));
if (Type().Default().get(Name())))
ann.addMemberValue("example", new ample(), constPool));
if (Type().Default().get(Name())))
ann.addMemberValue("example", new IntegerMemberValue(Integer.ample()), constPool));
attr.addAnnotation(ann);
return ctField;
}
private CtClass getFieldType(String type) throws NotFoundException {
CtClass fileType = null;
switch (type) {
case "string":
fileType = Default().get(Name());
break;
case "int":
fileType = Default().get(Name());
break;
}
return fileType;
}
@Override
public boolean supports(DocumentationType delimiter) {
return true;
}
}
这⾥是ApiJsonObject注解和ApiJsonProperty注解的实现:
ller.agent.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiJsonObject {
ApiJsonProperty[] value(); //对象属性值
String name(); //对象名称
}
ller.agent.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiJsonProperty {
String key(); //key
String example() default "";
String type() default "string"; //⽀持string 和 int
String description() default "";
}
在这⾥我需要特殊说明⼀下,我们每⼀个ApiOperation都是按⼀个RequestMapping来加载的每⼀个RequestMapping在加载的时候都会经过许多不同类型的Plugin的处理,⽽负责管理全局的ModelRef的Plugin是OperationModelsProviderPlugin这个处理RequestMapping时会检测有没有还没有被放到全局的ModelRef对象(⽽我们放到DocumentContext的对象就是此时被加载的),但
是OperationModelsProviderPlugin类型的执⾏顺序是优先于ParameterBuilderPlugin类型的 ,所以这⾥就有了⼀个⼩问题,如果我们新建的ModelRef是最后⼀个被处理的RequestMapping那我们新建的ModelRef就没有机会被OperationModelsProviderPlugin放到全局的ModelRef中了,所以解决⽅法就是在这个Controller中添加⼀个⽆⽤的⽅法但是这个⽅法名要⾜够的长(这个Document范围内即可)保证这个⽅法才是被SpringFox最后解析的,让我们每个ModelRef都能被OperationModelsProviderPlu
gin装载进来,如果想看SpringFox这部分具体实现的可以关注下DocumentationPluginsManager这个类,打个断点(断点在OperationModelsProviderPlugin 和ParameterBuilderPlugin这两个plugin的调⽤地⽅)应该就能理解了:
Ok做完准备⼯作,来看下我们在controller层如何使⽤我们新开发的功能:
@ApiOperation(value = "Login", tags = "login")
@PutMapping
public void auth(@ApiJsonObject(name = "login_model", value = {
@ApiJsonProperty(key = "mobile", example = "186********", description = "user mobile"),
@ApiJsonProperty(key = "password", example = "123456", description = "user password")
})
@RequestBody Map<String, String> params) {
xxxxxxxxxxxxxx
}
@ApiOperation(value = "none")
@GetMapping
public void authaaaa(){
}
效果图:
总结param name
我这个解决⽅法是⽐较繁琐的,但是也实现了在Api⽂档中展⽰Map参数应要接收的详细对象。如果你
并没有很多Map参数需要表明结构,建议你新建个Class做ModelRef就可以了,或者新建个ModelRequestVo也是好的。最后如果同学们发现有更好的解决⽅法请告知,以免误导其他⼈,谢谢~
补充:这个只是个DEMO并没有经过完善的测试,不建议⽣产使⽤,个⼈建议还是新建个对象来做参数接收,代码可读性也要⾼些,好维护,也好进⾏参数校验等。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论