Swagger2 关于Map参数在API文档中展示详细参数以及参数说明

时间:2024-10-01 16:18:09

前言

本文主要解决的问题是 Swagger2 (SpringFox)关于Map参数生成的API文档中没有详细Json结构说明,问题如下图所示:

此种方式生成的Api文档中的请求参数如下:

如果是这样的参数类型的会让查看API的人员无法清晰的知道如何请求API文档。当然Swagger2 根据这种情况也给出了解决方案:

  1. @ApiOperation(value = "not use")
  2. @ApiImplicitParam(name = "params" , paramType = "body",examples = @Example({
  3. @ExampleProperty(value = "{'user':'id'}", mediaType = "application/json")
  4. }))
  5. @PostMapping("/xxx")
  6. public void test(Map<String,String> params){}

但是这种写法在SpringFox版本2.8.0至2.9.0之间好像没有实现@ApiImplicitParam的examples的用法,还是属于issue的状态,下面是关于这两个issue的说明:

/springfox/docs/current/#changing-how-generic-types-are-named

/questions/41861164/how-can-i-manually-describe-an-example-input-for-a-java-requestbody-mapstring

解决方法

SpringFox 提供给我们了一个ParameterBuilderPlugin接口,通过这个接口我们可以在SpringFox构造Map参数映射的ModelRef时使用javassist动态的生成类,并把这个map参数的modelRef对象指向我们动态生成的具体Class对象(通过自定义注解在Map参数上生成可表示JSON结构的类),具体实现如下(求方便的同学可以把下面3个类直接Copy到自己的代码中即可):

  1. package ;
  2. import ;
  3. import ;
  4. import ;
  5. import ;
  6. import javassist.*;
  7. import ;
  8. import ;
  9. import ;
  10. import ;
  11. import ;
  12. import ;
  13. import ;
  14. import ;
  15. import ;
  16. import ;
  17. import ;
  18. import ;
  19. import ;
  20. import ;
  21. @Component
  22. @Order //plugin加载顺序,默认是最后加载
  23. public class MapApiReader implements ParameterBuilderPlugin {
  24. @Autowired
  25. private TypeResolver typeResolver;
  26. @Override
  27. public void apply(ParameterContext parameterContext) {
  28. ResolvedMethodParameter methodParameter = ();
  29. if (().canCreateSubtype() || ().canCreateSubtype()) { //判断是否需要修改对象ModelRef,这里我判断的是Map类型和String类型需要重新修改ModelRef对象
  30. Optional<ApiJsonObject> optional = (); //根据参数上的ApiJsonObject注解中的参数动态生成Class
  31. if (()) {
  32. String name = ().name(); //model 名称
  33. ApiJsonProperty[] properties = ().value();
  34. ().getAdditionalModels().add((createRefModel(properties, name))); //像documentContext的Models中添加我们新生成的Class
  35. () //修改Map参数的ModelRef为我们动态生成的class
  36. .parameterType("body")
  37. .modelRef(new ModelRef(name))
  38. .name(name);
  39. }
  40. }
  41. }
  42. private final static String basePackage = "."; //动态生成的Class名
  43. /**
  44. * 根据propertys中的值动态生成含有Swagger注解的javaBeen
  45. */
  46. private Class createRefModel(ApiJsonProperty[] propertys, String name) {
  47. ClassPool pool = ();
  48. CtClass ctClass = (basePackage + name);
  49. try {
  50. for (ApiJsonProperty property : propertys) {
  51. (createField(property, ctClass));
  52. }
  53. return ();
  54. } catch (Exception e) {
  55. ();
  56. return null;
  57. }
  58. }
  59. /**
  60. * 根据property的值生成含有swagger apiModelProperty注解的属性
  61. */
  62. private CtField createField(ApiJsonProperty property, CtClass ctClass) throws NotFoundException, CannotCompileException {
  63. CtField ctField = new CtField(getFieldType(()), (), ctClass);
  64. ();
  65. ConstPool constPool = ().getConstPool();
  66. AnnotationsAttribute attr = new AnnotationsAttribute(constPool, );
  67. Annotation ann = new Annotation("", constPool);
  68. ("value", new StringMemberValue((), constPool));
  69. if (().subclassOf(().get(())))
  70. ("example", new StringMemberValue((), constPool));
  71. if (().subclassOf(().get(())))
  72. ("example", new IntegerMemberValue((()), constPool));
  73. (ann);
  74. ().addAttribute(attr);
  75. return ctField;
  76. }
  77. private CtClass getFieldType(String type) throws NotFoundException {
  78. CtClass fileType = null;
  79. switch (type) {
  80. case "string":
  81. fileType = ().get(());
  82. break;
  83. case "int":
  84. fileType = ().get(());
  85. break;
  86. }
  87. return fileType;
  88. }
  89. @Override
  90. public boolean supports(DocumentationType delimiter) {
  91. return true;
  92. }
  93. }

