swagger优雅显示枚举

项目中使用到了swagger做文档,对于一些枚举值都是手动写的,比较死板。于是对swagger进行改造,更加友好的显示枚举

改动方向

首先改动目标在两个地方:

  • 参数里的枚举
  • 返回model中的枚举

swagger原生显示效果

swagger的@ApiModelProperty本身支持枚举,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Limits the acceptable values for this parameter.
* <p>
* There are three ways to describe the allowable values:
* <ol>
* <li>To set a list of values, provide a comma-separated list.
* For example: {@code first, second, third}.</li>
* <li>To set a range of values, start the value with "range", and surrounding by square
* brackets include the minimum and maximum values, or round brackets for exclusive minimum and maximum values.
* For example: {@code range[1, 5]}, {@code range(1, 5)}, {@code range[1, 5)}.</li>
* <li>To set a minimum/maximum value, use the same format for range but use "infinity"
* or "-infinity" as the second value. For example, {@code range[1, infinity]} means the
* minimum allowable value of this parameter is 1.</li>
* </ol>
*/
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 类,其两个子类ApiModelPropertyPropertyBuilderXmlPropertyPlugin分别处理@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 {
/**
* 枚举值可能并非使用序号,而是自定义code
*
* @return 实际使用的code值
*/
int getCode();

/**
* 说明描述
*
* @return 描述文本
*/
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());
//获取CodedEnum的code值
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());
//固定设置为int类型
final ResolvedType resolvedType = context.getResolver().resolve(int.class);
context.getBuilder().description(annotation.get().value() + ":" + displayValues).type(resolvedType).allowableValues(allowableListValues);
// context.getBuilder().allowableValues(allowableListValues).type(resolvedType);
}
}

@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);

}
}

swagger优雅显示枚举
http://blog.inkroom.cn/2021/10/18/H1ZH22.html
作者
inkbox
发布于
2021年10月18日
许可协议