源碼 https://gitee.com/eric-tutorial/SpringCloud-multiple-gradle
本文內(nèi)容是:SpringBoot+Mybatisplus中枚舉正反序列化的實際應(yīng)用
本文基于SpringBoot+Mybatisplus 框架就Java枚舉的正反序列化的實際應(yīng)用進(jìn)行一次分析與研究,此外順便帶上DAO層關(guān)于枚舉的操作,使得程序中完全使用枚舉編程。由于SpringBoot內(nèi)置的json處理器是jackson,所以本文的json相關(guān)處理也就是采用默認(rèn)的jackson。
背景
N久之前,leo曾經(jīng)問我枚舉的應(yīng)用,我清楚地記得菜鳥教程(https://www.runoob.com/)上面有這樣一段話。
當(dāng)時我還找到了給leo看,說這玩意要被取代了.現(xiàn)在看來是我斷章取義了。因為那時候很少會接觸到枚舉,所以我以為這玩意真的沒救了。
現(xiàn)在看來,看多了不去實踐與思考,正應(yīng)了一句話“盡信書不如無書”。
最近《阿里巴巴開發(fā)規(guī)范-嵩山版版》有下面的一句話,可能會與接下來的內(nèi)容相沖。這里為啥不能返回枚舉類型?大概率因為集團內(nèi)部的RPC調(diào)用的時候,版本升級無法正確兼容。
無論枚舉要怎么使用,我還是按照自己的相關(guān)需求來實踐了一把,由于項目中有很多枚舉,使用和管理起來非常暈乎乎的。需要把枚舉與Integer轉(zhuǎn)來轉(zhuǎn)去,前端傳輸過來了一個Integer,需要手動將Integer轉(zhuǎn)成枚舉,存儲到數(shù)據(jù)庫的時候,又得將枚舉轉(zhuǎn)成Integer保存。如果純粹使用Integer傳值,編碼又不能知道這個數(shù)字代表啥意思,最后找來找去。不光是后端很是暈乎乎的。前端由于也只接受了Integer,需要顯示文字的時候,只能前后端共同定,一旦后端修改了枚舉,那么前端必須同步修改。所以我在網(wǎng)上找了一些解決辦法,但是都不盡人意。最后折騰了jackson源碼并求助于jackson的維護者解決了枚舉正反序列化的問題。
先看下一般工程的基本模型
本文的重點是枚舉的正反序列化,但是為了讓整個枚舉在工程中的應(yīng)用比較完整,也會描述下枚舉在DAO層的操作。jackson的正反序列化主要應(yīng)用在Controller層的參數(shù)接收與結(jié)果返回。在參數(shù)接收的時候有兩種形式,一種的前端通過表單提交的數(shù)據(jù),另一種是從body提交的json數(shù)據(jù),兩種有很大的區(qū)別,在Controller的方法里面主要體現(xiàn)在body提交的json數(shù)據(jù)需要在對象前面加上@RequestBody.當(dāng)然兩者本質(zhì)上有點區(qū)別,由于表單提交的不是json,所以無法采用json反序列化,但是本文中會順帶描述到表單提交的數(shù)據(jù)如何轉(zhuǎn)換成枚舉。
show you code
工程源代碼
https://gitee.com/eric-tutorial/SpringCloud-multiple-gradle
篇幅有限,只講述重點代碼邏輯,完整的可以參考源代碼。項目基于Gradle構(gòu)建.
定義枚舉
public enum GenderEnum {
BOY(100, "男"), GIRL(200, "女"),UNKNOWN(0, "未知");
private final Integer code;
private final String description;
GenderEnum(int code, String description) {
this.code = code;
this.description = description;
}
}
接受參數(shù)的對象
@Data
public class UserParam {
@NotBlank(message = "name不能為空")
String name;
@NotNull(message = "gender為100或者200")
GenderEnum gender;
@NotNull(message = "age不能為空")
Integer age;
}
Controller POST方法
@PostMapping("add/body")
public BaseResponseVO saveBody(@Valid @RequestBody UserParam userParam) {
UserModel userModel = userService.add(userParam);
return BaseResponseVO.success(userModel);
}
上面代碼可以看出來框架在接受參數(shù)的時候?qū)⒕W(wǎng)絡(luò)傳輸過來的數(shù)據(jù)進(jìn)行了反序列化,在返回給前端的時候進(jìn)行了正序列化成json返回的。默認(rèn)的jackson是無法直接按照GenderEnum中的code來正反序列化枚舉的,因為jackson有一套自己的枚舉序列化機制,從源代碼中看出來,它是按照name和ordinal來正反序列化的。但是這個不能滿足我自己定義的code和description來正反序列化的需求。因此我在網(wǎng)上搜了下,看看有木有人完成這樣的需求,我想這個需求應(yīng)該比較正常,網(wǎng)上一搜果然有很多。很快就有了下面的代碼(最后發(fā)現(xiàn)都是采用默認(rèn)的jackson枚舉正反序列化器,并不滿足需求)。
自定義的枚舉序列化器
面向接口編程
為我需要正序列化的枚舉統(tǒng)一定義了一個接口.所以需要參與正序列化的枚舉都得實現(xiàn)這個接口.
public interface BaseEnum {
/**
* Code integer.
*
* @return the integer
*/
Integer code();
/**
* Description string.
*
* @return the string
*/
String description();
}
正序列化器
@Slf4j
public class BaseEnumSerializer extends JsonSerializer<BaseEnum> {
@Override
public void serialize(BaseEnum value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException {
log.info("\n====>開始序列化[{}]", value);
gen.writeStartObject();
gen.writeNumberField("code", value.code());
gen.writeStringField("description", value.description());
gen.writeEndObject();
}
}
效果就是既返回code和description,前端既知道code也知道description.description可以直接顯示,code可以用來返回給后端的操作.前端再也不用同步修改description了,也不需要自己判斷code是啥意思,直接顯示description即可.皆大歡喜.
反序列化器
@Slf4j
public class BaseEnumDeserializer extends JsonDeserializer<BaseEnum> {
@Override
public BaseEnum deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
try {
//前端輸入的值
String inputParameter = p.getText();
if (StringUtils.isBlank(inputParameter)) {
return null;
}
JsonStreamContext parsingContext = p.getParsingContext();
String currentName = parsingContext.getCurrentName();//字段名
Object currentValue = parsingContext.getCurrentValue();//前端注入的對象(ResDTO)
Field field = ReflectionUtils.getField(currentValue.getClass(), currentName); // 通過對象和屬性名獲取屬性的類型
// 獲取對應(yīng)得枚舉類
Class enumClass = field.getType();
// 根據(jù)對應(yīng)的值和枚舉類獲取相應(yīng)的枚舉值
BaseEnum anEnum = DefaultInputJsonToEnum.getEnum(inputParameter, enumClass);
log.info("\n====>測試反序列化枚舉[{}]==>[{}.{}]", inputParameter, anEnum.getClass(), anEnum);
return anEnum;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
效果就是反序列化器用來解決參數(shù)接受的時候,將前端傳過來的code轉(zhuǎn)成Enum.方便枚舉在程序中的操作,降低程序的復(fù)雜度,使編碼更加簡單,代碼清晰明了.
注入到SpringBoot框架中
@Bean
public Jackson2ObjectMapperBuilderCustomizer enumCustomizer() {
// 將枚舉轉(zhuǎn)成json返回給前端
return jacksonObjectMapperBuilder -> {
// 自定義序列化器注入
Map<Class<?>, JsonSerializer<?>> serializers = new LinkedHashMap<>();
serializers.put(BaseEnum.class, new BaseEnumSerializer());
jacksonObjectMapperBuilder.serializersByType(serializers);
// 自定義反序列化器注入,這里的注入貌似效果不行
Map<Class<?>, JsonDeserializer<?>> deserializers = new LinkedHashMap<>();
deserializers.put(BaseEnum.class, new BaseEnumDeserializer());
jacksonObjectMapperBuilder.deserializersByType(deserializers);
};
}
經(jīng)過測試,枚舉序列化后返回到前端的效果如下,與期望的效果一致,這樣的好處就是前端不需要管數(shù)字是啥意思,直接顯示description即可,無論后端枚舉是否修改,前端都不需要關(guān)心了。
經(jīng)過反復(fù)測試與人分享成果的時候,發(fā)現(xiàn)一個非常嚴(yán)重的問題,雖然前端接收參數(shù)的時候也可以反序列化成枚舉,但是實際上沒有按照code來反序列化。最后只能把jackson的源代碼拉下來調(diào)試,經(jīng)過調(diào)試發(fā)現(xiàn),jackson反序列化的時候一直使用的是默認(rèn)的枚舉反序列化器,并沒有使用自定義枚舉反序列化器。
com.fasterxml.jackson.databind.deser.BasicDeserializerFactory#createEnumDeserializer
/**
* Factory method for constructing serializers of {@link Enum} types.
*/
@Override
public JsonDeserializer<?> createEnumDeserializer(DeserializationContext ctxt,
JavaType type, BeanDescription beanDesc)
throws JsonMappingException
{
final DeserializationConfig config = ctxt.getConfig();
final Class<?> enumClass = type.getRawClass();
// 23-Nov-2010, tatu: Custom deserializer?
JsonDeserializer<?> deser = _findCustomEnumDeserializer(enumClass, config, beanDesc);
if (deser == null) {
// 12-Feb-2020, tatu: while we can't really create real deserializer for `Enum.class`,
// it is necessary to allow it in one specific case: see [databind#2605] for details
// but basically it can be used as polymorphic base.
// We could check `type.getTypeHandler()` to look for that case but seems like we
// may as well simply create placeholder (AbstractDeserializer) regardless
if (enumClass == Enum.class) {
return AbstractDeserializer.constructForNonPOJO(beanDesc);
}
ValueInstantiator valueInstantiator = _constructDefaultValueInstantiator(ctxt, beanDesc);
SettableBeanProperty[] creatorProps = (valueInstantiator == null) ? null
: valueInstantiator.getFromObjectArguments(ctxt.getConfig());
// May have @JsonCreator for static factory method:
for (AnnotatedMethod factory : beanDesc.getFactoryMethods()) {
if (_hasCreatorAnnotation(ctxt, factory)) {
if (factory.getParameterCount() == 0) { // [databind#960]
deser = EnumDeserializer.deserializerForNoArgsCreator(config, enumClass, factory);
break;
}
Class<?> returnType = factory.getRawReturnType();
// usually should be class, but may be just plain Enum<?> (for Enum.valueOf()?)
if (returnType.isAssignableFrom(enumClass)) {
deser = EnumDeserializer.deserializerForCreator(config, enumClass, factory, valueInstantiator, creatorProps);
break;
}
}
}
// Need to consider @JsonValue if one found
if (deser == null) {
deser = new EnumDeserializer(constructEnumResolver(enumClass,
config, beanDesc.findJsonValueAccessor()),
config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS));
}
}
// and then post-process it too
if (_factoryConfig.hasDeserializerModifiers()) {
for (BeanDeserializerModifier mod : _factoryConfig.deserializerModifiers()) {
deser = mod.modifyEnumDeserializer(config, type, beanDesc, deser);
}
}
return deser;
}
從上面可以看出來枚舉反系列化器是怎么找到的.仔細(xì)閱讀后發(fā)現(xiàn),上面并沒有按照接口 BaseEnum 來查找反序列化器,這也是為啥自定義的反序列化器沒有生效的原因.
既然我發(fā)現(xiàn)了這個問題,我直接在github拉下來了jackson代碼,然后修改成按照接口查找自定義反序列化器的方式提交了我的代碼.于是下面的代碼就來了
List<JavaType> interfaces = type.getInterfaces();
for (JavaType javaType : interfaces) {
Class<?> rawClass = javaType.getRawClass();
deser = _findCustomEnumDeserializer(rawClass, config, beanDesc);
if (deser != null) {
return deser;
}
}
pull request之后,管理者很快給我回復(fù)了。我們來回扯了幾個回合之后,我們得到一個更加合理的解決辦法. 這個問題,這個也是本文的重點。就是重寫查找枚舉反序列化器的方法,把我寫的代碼放在一個重寫類里面即可.
https://github.com/FasterXML/jackson-databind/pull/2842
依據(jù)開閉原則,修改源代碼的事情不太能發(fā)生,管理者說修改違背了原有的思想,所以我的PR最后被我自己關(guān)閉了。
com.fasterxml.jackson.databind.module.SimpleDeserializers#findEnumDeserializer
@Override
public JsonDeserializer<?> findEnumDeserializer(Class<?> type,
DeserializationConfig config, BeanDescription beanDesc)
throws JsonMappingException
{
if (_classMappings == null) {
return null;
}
JsonDeserializer<?> deser = _classMappings.get(new ClassKey(type));
if (deser == null) {
// 29-Sep-2019, tatu: Not 100% sure this is workable logic but leaving
// as is (wrt [databind#2457]. Probably works ok since this covers direct
// sub-classes of `Enum`; but even if custom sub-classes aren't, unlikely
// mapping for those ever requested for deserialization
if (_hasEnumDeserializer && type.isEnum()) {
deser = _classMappings.get(new ClassKey(Enum.class));
}
}
return deser;
}
從上面看出來這個就是查找枚舉反序列化器的邏輯,重寫SimpleDeserializers類即可.上面這個代碼是無法按照接口找到反序列化器的,所以重寫它,讓它按照我期望的接口方式找到即可,最后也成功了.
此外還從源碼中分析出來 為啥有的枚舉反序列化就能正常,但是有的不能完成翻序列化。原來默認(rèn)的枚舉反序列化器是按照ordinal來反序列化的,也就是說只有當(dāng)code與ordinal一致的時候就會造成一種假象, 以為是code反序列化來的,其實依舊是ordinal反序列化來的。
從下面代碼中可以看出來,枚舉存儲在數(shù)組中,而ordinal剛好是下標(biāo).
com.fasterxml.jackson.databind.deser.std.EnumDeserializer#deserialize
@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException
{
JsonToken curr = p.currentToken();
// Usually should just get string value:
if (curr == JsonToken.VALUE_STRING || curr == JsonToken.FIELD_NAME) {
CompactStringObjectMap lookup = ctxt.isEnabled(DeserializationFeature.READ_ENUMS_USING_TO_STRING)
? _getToStringLookup(ctxt) : _lookupByName;
final String name = p.getText();
Object result = lookup.find(name);
if (result == null) {
return _deserializeAltString(p, ctxt, lookup, name);
}
return result;
}
// But let's consider int acceptable as well (if within ordinal range)
if (curr == JsonToken.VALUE_NUMBER_INT) {
// ... unless told not to do that
int index = p.getIntValue();
if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS)) {
return ctxt.handleWeirdNumberValue(_enumClass(), index,
"not allowed to deserialize Enum value out of number: disable DeserializationConfig.DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS to allow"
);
}
if (index >= 0 && index < _enumsByIndex.length) {
return _enumsByIndex[index];
}
if ((_enumDefaultValue != null)
&& ctxt.isEnabled(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)) {
return _enumDefaultValue;
}
if (!ctxt.isEnabled(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL)) {
return ctxt.handleWeirdNumberValue(_enumClass(), index,
"index value outside legal index range [0..%s]",
_enumsByIndex.length-1);
}
return null;
}
return _deserializeOther(p, ctxt);
}
Java的枚舉本質(zhì)上是java.lang.Enum.class,自帶有ordinal和name兩個屬性。ordinal可以理解成數(shù)組的下標(biāo)。
調(diào)試過程中最讓人百思不得解的是,自定義的正反枚舉序列化器,序列化器是可以按照自己定義的接口來序列化,但是反序列化不行。最后經(jīng)過反復(fù)調(diào)試,發(fā)現(xiàn)正反序列化過程有點區(qū)別,正序列化的時候會找父類找接口,按照父類或者接口定義的序列化器來序列化。而反序列化的時候不會。體會一下,可以理解成一個正序列化的時候,準(zhǔn)確度可以忽略,反正都是丟出去的。但是反序列化的時候必須保證精度,否則無法正確反序列化,那么對應(yīng)的對象無法獲取到正確的值。瞎扯一下.好比,銀行存錢的時候不需要密碼,取錢的時候就需要密碼一樣,看似一個對稱的過程,但是校驗機制還是有點區(qū)別的,可以細(xì)細(xì)體會這種方式的必要性。
重寫SimpleDeserializers的findEnumDeserializer方法
重寫了這個方法之后,把我原本寫在源代碼的邏輯搬出來了,很快就解決了枚舉無法找到自定義反序列化器的問題。
public class SimpleDeserializersWrapper extends SimpleDeserializers {
static final Logger logger = LoggerFactory.getLogger(SimpleDeserializersWrapper.class);
@Override
public JsonDeserializer<?> findEnumDeserializer(Class<?> type, DeserializationConfig config, BeanDescription beanDesc) throws JsonMappingException {
JsonDeserializer<?> enumDeserializer = super.findEnumDeserializer(type, config, beanDesc);
if (enumDeserializer != null) {
return enumDeserializer;
}
for (Class<?> typeInterface : type.getInterfaces()) {
enumDeserializer = this._classMappings.get(new ClassKey(typeInterface));
if (enumDeserializer != null) {
logger.info("\n====>重寫枚舉查找邏輯[{}]",enumDeserializer);
return enumDeserializer;
}
}
return null;
}
}
換種方式注入到SpringBoot
放棄之前的注入方式,換用新的注入方式向jackson注冊重寫的類SimpleDeserializersWrapper。
@Bean
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
SimpleDeserializersWrapper deserializers = new SimpleDeserializersWrapper();
deserializers.addDeserializer(BaseEnum.class, new BaseEnumDeserializer());
SimpleModule simpleModule = new SimpleModule();
simpleModule.setDeserializers(deserializers);
simpleModule.addSerializer(BaseEnum.class, new BaseEnumSerializer());
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
objectMapper.registerModule(simpleModule);
return objectMapper;
}
時間等序列化
一般來說,會在Date上滿加上時間序列化的注解@JsonFormat,但是也可以針對Date自定義正反序列化器,就可以很輕松解決問題。
仔細(xì)閱讀jackson的源代碼你會發(fā)現(xiàn)這個還是里面有很多的默認(rèn)序列化器,用來解決一些常用的類型序列化.
表單提交的數(shù)據(jù)轉(zhuǎn)成枚舉
表單提交的數(shù)據(jù)與jackson沒有關(guān)系,主要與SpringWebMVC有關(guān)系,所以具體可以看工程源代碼,應(yīng)用比較簡單,但是底層原理可以看看Spring源代碼。表單提交的數(shù)據(jù)與jackson沒有關(guān)系,主要與SpringWebMVC有關(guān)系,所以具體可以看工程源代碼,應(yīng)用比較簡單,但是底層原理可以看看Spring源代碼。
DAO 層處理枚舉存到數(shù)據(jù)庫
具體就是在枚舉的屬性上面上一個注解
@EnumValue//標(biāo)記數(shù)據(jù)庫存的值是code
private final Integer code;
此外在yaml配置文件中指定枚舉所在的包。
mybatis-plus:
type-enums-package: hxy.dream.entity.enums
上面兩步,就是借助mybatis-plus完成了枚舉存儲到數(shù)據(jù)庫,與讀取的時候轉(zhuǎn)換的問題。這個比較簡單,框架也就是做這些事情的,讓開發(fā)者專注于業(yè)務(wù),而不是實現(xiàn)技術(shù)的本身(不是說不要鉆研技術(shù)底層原理)。
參考 mybatis-plus:https://mp.baomidou.com/guide/enum.html
總結(jié)
以上的操作完成了枚舉的從前端接收,反序列化成枚舉對象在程序中表達(dá)。然后再存儲到數(shù)據(jù)庫中。從數(shù)據(jù)庫中取code轉(zhuǎn)成枚舉,在程序中表達(dá),再序列化枚舉后傳輸給前端。一個非常完整的循環(huán),基本上滿足了程序中對枚舉使用的需求。
源碼 https://gitee.com/eric-tutorial/SpringCloud-multiple-gradle