buguge - Keep it simple,stupid

知识就是力量,但更重要的,是运用知识的能力why buguge?

导航

enumgen升级,支持默认枚举项

要解决的问题--->enumgen支持默认枚举项

我的插件工具enumgen投产后,在一次codereview时,我注意到,有的枚举里getBeanByCode是如下这样实现的。即,当无法匹配到对应枚举时,就返回一个默认枚举项。而我的enumgen生成的getBeanByCode里,最后是return null;,这样的代码可能邂逅NPE异常。

public enum PayBusinessResultStatusEnum implements EnumAbility<String> {
 
    SUCCESS("S""成功"),
    FAIL("F""失败"),
    ...
    REJECT("R""否决 企业审批否决"),
    DEFAULT("UNKNOWN","未知"),
    ;
 
    private String code;
    private String desc;
    ....
 
    public static PayBusinessResultStatusEnum getBeanByCode(String code) {
        PayBusinessResultStatusEnum[] values = PayBusinessResultStatusEnum.values();
        for (PayBusinessResultStatusEnum value : values) {
            if (code.equals(value.getCode())) {
                return value;
            }
        }
        return DEFAULT;
    }
}

 

enumgen怎么支持这一点呢?-->方案有二

1)再定义一个注解 @EnumDefaultValue ,用以在枚举类里标记某个枚举项是默认枚举项。EnumGenProcessor 里,在生成getBeanByCode方法的return语句处,进行判断,是否存在标记了 @EnumDefaultValue 的枚举项,有则返回该枚举项。

2)上面又加了一个注解,总觉着会增加开发者的理解成本。所以,不如还是在 @EnumGetByCode 注解上做文章,那么,不外乎为 注解增加一个成员 defaultValue,用来指定枚举类的默认枚举项。

综合评估,我觉得还是方案二更优一些。

 

下面来介绍针对两种方案的javac代码实现

1) @EnumDefaultValue注解方案。javac代码比较好写,我三下五除二就完成了。

1.1)新建注解 @EnumDefaultValue

package com.emax.annotation;
 
/**
 * 标记枚举项为默认项。 当为enum设置了默认项, 则在生成的getBeanByCode方法里,当无法获取到特定的枚举项时,不再返回null,而会返回这个默认项。
 * @see <a href=“http://wiki.youfupingtai.com/x/4QFOB”>wiki说明文档</a>
 */
@Target(ElementType.FIELD)
@Retention(value = RetentionPolicy.SOURCE)
public @interface EnumDefaultValue {
}

1.2)EnumGenProcessor#getBeanByCode改造

private JCTree.JCMethodDecl getBeanByCode(JCTree.JCClassDecl jcClassDecl, CodeDefinitionEnum codeDefinitionEnum, JCTree.JCExpression paramType) {
    ...
 
    /* return null; */
    JCTree.JCReturn aReturn1;
    JCTree.JCVariableDecl enumDefaultValue = JavacASTUtil.getEnumDefaultValue(jcClassDecl);
    if (enumDefaultValue == null) {
        aReturn1 = maker.Return(maker.Literal(TypeTag.BOT, null));
    else {
        aReturn1 = maker.Return(maker.Select(maker.Ident(jcClassDecl.name), enumDefaultValue.name));
    }
    body.append(aReturn1);
     
    ...
}

JavacASTHelper#getEnumDefaultValue

public class JavacASTUtil {
    public static JCTree.JCVariableDecl getEnumDefaultValue(JCTree.JCClassDecl enumDef) {
        List<JCTree> members = enumDef.getMembers();
        for (JCTree p : members) {
            if (p.getKind() == Tree.Kind.VARIABLE) {
                if (hasAnnotation(((JCTree.JCVariableDecl) p).mods, EnumDefaultValue.class)) {
                    return (JCTree.JCVariableDecl) p;
                }
            }
        }
        return null;
    }
 
    public static boolean hasAnnotation(JCTree.JCModifiers jcModifiers, Class annotationType) {
        List<JCTree.JCAnnotation> annotationList = jcModifiers.annotations;
        if (annotationList == nullreturn false;
        for (JCTree annotation : annotationList) {
            if (annotation.type.toString().equals(annotationType.getName()))
                return true;
        }
        return false;
    }
}

 

1.3)@EnumGetByCode,完善javadoc,引导开发者了解@EnumDefaultValue

package com.emax.annotation;
 
/**
 * 为Enum类生成 getBeanByCode 方法.前提是Enum包含getCode()方法
 * @see <a href=“http://wiki.youfupingtai.com/x/4QFOB”>wiki说明文档</a>
 * @see EnumDefaultValue
 */
@Target(ElementType.TYPE)
@Retention(value = RetentionPolicy.SOURCE)
public @interface EnumGetByCode {
}


2)@EnumGetByCode#defaultValue方案

2.1)定义@EnumGetByCode#defaultValue

package com.emax.annotation;
 
/**
 * 为Enum类生成 getBeanByCode 方法.前提是Enum包含getCode()方法
 * @see <a href=“http://wiki.youfupingtai.com/x/4QFOB”>wiki说明文档</a>
 */
