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