稀土掘金技术社区

巧用Java枚举封装和自定义Function,给前端输出标准的字典

一、需求背景

在日常的后端接口开发过程中,我们经常会用到一些枚举字典来表示一些类型、状态等字典值,例如 「性别」、「订单状态」、「支付方式」 等等。

1.1 通用枚举

在 Java 中,我们一般都这么封装:

@Getter
@AllArgsConstructor
publicenum NotifyChannel {
  WORK_WECHAT(1, "企业微信"),
  FEI_SHU(2, "飞书")
// ...
  ;
privatefinalint key;
privatefinal String label;
}

然而,为了方便前端通过 「API」 来查询这个字典,我们一般都会封装成 「JSON」 格式的数据,例如:

[
  {
"key": 1,
"label": "企业微信"
  },
  {
"key": 2,
"label": "飞书"
  },
// ...
]

其中,key 和 label 是我们所有字典都应该有的两个属性。

1.2 扩展枚举

但我们可能会添加一些扩展属性:

@Getter
@AllArgsConstructor
publicenum Gender {
  MALE(1, "男", "#409EFF"),
  FEMALE(2, "女", "#67C23A");

privatefinalint key;
privatefinal String label;
privatefinal String color;
}

如何编写一个 API 来获取字典数据呢?

二、实现过程

2.1 提取公共属性

为了统一掉字典中都含有的 keylabel 两个属性,我们先抽离一个标准接口出来:

publicinterfaceIDictionary{
intgetKey();
String getLabel();
}

然后,要求所有的枚举类都实现这个接口,并实现其方法。

publicenum NotifyChannel implements IDictionary {
// 枚举项
  ;
privatefinalint key;
privatefinal String label;
}

2.2 遍历枚举

我们可以使用枚举类的 getEnumConstants 方法来获取所有的枚举项。

Arrays.stream(clazz.getEnumConstants()).forEach(enumItem -> {
// 处理每一项
});

2.3 编写工具类

接下来,我们就可以实现一个 DictionaryUtil 工具类,用于处理字典。

publicclassDictionaryUtil{
// 其他方法
publicstatic <D extends IDictionary> @NotNull List<Map<String, Object>> getDictionaryList(
@NotNull Class<D> clazz
    ) {
    List<Map<String, Object>> mapList = new ArrayList<>();
//取出所有枚举类型
    Arrays.stream(clazz.getEnumConstants()).forEach(enumItem -> {
      Map<String, Object> item = new HashMap<>(2);
      item.put("key", enumItem.getKey());
      item.put("label", enumItem.getLabel());
      mapList.add(item);
    });
return mapList;
  }
}

这样,我们确实能拿到了需求背景里的这个 「List」 数据了:

[
  {
"key": 1,
"label": "企业微信"
  },
  {
"key": 2,
"label": "飞书"
  },
// ...
]

「但依然有问题:」

❝

如果我们还要拿扩展的 color 这些属性的话,我们得给再写一个方法,不是很方便。

❞

2.4 重载自定义属性方法

于是,我们考虑给 getDictionaryList 支持一个重载,以便可以自由传入需要获取的属性:

publicclassDictionaryUtil{
// 其他方法
publicstatic <D extends IDictionary> @NotNull List<Map<String, Object>> getDictionaryLists(
@NotNull Class<D> clazz
  ) {
return getDictionaryLists(clazz, "key", "label");
  }

publicstatic <D extends IDictionary> @NotNull List<Map<String, Object>> getDictionaryLists(
@NotNull Class<D> clazz, String... props
  ) {
    List<Map<String, Object>> mapList = new ArrayList<>();
//取出所有枚举类型
    Arrays.stream(clazz.getEnumConstants()).forEach(enumItem -> {
      Map<String, Object> item = new HashMap<>(2);
for (String prop : props) {
try {
          Method method = clazz.getMethod("get" + StringUtils.capitalize(prop));
          Object value = method.invoke(enumItem);
          item.put(prop, value);
        } catch (Exception exception) {
          log.error(exception.getMessage(), exception);
        }
      }
      mapList.add(item);
    });
return mapList;
  }
}