这里是ApiJsonObject注解和ApiJsonProperty注解的实现:

  1. package ;
  2. import ;
  3. import ;
  4. import ;
  5. import ;
  6. @Target({, , })
  7. @Retention()
  8. public @interface ApiJsonObject {
  9. ApiJsonProperty[] value(); //对象属性值
  10. String name(); //对象名称
  11. }
  12. package ;
  13. import ;
  14. import ;
  15. import ;
  16. import ;
  17. @Target(ElementType.ANNOTATION_TYPE)
  18. @Retention()
  19. public @interface ApiJsonProperty {
  20. String key(); //key
  21. String example() default "";
  22. String type() default "string"; //支持string 和 int
  23. String description() default "";
  24. }

在这里我需要特殊说明一下,我们每一个ApiOperation都是按一个RequestMapping来加载的每一个RequestMapping在加载的时候都会经过许多不同类型的Plugin的处理,而负责管理全局的ModelRef的Plugin是OperationModelsProviderPlugin这个处理RequestMapping时会检测有没有还没有被放到全局的ModelRef对象(而我们放到DocumentContext的对象就是此时被加载的),但是OperationModelsProviderPlugin类型的执行顺序是优先于ParameterBuilderPlugin类型的 ,所以这里就有了一个小问题,如果我们新建的ModelRef是最后一个被处理的RequestMapping那我们新建的ModelRef就没有机会被OperationModelsProviderPlugin放到全局的ModelRef中了,所以解决方法就是在这个Controller中添加一个无用的方法但是这个方法名要足够的长(这个Document范围内即可)保证这个方法才是被SpringFox最后解析的,让我们每个ModelRef都能被OperationModelsProviderPlugin装载进来,如果想看SpringFox这部分具体实现的可以关注下DocumentationPluginsManager这个类,打个断点(断点在OperationModelsProviderPlugin和ParameterBuilderPlugin这两个plugin的调用地方)应该就能理解了:

Ok做完准备工作,来看下我们在controller层如何使用我们新开发的功能:

  1. @ApiOperation(value = "Login", tags = "login")
  2. @PutMapping
  3. public void auth(@ApiJsonObject(name = "login_model", value = {
  4. @ApiJsonProperty(key = "mobile", example = "18614242538", description = "user mobile"),
  5. @ApiJsonProperty(key = "password", example = "123456", description = "user password")
  6. })
  7. @RequestBody Map<String, String> params) {
  8. xxxxxxxxxxxxxx
  9. }
  10. @ApiOperation(value = "none")
  11. @GetMapping
  12. public void authaaaa(){
  13. }

效果图:

总结

我这个解决方法是比较繁琐的,但是也实现了在Api文档中展示Map参数应要接收的详细对象。如果你并没有很多Map参数需要表明结构,建议你新建个Class做ModelRef就可以了,或者新建个ModelRequestVo也是好的。最后如果同学们发现有更好的解决方法请告知,以免误导其他人,谢谢~

补充:这个只是个DEMO并没有经过完善的测试,不建议生产使用,个人建议还是新建个对象来做参数接收,代码可读性也要高些,好维护,也好进行参数校验等。