最近写代码时发现将枚举类暴露到 dict 接口时总需要做一些重复的装配工作,于是写了这么个工具类来提供枚举类的自动暴露功能。通过在想要暴露的枚举类属性上引用@DictApi注解,不需要再写额外的逻辑即可将对应枚举类和字段暴露出来。

期望

  1. 添加枚举类时可以低成本或无成本地添加到已有的枚举字典接口,不需要写额外的逻辑
  2. 可以指定枚举类哪些字段是需要暴露的,且可以指定字典接口中的属性别名
  3. 支持私有属性通过公有的Getter()方法获取属性值

实现

注解类:

/**
 * @author syf
 * @create 2021-03-02
 **/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DictApi {
    String name() default "";
}

工具类:

/**
 * @author syf
 * @create 2021-03-02
 **/
@Slf4j
public class EnumDictUtils {

    private EnumDictUtils() {
    }

    private static final Class<DictApi> dictApiClass = DictApi.class;

    /**
     * get field value by getter method, because field may be private
     *
     * @param instance the instance need to be extract
     * @param field    the field with {@link DictApi} annotation
     * @return field value
     */
    private static Object runGetter(Object instance, Field field) {
        // Find the correct method
        for (Method method : instance.getClass().getMethods()) {
            if (method.getName().startsWith("get")
                    && method.getName().length() == (field.getName().length() + 3)
                    && method.getName().toLowerCase().endsWith(field.getName().toLowerCase())) {
                // Method found, run it
                try {
                    return method.invoke(instance);
                } catch (Exception e) {
                    log.error("Could not determine method, {}", method.getName());
                }
            }
        }
        return "";
    }

    /**
     * get dict map from a enum instance
     *
     * @param enumInstance a enum instance
     * @return a dict map with field value, e.g. {"fieldA": 0, "fieldB": "foo", "fieldC": "bar"}
     */
    private static Map<String, Object> getDictMapFromEnumInstance(Object enumInstance) {
        Class<?> enumInstanceClass = enumInstance.getClass();
        return Arrays.stream(enumInstanceClass.getDeclaredFields())
                .filter(field -> field.isAnnotationPresent(dictApiClass))
                .collect(Collectors.toMap(field -> {
                    String dictName = field.getAnnotation(dictApiClass).name();
                    if (StringUtils.isBlank(dictName)) {
                        dictName = field.getName();
                    }
                    return dictName;
                }, field -> runGetter(enumInstance, field)));
    }

    /**
     * get full dict map of enum classes
     *
     * @param enumClass enum classes you want to exposed to dict
     * @return a dict map with keys: enum class names, values: enum dict map
     */
    private static List<Map<String, Object>> getEnumDictList(Class<?> enumClass) {
        return Arrays.stream(enumClass.getEnumConstants())
                .map(EnumDictUtils::getDictMapFromEnumInstance)
                .collect(Collectors.toList());
    }

    /**
     * get dict map from enum classes,
     * the field with {@link DictApi} annotation will be added into the dict
     *
     * @param classes enum classes
     * @return full dict
     */
    public static Map<String, List<Map<String, Object>>> getDictFromEnums(Class<?>... classes) {
        List<Class<?>> classList = Arrays.asList(classes);
        log.info("classes: {}", classList.stream().map(Class::getSimpleName).collect(Collectors.toList()));
        return classList.stream()
                .collect(Collectors.toMap(Class::getSimpleName, EnumDictUtils::getEnumDictList));
    }
}

流程中的获取操作通过反射完成,完整步骤如下:

  1. 接收传入的枚举类Class列表,获取所有枚举实例
  2. 对所有枚举实例获取所有field,过滤掉不包含注解的field
  3. 对于包含注解的field,获取注解的name属性作为dict的key,如果为空则获取fieldName作为key
  4. 对于上述field对象,获取其Getter()方法并调用来获取dict的value
  5. 使用流收集上述dict

使用示例

对于简单的枚举类想要全部暴露,直接在每个属性上引用注解即可,例:

/**
 * @author syf
 * @create 2021-03-02
 **/
@AllArgsConstructor
@Getter
public enum TopologyDimensionEnum {
    LOGISTIC_SERVICE(0, "逻辑服务", true),
    PHYSICAL_ARCH(1, "物理架构", false),
    ;
    @DictApi
    private final Integer code;
    @DictApi
    private final String desc;
    @DictApi
    private final Boolean canSelectMultiValue;
}

对于想要自定义哪些属性需要暴露、自定义key的:

/**
 * @author syf
 * @create 2021-02-22
 **/
@Getter
@AllArgsConstructor
public enum ComponentEnum {
    APPLICATION(0, Collections.singletonList(0), "业务服务", Application.class),
    MYSQL(1, Collections.singletonList(MYSQL_JDBC_DRIVER.getId()), "MySQL", MySQL.class),
    MQ(2, Collections.singletonList(ROCKET_MQ_PRODUCER.getId()), "MQ", MQ.class),
    SHARDING_MYSQL(3, Collections.singletonList(SHARDING_JDBC.getId()), "ShardingMySQL", ShardingMySQL.class),
    REDIS(4, Collections.singletonList(JEDIS.getId()), "Redis", Redis.class),
    ELASTICSEARCH(5, Collections.singletonList(REST_HIGH_LEVEL_CLIENT.getId()), "ElasticSearch", ElasticSearch.class),
    OUTER_SERVICE(6, Arrays.asList(JETTY_CLIENT.getId(), FEIGN.getId(), HTTPCLIENT.getId(), HTTP_ASYNC_CLIENT.getId(), OKHTTP.getId()), "外部服务", OuterService.class),
    UNKNOWN_SERVICE(7, Collections.singletonList(-1), "未知服务", UnknownService.class),
    ;

    @DictApi
    private final Integer code;
    private final List<Integer> componentId;
    @DictApi(name = "codeDesc")
    private final String desc;
    private final Class<? extends SkywalkingEntity> entityClass;
    public String getLabel() {
        return entityClass.getSimpleName();
    }

Controller层将需要暴露的枚举类Class列表传入即可:

@GetMapping("/dict")
public BaseResponse<Map<String, List<Map<String, Object>>>> testTopologyQuery() {
    return BaseResponse.createSuccessResult(EnumDictUtils.getDictFromEnums(TopologyDimensionEnum.class, ComponentEnum.class));
}

效果:

{
    "success": true,
    "model": {
        "ComponentEnum": [
            {
                "code": 0,
                "desc": "业务服务"
            },
            {
                "code": 1,
                "desc": "MySQL"
            },
            {
                "code": 2,
                "desc": "MQ"
            },
            {
                "code": 3,
                "desc": "ShardingMySQL"
            },
            {
                "code": 4,
                "desc": "Redis"
            },
            {
                "code": 5,
                "desc": "ElasticSearch"
            },
            {
                "code": 6,
                "desc": "外部服务"
            },
            {
                "code": 7,
                "desc": "未知服务"
            }
        ],
        "TopologyDimensionEnum": [
            {
                "codeDesc": "逻辑服务",
                "code": 0,
                "canSelectMultiValue": true
            },
            {
                "codeDesc": "物理架构",
                "code": 1,
                "canSelectMultiValue": false
            }
        ]
    }
}

标签: java, 反射, 注解, 枚举, enum

添加新评论