这样我们就可以方便的通过下面的方式来调用:

DictionaryUtil.getDictionaryLists(ServiceError.class);
DictionaryUtil.getDictionaryLists(ServiceError.class, "key", "label", "color");
DictionaryUtil.getDictionaryLists(ServiceError.class, "key", "color");

「但依然还不够优雅:」

❝

我们可能会传入枚举中压根没有的属性,而且存在很多字符串的 「魔法值」,这很不优雅。

❞

我们更希望这么调用:

DictionaryUtil.getDictionaryList(ServiceError.class, ServiceError::getKey, ServiceError::getLabel);

这样可以规避掉传入不存在的枚举属性,并且可以避免魔法值。

三、Lambda优化代码

于是我们再次修改和优化:

Arrays.stream(clazz.getEnumConstants()).forEach(enumItem -> {
  Map<String, Object> item = new HashMap<>(lambdas.length);
// 依次取出参数的值
  Arrays.stream(lambdas).forEach(lambda -> {
try {
// String prop = 从lamba表达式中取出属性名
      item.put(prop, lambda.apply(enumItem));
    } catch (Exception exception) {
      log.error(exception.getMessage(), exception);
    }
  });
  mapList.add(item);
});

这里我们就需要考虑,如何通过 lamba 表达式来获取属性名了,例如:ServiceError::getKey 我们需要获取到的属性名为 key

3.1 自定义Function

查阅资料发现,需要实现一个自定义的 Function 接口,并实现 Serializable 接口。于是我们声明了一个:

@FunctionalInterface
publicinterfaceIFunction<T, R> extendsFunction<T, R>, Serializable{
}

3.2 反射读取属性名

再编写一个反射方法来获取属性名:

/**
 * <h1>反射工具类</h1>
 *
 * @author Hamm.cn
 * @see IDictionary
 */

publicclassReflectUtil{
/**
   * <h3>获取 {@code Lambda} 的 {@code Function} 表达式的函数名</h3>
   *
   * @param lambda 表达式
   * @return 函数名
   */

publicstatic@NotNullString getLambdaFunctionName(@NotNull IFunction<?, ?> lambda){
try {
      Method replaceMethod = lambda.getClass().getDeclaredMethod("writeReplace");
      replaceMethod.setAccessible(true);
      SerializedLambda serializedLambda = (SerializedLambda) replaceMethod.invoke(lambda);
return serializedLambda.getImplMethodName()
                .replace("get", "");
    } catch (Exception exception) {
thrownew ServiceException(exception);
    }
  }
}

3.3 Lambda实现

这下就方便了,我们可以这么实现:

Arrays.stream(clazz.getEnumConstants()).forEach(enumItem -> {
  Map<String, Object> item = new HashMap<>(lambdas.length);
// 依次取出参数的值
  Arrays.stream(lambdas).forEach(lambda -> {
try {
// String prop = 从lamba表达式中取出属性名 并取消首字母的大写 
      String prop = StringUtils.uncapitalize(ReflectUtil.getLambdaFunctionName(lambda))
      item.put(prop, lambda.apply(enumItem));
    } catch (Exception exception) {
      log.error(exception.getMessage(), exception);
    }
  });
  mapList.add(item);
});

3.4 优雅搞掂~

接下来就可以开心优雅的通过 lamba 表达式获取枚举可选项列表了:

DictionaryUtil.getDictionaryList(ServiceError.class);
DictionaryUtil.getDictionaryList(
    ServiceError.class, 
ServiceError::getKey, 
ServiceError::getLabel
)
;
DictionaryUtil.getDictionaryList(
    UserGender.class, 
UserGender::getKey, 
UserGender::getLabel, 
UserGender::getColor
)
;

四、总结

懒得总结了。

参考源代码可以关注我们的开源项目:

https://github.com/HammCn/AirPower4J

今天就这样,Bye~

AI编程资讯AI Coding专区指南:https://aicoding.juejin.cn/aicoding

图片
点击"阅读原文"了解详情~