关于项目中的枚举类型使用的规范化

问题背景

公司中对枚举的使用规范(姑且称为规范吧)是对枚举定义一个int类型的code成员变量,同时定义配套的code()和codeOf(int)方法,枚举的存储、传输都是用code来代表具体的枚举值;
问题在于,int类型虽然便于存储和传输,但是int类型无法体现不同枚举类型的信息,无法充分利用特殊化的类型信息进行编程,在代码中直接用int来定义方法参数、处理业务逻辑也会给人带来模糊的含义(往往只能靠字段名来识别int类型的含义)。
总的来说,除了传输和存储以外,应用代码中应该统一使用纯的枚举类型而不是int,而应用代码与外部的交互则是使用经过转换的int。这里需要解决问题的便是应用边界处的枚举转换问题。

最佳实践

  • Dao层

    使用公司common包中既有的CodeEnumTypeHandler,由于涉及到从int到特定类型枚举的反向转换,该TypeHandler中需要保存特定枚举的class信息;使用的时候需要给CodeEnumTypeHandler配置对应的枚举类路径作为构造参数。

      <!-- 在mybatis-config.xml配置CodeEnumTypeHandler -->
      <typeHandlers>
          <!-- 为每个枚举类注册一个CodeEnumTypeHandler -->
          <typeHandler javaType="com.qunar.flight.jy.api.enums.BusinessScope"
                       handler="com.qunar.base.meerkat.orm.mybatis.type.CodeEnumTypeHandler"/>
          <typeHandler ... />
          <package name="com.qunar.flight.jy.common.utils.database.handler"/>
      </typeHandlers>
    

    // 当前的使用方式需要我们为枚举类逐个配置CodeEnumTypeHandler,考虑如何实现CodeEnumTypeHandler的自动化注册

  • Service层

    业务层,当需要将qconfig的json配置或者http接口的json返回值解析为包含枚举类型的对象时,需要使用定制化的ObjectMapper实例。
    在此之前,我们需要一个CodeEnumJsonSerialzer和一个CodeEnumJsonDeserialzer。

      /**
       * Code枚举json序列化
       */
      public class CodeEnumJsonSerializer<T extends Enum<T>> extends JsonSerializer<T> {
       
          @Override
          public void serialize(T value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
              int code = CodeEnumUtil.code(value);
              gen.writeNumber(code);
          }
       
      }
      
      /**
       * Code枚举json反序列化
       */
      public class CodeEnumJsonDeserializer<T extends Enum<T>> extends JsonDeserializer<T> implements ContextualDeserializer {
       
          @SuppressWarnings("unchecked")
          @Override
          public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
              Class<T> enumType = (Class<T>) ctxt.getAttribute(p.getCurrentName());
              return CodeEnumUtil.codeOf(enumType, p.getValueAsInt());
          }
       
          @Override
          public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
                  throws JsonMappingException {
              ctxt.setAttribute(property.getName(), property.getType().getRawClass());
              return this;
          }
      }
    

    // 实际使用中, 发现通用的反序列化不一定生效(待解决); 这种情况下, 一个退而求其次的办法是给枚举字段加上@JsonDeserialize注解, 每个字段使用指定的反序列化类

    为ObjectMapper注册一个支持CodeEnum枚举序列化和解序列化的module,并将ObjectMapper声明为spring bean:

      /**
       * ObjectMapper配置类, 对枚举类型使用code值做json序列化和解序列化的ObjectMapper
       *
       * @author chenjiahua.chen
       */
      @Configuration
      public class ObjectMapperConfiguration {
       
          @Bean
          public ObjectMapper objectMapper() {
              SimpleModule module = new SimpleModule();
              module.addSerializer(Enum.class, new CodeEnumJsonSerializer<>());
              module.addDeserializer(Enum.class, new CodeEnumJsonDeserializer<>());
              // TO.DO. 更多规范化的ser和deser, 如DateTime类型
              // TO.DO. 更加合理化的ObjectMapper序列化、解系列化配置,如忽略null字段
              return new ObjectMapper().registerModule(module);
          }
      }
    

    在相关的service类中注入配置过的objectMapper:

      @Component
      public class AccountingItemConfig {
       
          @Resource
          private ObjectMapper objectMapper;
       
          // use objectMapper to do something
      }
    

    // 对于dubbo接口,最好在接口定义阶段就使用枚举,提取出枚举的api(但要考虑api升级的兼容性问题?!)

  • Controller层

    Controller的处理方式取决于前端参数的提交方式和后端数据的返回方式。

    1)使用表单默认的数据编码方式(content-type为application/x-www-form-urlencoded类型),对参数的转换需要使用表单参数绑定方法(@InitBinder方法),可以在@ControllerAdvice注解的类中定义全局的@InitBinder方法:

      /**
       * 全局参数绑定的@ControllerAdvice类
       *
       * @author chenjiahua.chen
       */
      @ControllerAdvice
      public class GlobalControllerInitBinder {
       
          @InitBinder
          public void initBinderBusinessScope(WebDataBinder binder) {
              registerEnumEditor(binder, ProfitType.class, "profitType");
              registerEnumEditor(binder, BusinessScope.class, "businessScope");   // 可以不指定字段名
              // 其他类型参数绑定...
          }
       
          private <T extends Enum<T>> void registerEnumEditor(WebDataBinder binder, Class<T> clz, String propertyName) {
              binder.registerCustomEditor(clz, propertyName, newEnumEditor(clz));
          }
       
          private <T extends Enum<T>> PropertyEditor newEnumEditor(Class<T> clz) {
              return new PropertyEditorSupport() {
                  @Override
                  public void setAsText(String text) {
                      setValue(CodeEnumUtil.codeOf(clz, Integer.parseInt(text)));
                  }
              };
          }
      }
    

    2)使用json的编码方式(content-type为application/json类型),需要定制或者扩展Spring MVC的json消息转换器

      /**
       * 支持数字和枚举类型转换的json消息转换器
       *
       * @author chenjiahua.chen
       */
      public class CodeEnumJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
       
          public CodeEnumJackson2HttpMessageConverter() {
              super();
              registerModule();
          }
       
          private void registerModule() {
              SimpleModule module = new SimpleModule();
              module.addSerializer(Enum.class, new CodeEnumJsonSerializer());
              module.addDeserializer(Enum.class, new CodeEnumJsonDeserializer<>());
              objectMapper.registerModule(module);
          }
      }
    

    在mvc上下文中配置如下:

      <!-- 配置自定义的message-converter -->
      <mvc:annotation-driven>
          <mvc:message-converters>
              <bean class="com.qunar.flight.jy.common.utils.web.CodeEnumJackson2HttpMessageConverter"/>
          </mvc:message-converters>
      </mvc:annotation-driven>
    
  • 其他

    用到的枚举工具类:

      /**
       * 枚举类工具, 用于enum和int/String转换; 相关的枚举类应符合CodeEnum规范(包含成员方法code()和静态方法codeOf(int))
       *
       * @author chenjiahua.chen
       */
      public class CodeEnumUtil {
       
          private CodeEnumUtil() {
          }
       
          public static <T extends Enum<T>> int code(T t) {
              try {
                  Method code = t.getClass().getDeclaredMethod("code");
                  Object rs = code.invoke(t);
                  return (int) rs;
              } catch (ReflectiveOperationException e) {
                  throw new IllegalArgumentException(e);
              }
          }
       
          @SuppressWarnings("unchecked")
          public static <T extends Enum<T>> T codeOf(Class<T> enumType, int code) {
              try {
                  Method codeOf = enumType.getDeclaredMethod("codeOf", int.class);
                  Object rs = codeOf.invoke(null, code);
                  return (T) rs;
              } catch (ReflectiveOperationException e) {
                  throw new IllegalArgumentException(e);
              }
          }
       
          public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) {
              return T.valueOf(enumType, name);
          }
      }
    

    对于spring-boot应用,可以利用spring-boot提供的JsonComponentModule来扫描被@JsonComponent注解的类并自动注册JsonSerializer和JsonDeserializer。
    一个定义好的JsonComponent如下:

      /**
       * 该类结合spring-boot的{@link JsonComponentModule}使用, 可被自动发现并注册json序列化和反序列化组件
       *
       * @author chenjiahua.chen
       */
      @JsonComponent
      public class CodeEnumJsonComponent {
       
          private CodeEnumJsonComponent() {
          }
       
          public static class Ser extends CodeEnumJsonSerializer {
          }
       
          public static class Deser extends CodeEnumJsonDeserializer {
          }
      }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354