SpringDoc枚举字段处理与SpringBoot接收枚举参数处理

本期内容

  1. 添加SpringDoc配置展示枚举字段,在文档页面中显示枚举值和对应的描述
  2. 添加SpringMVC配置使项目可以接收枚举值,根据枚举值找到对应的枚举

默认内容

先不做任何处理看一下直接使用枚举当做入参是什么效果。

  1. 定义一个枚举
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;

}

  1. 定义一个入参类
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;

}

  1. 定义一个接口,测试枚举入参的效果
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;
    }

}

  1. 启动项目,查看接口文档显示效果

    单个枚举效果
    SpringDoc枚举字段处理与SpringBoot接收枚举参数处理_第1张图片
    作为参数属性显示效果
    SpringDoc枚举字段处理与SpringBoot接收枚举参数处理_第2张图片

文档中默认显示枚举可接收的值是定义的枚举名字(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;
}

单独枚举入参显示效果

SpringDoc枚举字段处理与SpringBoot接收枚举参数处理_第3张图片

枚举作为参数类属性

单独处理没有好的办法,像上边添加allowableValues属性只会在原有列表上添加,如下

SpringDoc枚举字段处理与SpringBoot接收枚举参数处理_第4张图片

全局统一处理方案

准备工作

  1. 定义一个统一枚举接口
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);
    }

}

我这里为了通用性将枚举值的类型也设置为泛型类型了,如果不需要可以设置为具体的类型,比如StringInteger等,如果像我这样处理起来会稍微麻烦一些;另外我这里只提供了一个getValue的抽象方法,你也可以再提供一个getNamegetDescription等获取枚举描述字段值的抽象方法。

  1. 让项目中的枚举实现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;

}

  1. 定义一个基础自定义接口,提供一些对枚举的操作方法
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;
    }

}

如果读者不喜欢这样的效果可以自行修改枚举值、描述信息的显示效果

重启项目查看效果

接口1
SpringDoc枚举字段处理与SpringBoot接收枚举参数处理_第5张图片
接口2
SpringDoc枚举字段处理与SpringBoot接收枚举参数处理_第6张图片
接口3
SpringDoc枚举字段处理与SpringBoot接收枚举参数处理_第7张图片

SpringBoot接收枚举入参处理

不知道大家有没有注意到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);
    }
}

重启项目并打开在线文档进行测试

SpringDoc枚举字段处理与SpringBoot接收枚举参数处理_第8张图片

Gitee地址、Github地址

你可能感兴趣的:(SpringDoc,spring,boot,java,spring)