美文网首页
关于项目中的枚举类型使用的规范化

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

作者: 乐不思孰 | 来源:发表于2018-03-29 19:48 被阅读0次

问题背景

公司中对枚举的使用规范(姑且称为规范吧)是对枚举定义一个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 {
          }
      }

相关文章

网友评论

      本文标题:关于项目中的枚举类型使用的规范化

      本文链接:https://www.haomeiwen.com/subject/qmkvcftx.html