巧用Java枚举封装和自定义Function,给前端输出标准的字典
2025-02-21 09:02 阅读(95)

一、需求背景

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


通用枚举

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

@Getter
@AllArgsConstructor
public enum NotifyChannel {
  WORK_WECHAT(1, "企业微信"),
  FEI_SHU(2, "飞书")
  // ...
  ;
  private final int key;
  private final String label;
}

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

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

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


扩展枚举

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


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

  private final int key;
  private final String label;
  private final String color;
}

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


二、实现过程

提取公共属性

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

public enum NotifyChannel implements IDictionary {
  // 枚举项
  ;
  private final int key;
  private final String label;
}

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

public interface IDictionary {
  int getKey();
  String getLabel();
}

遍历枚举

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


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

编写工具类

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

public class DictionaryUtil {
  // 其他方法
  public static <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 这些属性的话,我们得给再写一个方法,不是很方便。


重载自定义属性方法

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

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

  public static <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");

但依然还不够优雅:


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


我们更希望这么调用:

java 代码解读复制代码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

自定义Function

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

@FunctionalInterface
public interface IFunction<T, R> extends Function<T, R>, Serializable {
}

反射读取属性名

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

/**
 * <h1>反射工具类</h1>
 *
 * @author Hamm.cn
 * @see IDictionary
 */
public class ReflectUtil {
  /**
   * <h3>获取 {@code Lambda} 的 {@code Function} 表达式的函数名</h3>
   *
   * @param lambda 表达式
   * @return 函数名
   */
  public static @NotNull String 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) {
      throw new ServiceException(exception);
    }
  }
}

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);
});

优雅搞掂~

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

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