项目中使用到了swagger做文档,对于一些枚举值都是手动写的,比较死板。于是对swagger进行改造,更加友好的显示枚举
改动方向 首先改动目标在两个地方:
swagger原生显示效果 swagger的@ApiModelProperty 本身支持枚举,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 String allowableValues () default "" ;
但是当我给一个Integer类型加上这个属性时,如下
1 2 @ApiModelProperty(value = "测试", allowableValues = "1执行,2测试,3问题,4但是") private Integer demo;
web界面上并没有出现枚举值,只有去掉非数字字符才会显示枚举值
很明显,这种效果没多大意义,光有数值没有用
自定义显示效果 基本思路 swagger中有个非常重要的类——org.springframework.plugin.core.Plugin,在这里接口下扩展出了若干种处理器
这些处理器总体通过责任链模式调用,在此只需要关注两个类
springfox.documentation.spi.schema.ModelPropertyBuilderPlugin负责解析 Model 类,其两个子类ApiModelPropertyPropertyBuilder和XmlPropertyPlugin分别处理@ApiModelProperty以及@XmlElement、@XmlAttribute
springfox.documentation.spi.service.ExpandedParameterBuilderPlugin负责处理参数上的某一个非嵌套类型;同样两个子类,需要处理的是springfox.documentation.spring.web.readers.parameter.ExpandedParameterBuilder
现在只需要提供两个类,覆盖上述类的逻辑即可。
前期准备 为了更友好的显示枚举,重点在两个方面:一个是需要自定义枚举代表的值,而不是直接使用其ordinal()或者name;其次是要文字说明枚举代表的意义
因此,定义一个接口如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public interface EnumDescription { int getCode () ; String getInfo () ; }
每个Enum需要继承该接口,并重写方法,例如以下例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public enum ContentTypeEnum implements EnumDescription { ADVISORY(1 , "测试1" ), GRAPHIC_LIVE(2 , "测试2" ), ; private final Integer code; private final String info; ContentTypeEnum(Integer code, String info) { this .code = code; this .info = info; } public static ContentTypeEnum valueOf (Integer code) { for (ContentTypeEnum result : ContentTypeEnum.values()) { if (result.code.equals(code)) { return result; } } return null ; } @Override public int getCode () { return code; } @Override public String getInfo () { return info; } }
在后续的逻辑中,类型判断就应该使用EnumDescription而非Enum了
实现 首先是处理Model的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 import com.fasterxml.classmate.ResolvedType;import com.google.common.base.Optional;import com.ruoyi.common.core.enums.EnumDescription;import io.swagger.annotations.ApiModelProperty;import lombok.extern.slf4j.Slf4j;import springfox.documentation.schema.Annotations;import springfox.documentation.service.AllowableListValues;import springfox.documentation.spi.DocumentationType;import springfox.documentation.spi.schema.ModelPropertyBuilderPlugin;import springfox.documentation.spi.schema.contexts.ModelPropertyContext;import springfox.documentation.swagger.schema.ApiModelProperties;import java.util.Arrays;import java.util.List;import java.util.stream.Collectors;@Slf4j public class EnumPropertyDisplayConfig implements ModelPropertyBuilderPlugin { @Override public void apply (ModelPropertyContext context) { Optional<ApiModelProperty> annotation = Optional.absent(); if (context.getAnnotatedElement().isPresent()) { annotation = annotation.or(ApiModelProperties.findApiModePropertyAnnotation(context.getAnnotatedElement().get())); } if (context.getBeanPropertyDefinition().isPresent()) { annotation = annotation.or(Annotations.findPropertyAnnotation( context.getBeanPropertyDefinition().get(), ApiModelProperty.class)); } final Class<?> rawPrimaryType = context.getBeanPropertyDefinition().get().getRawPrimaryType(); if (annotation.isPresent() && EnumDescription.class.isAssignableFrom(rawPrimaryType)) { log.info("des={}" , annotation.get().value()); EnumDescription[] values = (EnumDescription[]) rawPrimaryType.getEnumConstants(); final List<String> displayValues = Arrays.stream(values).map(codedEnum -> codedEnum.getCode() + codedEnum.getInfo()).collect(Collectors.toList()); final AllowableListValues allowableListValues = new AllowableListValues (displayValues, rawPrimaryType.getTypeName()); final ResolvedType resolvedType = context.getResolver().resolve(int .class); context.getBuilder().description(annotation.get().value() + ":" + displayValues).type(resolvedType).allowableValues(allowableListValues); } } @Override public boolean supports (DocumentationType documentationType) { return true ; } }
然后是覆盖参数的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 import com.fasterxml.classmate.ResolvedType;import com.fasterxml.classmate.TypeResolver;import com.google.common.base.Function;import com.google.common.base.Optional;import com.ruoyi.common.core.enums.EnumDescription;import org.springframework.core.annotation.Order;import springfox.documentation.schema.Enums;import springfox.documentation.schema.ModelRef;import springfox.documentation.schema.ModelReference;import springfox.documentation.service.AllowableListValues;import springfox.documentation.service.AllowableValues;import springfox.documentation.spi.DocumentationType;import springfox.documentation.spi.schema.EnumTypeDeterminer;import springfox.documentation.spi.service.contexts.ParameterExpansionContext;import springfox.documentation.spring.web.readers.parameter.ExpandedParameterBuilder;import java.util.Arrays;import java.util.List;import java.util.stream.Collectors;import static com.google.common.base.Strings.isNullOrEmpty;import static com.google.common.collect.Lists.transform;import static springfox.documentation.schema.Collections.*;import static springfox.documentation.schema.Collections.isContainerType;import static springfox.documentation.schema.Types.typeNameFor;import static springfox.documentation.service.Parameter.DEFAULT_PRECEDENCE;import static springfox.documentation.swagger.common.SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER;@Order(SWAGGER_PLUGIN_ORDER + 1000) public class EnumParamBuilderPlugin extends ExpandedParameterBuilder { private final TypeResolver resolver; private final EnumTypeDeterminer enumTypeDeterminer; public EnumParamBuilderPlugin (TypeResolver resolver, EnumTypeDeterminer enumTypeDeterminer) { super (resolver, enumTypeDeterminer); this .resolver = resolver; this .enumTypeDeterminer = enumTypeDeterminer; } @Override public void apply (ParameterExpansionContext context) { AllowableValues allowable = allowableValues(context.getFieldType().getErasedType()); String name = isNullOrEmpty(context.getParentName()) ? context.getFieldName() : String.format("%s.%s" , context.getParentName(), context.getFieldName()); String typeName = context.getDataTypeName(); ModelReference itemModel = null ; ResolvedType resolved = resolver.resolve(context.getFieldType()); if (isContainerType(resolved)) { resolved = fieldType(context).or(resolved); ResolvedType elementType = collectionElementType(resolved); String itemTypeName = typeNameFor(elementType.getErasedType()); AllowableValues itemAllowables = null ; if (enumTypeDeterminer.isEnum(elementType.getErasedType())) { itemAllowables = Enums.allowableValues(elementType.getErasedType()); itemTypeName = "int" ; } typeName = containerType(resolved); itemModel = new ModelRef (itemTypeName, itemAllowables); } else if (enumTypeDeterminer.isEnum(resolved.getErasedType())) { typeName = "int" ; } context.getParameterBuilder() .name(name) .description(null ) .defaultValue(null ) .required(Boolean.FALSE) .allowMultiple(isContainerType(resolved)) .type(resolved) .modelRef(new ModelRef (typeName, itemModel)) .allowableValues(allowable) .parameterType(context.getParameterType()) .order(DEFAULT_PRECEDENCE) .parameterAccess(null ); } private Optional<ResolvedType> fieldType (ParameterExpansionContext context) { return Optional.of(context.getFieldType()); } @Override public boolean supports (DocumentationType delimiter) { return true ; } private AllowableValues allowableValues (Class<?> fieldType) { AllowableListValues allowable = null ; if (enumTypeDeterminer.isEnum(fieldType)) { List<String> enumValues = getEnumValues(fieldType); allowable = new AllowableListValues (enumValues, "LIST" ); } return allowable; } private List<String> getEnumValues (final Class<?> subject) { if (EnumDescription.class.isAssignableFrom(subject)) { EnumDescription[] enumConstants = (EnumDescription[]) subject.getEnumConstants(); return Arrays.stream(enumConstants).map(f -> f.getCode() + f.getInfo()).collect(Collectors.toList()); } return transform(Arrays.asList(subject.getEnumConstants()), (Function<Object, String>) input -> input.toString()); } }
然后将两个类注入
1 2 3 4 5 6 7 8 9 @Bean public EnumPropertyDisplayConfig enumDisplayConfig () { return new EnumPropertyDisplayConfig (); }@Bean public ExpandedParameterBuilder enumParamBuilderPlugin (TypeResolver resolver, EnumTypeDeterminer enumTypeDeterminer) { return new EnumParamBuilderPlugin (resolver, enumTypeDeterminer); }
需要注意的是,swagger默认的处理器在容器中依然存在,只是其执行结果被自定义的处理器覆盖了。
另外,在注入参数处理器时,由于责任链中的处理器顺序问题,可能不会生效,因此需要@Order或者使用Ordered接口指定顺序为最末
效果 最终效果如下:
以后如果有值变动,只需要修改枚举类即可,相关model直接使用Enum,只需要注明参数作用即可
附 同时可以对mybatis typehandler和jackson序列化做一下处理,实现代码中完全使用枚举类。因为前两者默认情况下都是使用的name,不一定符合实际情况
2021-10-28 补充 原本的参数显示效果很好,但是后来又发现了新的问题
请求调试
直接在availableValues中写说明,会影响后面调试请求 这样类型不匹配,请求发不出去
容器
当使用一个容器存储枚举时,当子项类型为int时,前端无法显示字符串的availableValues;
因此,availableValues还是使用int,文字说明改到description里
解决思路有三种,一是继续在原本的EnumParamBuilderPlugin上修改;二是直接覆盖SwaggerExpandedParameterBuilder的逻辑;三是写一个ExpandedParameterBuilderPlugin只处理description部分
这里我选择第三种方案
新版代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 import com.fasterxml.classmate.ResolvedType;import com.fasterxml.classmate.TypeResolver;import com.google.common.base.Function;import com.google.common.base.Optional;import com.ruoyi.common.core.enums.EnumDescription;import io.swagger.annotations.ApiModelProperty;import io.swagger.annotations.ApiParam;import org.springframework.core.annotation.Order;import springfox.documentation.spi.DocumentationType;import springfox.documentation.spi.schema.EnumTypeDeterminer;import springfox.documentation.spi.service.ExpandedParameterBuilderPlugin;import springfox.documentation.spi.service.contexts.ParameterExpansionContext;import springfox.documentation.spring.web.DescriptionResolver;import springfox.documentation.swagger.common.SwaggerPluginSupport;import java.util.Arrays;import java.util.List;import java.util.stream.Collectors;import static com.google.common.collect.Lists.transform;import static springfox.documentation.schema.Collections.*;import static springfox.documentation.swagger.common.SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER;@Order(SWAGGER_PLUGIN_ORDER + 1001) public class EnumDescriptionExpandedParameterBuilder implements ExpandedParameterBuilderPlugin { private final DescriptionResolver descriptions; private final EnumTypeDeterminer enumTypeDeterminer; private final TypeResolver resolver; public EnumDescriptionExpandedParameterBuilder ( DescriptionResolver descriptions, TypeResolver typeResolver, EnumTypeDeterminer enumTypeDeterminer) { this .resolver = typeResolver; this .descriptions = descriptions; this .enumTypeDeterminer = enumTypeDeterminer; } @Override public void apply (ParameterExpansionContext context) { Optional<ApiModelProperty> apiModelPropertyOptional = context.findAnnotation(ApiModelProperty.class); if (apiModelPropertyOptional.isPresent()) { fromApiModelProperty(context, apiModelPropertyOptional.get()); } Optional<ApiParam> apiParamOptional = context.findAnnotation(ApiParam.class); if (apiParamOptional.isPresent()) { fromApiParam(context, apiParamOptional.get()); } } @Override public boolean supports (DocumentationType delimiter) { return SwaggerPluginSupport.pluginDoesApply(delimiter); } private void fromApiParam (ParameterExpansionContext context, ApiParam apiParam) { context.getParameterBuilder() .description(description(context, apiParam.value())); } private void fromApiModelProperty (ParameterExpansionContext context, ApiModelProperty apiModelProperty) { context.getParameterBuilder() .description(description(context, apiModelProperty.value())); } private String description (ParameterExpansionContext context, String value) { value = descriptions.resolve(value); ResolvedType resolved = this .resolver.resolve(context.getFieldType()); if (isContainerType(resolved)) { resolved = fieldType(context).or(resolved); ResolvedType elementType = collectionElementType(resolved); if (enumTypeDeterminer.isEnum(elementType.getErasedType())) { return value + ":" + enumValues(elementType.getErasedType()); } } else if (enumTypeDeterminer.isEnum(resolved.getErasedType())) { return value + ":" + enumValues(resolved.getErasedType()); } return value; } private Optional<ResolvedType> fieldType (ParameterExpansionContext context) { return Optional.of(context.getFieldType()); } private List<String> enumValues (final Class<?> subject) { if (EnumDescription.class.isAssignableFrom(subject)) { EnumDescription[] enumConstants = (EnumDescription[]) subject.getEnumConstants(); return Arrays.stream(enumConstants).map(f -> f.getCode() + f.getInfo()).collect(Collectors.toList()); } return transform(Arrays.asList(subject.getEnumConstants()), (Function<Object, String>) Object::toString); } }