@Target(ElementType.TYPE)
@Retention(value = RetentionPolicy.SOURCE)
public @interface EnumGetByCode {
    /**
     * 为枚举类指定默认的枚举项。<br>当<code>getBeanByCode</code>方法匹配不到枚举项时,使用此默认项。
     * <br>【注意】所指定的枚举项字符串必须存在于枚举类中,否则编译会报错---->找不到符号  符号:   变量 DEFAULT1 位置: 类 com.emax.enums.CreditStatusEnum
     * @return
     */
    String defaultValue() default "";
}

美中不足,defaultValue的类型是String。如果能支持 枚举类.枚举项 的方式,当是极好。不过,与优秀的 `双JIE` 探讨,无法实现。

2.2)EnumGenProcessor#getBeanByCode改造

private JCTree.JCMethodDecl getBeanByCode(JCTree.JCClassDecl jcClassDecl, CodeDefinitionEnum codeDefinitionEnum, JCTree.JCExpression paramType) {
    ...
    /* return null; */
    JCTree.JCReturn aReturn1;
    String enumDefaultValue = JavacASTUtil.getEnumDefaultValueString(jcClassDecl);
    if (enumDefaultValue == null) {
        aReturn1 = maker.Return(maker.Literal(TypeTag.BOT, null));
    else {
        aReturn1 = maker.Return(maker.Select(maker.Ident(jcClassDecl.name), names.fromString(enumDefaultValue)));
    }
    body.append(aReturn1);
     
    ...
}

JavacASTUtil#getEnumDefaultValueString

public class JavacASTUtil {
    public static String getEnumDefaultValueString(JCTree.JCClassDecl enumDef) {
        List<JCTree.JCAnnotation> annotations = enumDef.mods.annotations;
        JCTree.JCAnnotation jcAnnotation = annotations.stream().filter(p -> p.type.toString().equals(EnumGetByCode.class.getName())).findFirst().get();
        List<JCTree.JCExpression> arguments = jcAnnotation.getArguments();
        for (JCTree.JCExpression arg:arguments){
            JCTree.JCAssign assign = (JCTree.JCAssign) arg;
            String mName = assign.lhs.toString();
            if ("defaultValue".equals(mName)){
                // 借鉴自 lombok javac.java#calculateGuess(JCExpression expr)
                return ((JCTree.JCLiteral)assign.rhs).value.toString();
            }
        }
        return null;
    }
}

 

【花絮】

花絮1

对于@EnumGetByCode#defaultValue方案,改造完后,在build项目时,遇到如下错误。定睛细看,原来生成的getBeanByCode方法里,最后的return语句是 return CreditStatusEnum."DEFAULT" 。这是因为当时在获取defaultValue时的javac代码是 return assign.rhs.toString(); 。后来翻看lombok源码才找到正确答案。

Information:java: Errors occurred while compiling module 'enumgenmaven'
Information:javac 1.8.0_241 was used to compile java sources
Information:2023/4/23 17:28 - Build completed with 1 error and 0 warnings in 6 476 ms
D:\SourceProject\mono\enumgenmaven\src\test\java\com\emax\annotation\plugintest\CreditStatusEnum.java
Error:(118) java: 找不到符号
  符号:   变量 "DEFAULT"
  位置: 类 com.emax.annotation.plugintest.CreditStatusEnum

 

花絮2

我在编码时尤其注重明确数据类型,不是上来就用String来定义年龄、性别、状态这样的数据。用字符串表示这些数据,一来不利于理解和扩展,再者可能会为代码重构埋雷。

仔细推敲琢磨,再去跟优秀的双JIE探讨,最终不得已才将@EnumGetByCode#defaultValue定义成了String。

我在编写EnumGenProccessor#getBeanByCode里的javac代码时,一度想兼容开发者不小心输错defaultValue的情况。后来,王JIE建议,错了就让编译器报错,提示开发者去修正。三个臭皮匠赛过诸葛亮,此建议甚好!

例如,下面枚举在编译时生成getBeanByCode后,编译器会报错。此时,开发者将defaultValue的值修正即可。

@Getter
@EnumGetByCode(defaultValue = "DEFAULT1")
public enum CreditStatusEnum {
    UN_REPAYMENT("UN_REPAYMENT""待还款"),
    OVERDUE("OVERDUE""已逾期"),
    REPAYMENT_SUCCESS("REPAYMENT_SUCCESS""已还款"),
    DEFAULT("DEFAULT""未知");
 
    private String code;
    private String value;
}

CreditStatusEnum添加的getBeanByCode方法如下

public static CreditStatusEnum getBeanByCode(String code) {
  if (Objects.isNull(code)) return null;
  CreditStatusEnum[] vals = values();
  for (CreditStatusEnum val : vals) {
      if (val.getCode().equals(code)) return val;
  }
  return CreditStatusEnum.DEFAULT1;
}

编译器报错截图:

 

 

 

【EOF】

posted on 2023-04-19 21:11  buguge  阅读(81)  评论(1编辑  收藏  举报