优雅地将 Enum 暴露到 HTTP 接口
最近写代码时发现将枚举类暴露到 dict 接口时总需要做一些重复的装配工作,于是写了这么个工具类来提供枚举类的自动暴露功能。通过在想要暴露的枚举类属性上引用@DictApi
注解,不需要再写额外的逻辑即可将对应枚举类和字段暴露出来。
期望
- 添加枚举类时可以低成本或无成本地添加到已有的枚举字典接口,不需要写额外的逻辑
- 可以指定枚举类哪些字段是需要暴露的,且可以指定字典接口中的属性别名
- 支持私有属性通过公有的
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));
}
}
流程中的获取操作通过反射完成,完整步骤如下:
- 接收传入的枚举类Class列表,获取所有枚举实例
- 对所有枚举实例获取所有field,过滤掉不包含注解的field
- 对于包含注解的field,获取注解的
name
属性作为dict的key,如果为空则获取fieldName作为key - 对于上述field对象,获取其
Getter()
方法并调用来获取dict的value - 使用流收集上述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
}
]
}
}