先不做任何处理看一下直接使用枚举当做入参是什么效果。
package com.example.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 来源枚举
*
* @author vains
*/
@Getter
@AllArgsConstructor
public enum SourceEnum {
/**
* 1-web网站
*/
WEB(1, "web网站"),
/**
* 2-APP应用
*/
APP(2, "APP应用");
/**
* 来源代码
*/
private final Integer value;
/**
* 来源名称
*/
private final String source;
}
package com.example.model;
import com.example.enums.SourceEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 枚举属性类
*
* @author vains
*/
@Data
@Schema(title = "包含枚举属性的类")
public class EnumModel {
@Schema(title = "名字")
private String name;
@Schema(title = "来源")
private SourceEnum source;
}
package com.example.controller;
import com.example.enums.SourceEnum;
import com.example.model.EnumModel;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 枚举接口
*
* @author vains
*/
@RestController
@RequestMapping("/enum")
@Tag(name = "枚举入参接口", description = "提供以枚举作为入参的接口,展示SpringDoc自定义配置效果")
public class EnumController {
@GetMapping("/test01/{source}")
@Operation(summary = "url参数枚举", description = "将枚举当做url参数")
public SourceEnum test01(@PathVariable SourceEnum source) {
return source;
}
@GetMapping("/test02")
@Operation(summary = "查询参数枚举", description = "将枚举当做查询参数")
public SourceEnum test02(SourceEnum source) {
return source;
}
@PostMapping(value = "/test03")
@Operation(summary = "参数类包含枚举", description = "将枚举当做参数类的属性")
public EnumModel test03(@RequestBody EnumModel model) {
return model;
}
}
文档中默认显示枚举可接收的值是定义的枚举名字(APP
,WEB
),但是在实际开发中前端会传入枚举对应的值/代码(1
,2
),根据代码映射到对应的枚举。
详细内容见文档
使用@Parameter
注解(方法上/参数前)或者@Parameters
注解来指定枚举参数可接受的值。如下所示
例1
@GetMapping("/test01/{source}")
@Parameter(name = "source", schema = @Schema(description = "来源枚举", type = "int32", allowableValues = {"1", "2"}))
@Operation(summary = "url参数枚举", description = "将枚举当做url参数")
public SourceEnum test01(@PathVariable SourceEnum source) {
return source;
}
例2
@GetMapping("/test01/{source}")
@Operation(summary = "url参数枚举", description = "将枚举当做url参数")
public SourceEnum test01(@PathVariable
@Parameter(name = "source", schema =
@Schema(description = "来源枚举", type = "int32", allowableValues = {"1", "2"}))
SourceEnum source) {
return source;
}
单独枚举入参显示效果
单独处理没有好的办法,像上边添加allowableValues
属性只会在原有列表上添加,如下
package com.example.enums;
import com.fasterxml.jackson.annotation.JsonValue;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Objects;
/**
* 通用枚举接口
*
* @param 枚举值的类型
* @param 子枚举类型
* @author vains
*/
public interface BasicEnum<V extends Serializable, E extends Enum<E>> {
@JsonValue
V getValue();
/**
* 根据子枚举和子枚举对应的入参值找到对应的枚举类型
*
* @param value 子枚举中对应的值
* @param clazz 子枚举类型
* @param {@link BasicEnum} 的子类类型
* @param 子枚举值的类型
* @param 子枚举的类型
* @return 返回 {@link BasicEnum} 对应的子类实例
*/
static <B extends BasicEnum<V, E>, V extends Serializable, E extends Enum<E>> B fromValue(V value, Class<B> clazz) {
return Arrays.stream(clazz.getEnumConstants())
.filter(e -> Objects.equals(e.getValue(), value))
.findFirst().orElse(null);
}
}
我这里为了通用性将枚举值的类型也设置为泛型类型了,如果不需要可以设置为具体的类型,比如String
、Integer
等,如果像我这样处理起来会稍微麻烦一些;另外我这里只提供了一个getValue
的抽象方法,你也可以再提供一个getName
、getDescription
等获取枚举描述字段值的抽象方法。
BasicEnum
接口并重写getValue
方法,如下package com.example.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 来源枚举
*
* @author vains
*/
@Getter
@AllArgsConstructor
public enum SourceEnum implements BasicEnum<Integer, SourceEnum> {
/**
* 1-web网站
*/
WEB(1, "web网站"),
/**
* 2-APP应用
*/
APP(2, "APP应用");
/**
* 来源代码
*/
private final Integer value;
/**
* 来源名称
*/
private final String source;
}
package com.example.config.basic;
import io.swagger.v3.core.util.PrimitiveType;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.Schema;
import org.springframework.beans.BeanUtils;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 基础自定义接口
*
* @author vains
*/
public interface BasicEnumCustomizer {
/**
* 获取枚举的所有值
*
* @param enumClazz 枚举的class
* @return 枚举的所有值
*/
default List<Object> getValues(Class<?> enumClazz) {
return Arrays.stream(enumClazz.getEnumConstants())
.filter(Objects::nonNull)
.map(item -> {
// 收集values
Method getValue = ReflectionUtils.findMethod(item.getClass(), "getValue");
if (getValue != null) {
ReflectionUtils.makeAccessible(getValue);
return ReflectionUtils.invokeMethod(getValue, item);
}
return null;
}).filter(Objects::nonNull).toList();
}
/**
* 获取值和描述对应的描述信息,值和描述信息以“:”隔开
*
* @param enumClazz 枚举class
* @return 描述信息
*/
default String getDescription(Class<?> enumClazz) {
List<Field> fieldList = Arrays.stream(enumClazz.getDeclaredFields())
.filter(f -> !Modifier.isStatic(f.getModifiers()))
// 排序
.sorted(Comparator.comparing(Field::getName).reversed())
.toList();
fieldList.forEach(ReflectionUtils::makeAccessible);
return Arrays.stream(enumClazz.getEnumConstants())
.filter(Objects::nonNull)
.map(item -> fieldList.stream()
.map(field -> ReflectionUtils.getField(field, item))
.map(String::valueOf)
.collect(Collectors.joining(" : ")))
.collect(Collectors.joining("; "));
}
/**
* 根据枚举值的类型获取对应的 {@link Schema} 类
* 这么做是因为当SpringDoc获取不到属性的具体类型时会自动生成一个string类型的 {@link Schema} ,
* 所以需要根据枚举值的类型获取不同的实例,例如 {@link io.swagger.v3.oas.models.media.IntegerSchema}、
* {@link io.swagger.v3.oas.models.media.StringSchema}
*
* @param type 枚举值的类型
* @param sourceSchema 从属性中加载的 {@link Schema} 类
* @return 获取枚举值类型对应的 {@link Schema} 类
*/
@SuppressWarnings({"unchecked"})
default Schema<Object> getSchemaByType(Type type, Schema<?> sourceSchema) {
Schema<Object> schema;
PrimitiveType item = PrimitiveType.fromType(type);
if (item == null) {
schema = new ObjectSchema();
} else {
schema = item.createProperty();
}
// 获取schema的type和format
String schemaType = schema.getType();
String format = schema.getFormat();
// 复制原schema的其它属性
BeanUtils.copyProperties(sourceSchema, schema);
// 使用根据枚举值类型获取到的schema
return schema.type(schemaType).format(format);
}
}
全局自定义内容都是基于org.springdoc.core.customizers
包下的一些Customizer
接口,SpringDoc
在扫描接口信息时会调用这些接口以实现加载使用者的自定义内容,所以这里提供一个基础的Customizer
接口。
定义一个ApiEnumParameterCustomizer
类并实现ParameterCustomizer
接口,实现对枚举入参的自定义,同时实现BasicEnumCustomizer
接口使用工具方法。
package com.example.config.customizer;
import com.example.config.basic.BasicEnumCustomizer;
import com.example.enums.BasicEnum;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import org.springdoc.core.customizers.ParameterCustomizer;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
/**
* 枚举参数自定义配置
*
* @author vains
*/
@Component
public class ApiEnumParameterCustomizer implements ParameterCustomizer, BasicEnumCustomizer {
@Override
public Parameter customize(Parameter parameterModel, MethodParameter methodParameter) {
Class<?> parameterType = methodParameter.getParameterType();
// 枚举处理
if (BasicEnum.class.isAssignableFrom(parameterType)) {
parameterModel.setDescription(getDescription(parameterType));
Schema<Object> schema = new Schema<>();
schema.setEnum(getValues(parameterType));
parameterModel.setSchema(schema);
}
return parameterModel;
}
}
定义一个ApiEnumPropertyCustomizer
类并实现PropertyCustomizer
接口,实现对枚举属性的自定义,同时实现BasicEnumCustomizer
接口使用工具方法。
package com.example.config.customizer;
import com.example.config.basic.BasicEnumCustomizer;
import com.example.enums.BasicEnum;
import com.fasterxml.jackson.databind.type.SimpleType;
import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.oas.models.media.Schema;
import org.springdoc.core.customizers.PropertyCustomizer;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
/**
* 枚举属性自定义配置
*
* @author vains
*/
@Component
public class ApiEnumPropertyCustomizer implements PropertyCustomizer, BasicEnumCustomizer {
@Override
public Schema<?> customize(Schema property, AnnotatedType type) {
// 检查实例并转换
if (type.getType() instanceof SimpleType fieldType) {
// 获取字段class
Class<?> fieldClazz = fieldType.getRawClass();
// 是否是枚举
if (BasicEnum.class.isAssignableFrom(fieldClazz)) {
// 获取父接口
if (fieldClazz.getGenericInterfaces()[0] instanceof ParameterizedType parameterizedType) {
// 通过父接口获取泛型中枚举值的class类型
Type actualTypeArgument = parameterizedType.getActualTypeArguments()[0];
Schema<Object> schema = getSchemaByType(actualTypeArgument, property);
// 重新设置字段的注释和默认值
schema.setEnum(this.getValues(fieldClazz));
// 获取字段注释
String description = this.getDescription(fieldClazz);
// 重置字段注释和标题为从枚举中提取的
if (ObjectUtils.isEmpty(property.getTitle())) {
schema.setTitle(description);
} else {
schema.setTitle(property.getTitle() + " (" + description + ")");
}
if (ObjectUtils.isEmpty(property.getDescription())) {
schema.setDescription(description);
} else {
schema.setDescription(property.getDescription() + " (" + description + ")");
}
return schema;
}
}
}
return property;
}
}
如果读者不喜欢这样的效果可以自行修改枚举值、描述信息的显示效果
不知道大家有没有注意到BasicEnum
接口中的抽象方法getValue
上有一个@JsonValue
注解,这个注解会在进行Json序列化时会将该方法返回的值当做当前枚举的值,例如:1/2,如果不加该注解则序列化时会直接变为枚举的名字,例如: APP/WEB。
如果Restful接口入参中有@RequestBody
注解则在——统一枚举的getValue
方法上有@JsonValue
注解的基础上,无需做任何处理,对于Json入参可以这样处理,但是对于POST表单参数或GET查询参数需要添加单独的处理。
EnumConverterFactory
根据枚举的class类型获取对应的converter
,并在converter
中直接将枚举值转为对应的枚举
,具体逻辑情况代码中的注释
package com.example.config.converter;
import com.example.enums.BasicEnum;
import com.fasterxml.jackson.databind.type.TypeFactory;
import lombok.NonNull;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.function.Function;
/**
* 处理除 {@link org.springframework.web.bind.annotation.RequestBody } 注解标注之外是枚举的入参
*
* @param 枚举值的类型
* @param 枚举的类型
* @author vains
*/
@Component
public class EnumConverterFactory<V extends Serializable, E extends Enum<E>> implements ConverterFactory<String, BasicEnum<V, E>> {
@NonNull
@Override
@SuppressWarnings("unchecked")
public <T extends BasicEnum<V, E>> Converter<String, T> getConverter(Class<T> targetType) {
// 获取父接口
Type baseInterface = targetType.getGenericInterfaces()[0];
if (baseInterface instanceof ParameterizedType parameterizedType
&& parameterizedType.getActualTypeArguments().length == 2) {
// 获取具体的枚举类型
Type targetActualTypeArgument = parameterizedType.getActualTypeArguments()[1];
Class<?> targetAawArgument = TypeFactory.defaultInstance()
.constructType(targetActualTypeArgument).getRawClass();
// 判断是否实现自通用枚举
if (BasicEnum.class.isAssignableFrom(targetAawArgument)) {
// 获取父接口的泛型类型
Type valueArgument = parameterizedType.getActualTypeArguments()[0];
// 获取值的class
Class<V> valueRaw = (Class<V>) TypeFactory.defaultInstance()
.constructType(valueArgument).getRawClass();
String valueOfMethod = "valueOf";
// 转换入参的类型
Method valueOf = ReflectionUtils.findMethod(valueRaw, valueOfMethod, String.class);
if (valueOf != null) {
ReflectionUtils.makeAccessible(valueOf);
}
// 将String类型的值转为枚举值对应的类型
Function<String, V> castValue =
// 获取不到转换方法时直接返回null
source -> {
if (valueRaw.isInstance(source)) {
// String类型直接强转
return valueRaw.cast(source);
}
// 其它包装类型使用valueOf转换
return valueOf == null ? null
: (V) ReflectionUtils.invokeMethod(valueOf, valueRaw, source);
};
return source -> BasicEnum.fromValue(castValue.apply(source), targetType);
}
}
return source -> null;
}
}
WebmvcConfig
配置类,将EnumConverterFactory
注册到添加到mvc配置中package com.example.config;
import com.example.config.converter.EnumConverterFactory;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 添加自定义枚举转换配置
*
* @author vains
*/
@AllArgsConstructor
@Configuration(proxyBeanMethods = false)
public class WebmvcConfig implements WebMvcConfigurer {
private final EnumConverterFactory<?, ?> enumConverterFactory;
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverterFactory(enumConverterFactory);
}
}
Gitee地址、Github地址