# MyBatis 枚举全面使用指南

说明:禁止转载。

如果你在 MyBatis 使用了枚举,你可能对基本的用法已经熟悉。如果你从未用过枚举,看完本篇文章后,你可能会在某些情况会优先选择枚举。

MyBatis 中的枚举很好用,前提是会用!本篇文章会把 MyBatis 关于枚举的用法和最新版本中新增的功能统统告诉你,让你在使用枚举时更容易!

配套代码:

https://github.com/abel533/mybatis-enum

文章内所有链接都可以跳转至相关的代码。

# 1. 从最早版本的用法说起

MyBatis 从一开始就自带了两个枚举的类型处理器 EnumTypeHandlerEnumOrdinalTypeHandler,这两个枚举类型处理器可以用于最简单情况下的枚举类型。

为了方便下面的讲解,先假设有如下简单的枚举类型:

package tk.mybatis.enums.enumordinaltypehandler;

public enum Sex {
  MALE, FEMALE
}

# 1.1 EnumTypeHandler (opens new window)

这个类型处理器是 MyBatis 中默认的枚举类型处理器,他的作用是将枚举的名字和枚举类型对应起来。对于 Sex 枚举来说,存数据库时会使用 "MALE" 或者 "FEMALE" 字符串存储,从数据库取值时,会将字符串转换为对应的枚举。

# 1.2 EnumOrdinalTypeHandler (opens new window)

这是另一个枚举类型处理器,他的作用是将枚举的索引和枚举类型对应起来。对于 YesNoEnum 枚举来说,存数据库时会使用枚举对应的顺序 0(MALE) 或者 1(FEMALE) 存储,从数据库取值时,会将整型顺序号(int)转换为对应的枚举。

# 1.3 如何配置这两种枚举类型

因为 EnumTypeHandler 是默认的枚举处理器,所以默认不做任何配置的情况下,就使用的这个类型处理器,因此如果需要存储 "MALE" 或者 "FEMALE" 值,就不需要任何配置。

如果想存储枚举对应的索引,可以按照下面的方式进行配置:

<typeHandlers>
  <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"
               javaType="tk.mybatis.enums.enumordinaltypehandler.Sex"
               jdbcType="VARCHAR"/>
</typeHandlers>

还可以省略 jdbcType="VARCHAR" 属性,按下面方式进行配置:

<typeHandlers>
  <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"
               javaType="tk.mybatis.enums.enumordinaltypehandler.Sex"/>
</typeHandlers>

按上述方式进行配置后,就可以使用索引值存库,如果有很多枚举怎么办,难道还要一个个进行配置吗?目前有很多方式可以解决这个问题,不同 MyBatis 版本可以通过不同的方式去处理,后面会一一说明。

当你看到上面这两种配置的变化时,会不会有种心虚的感觉,以后遇到类似情况时会不会无从下手?

既然枚举用法只是类型处理器的一种,而类型处理器又存在着各种变化,我们不妨先深入看看类型处理器的处理过程。

本文主要的配置都使用的 mybatis-3-config 格式的配置文件,后续和 Spring 及 Spring Boot 集成部分也有相应的多种配置方式

# 2. 深入了解 TypeHandler 类型处理器

在开始深入了解之前,先看看上面 EnumOrdinalTypeHandler 处理器的部分代码:

public class EnumOrdinalTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {

  private final Class<E> type;
  private final E[] enums;

  public EnumOrdinalTypeHandler(Class<E> type) {
    if (type == null) {
      throw new IllegalArgumentException("Type argument cannot be null");
    }
    this.type = type;
    this.enums = type.getEnumConstants();
    if (this.enums == null) {
      throw new IllegalArgumentException(type.getSimpleName() + " does not represent an enum type.");
    }
  }
  //其他代码

在这个类型处理器中,只给了一个需要提供 javaType 类的构造方法,因此想要使用这个枚举类型处理器就必须提供具体的枚举类型,否则这里的枚举类型处理器就没法创建。

接下来透过源码来看看类型处理器配置的执行过程,在 XMLConfigBuilder 中通过 typeHandlerElement(root.evalNode("typeHandlers")) 解析 <typeHandlers> 标签配置的内容,代码如下:

private void typeHandlerElement(XNode parent) throws Exception {
    //当配置文件中存在 <typeHandlers> 配置时
    if (parent != null) {
      //遍历所有的 子节点配置
      for (XNode child : parent.getChildren()) {
        //如果子节点名称为 <package>,就按包导入所有的类型处理器
        if ("package".equals(child.getName())) {
          //获取配置的包名
          String typeHandlerPackage = child.getStringAttribute("name");
          //具体实现中会排除内部类,接口(包括 package-info.java)和抽象类
          //在具体注册时,会判断类上是否有 @MappedTypes({A.class, B.class}) 注解
          //该注解可以提供 javaTypeClass,存在 javaTypeClass 时优先使用带该参数的构造方法
          //如果创建失败就使用无参的构造方法
          //没有该注解时,使用无参的构造方法,如果不存在默认的构造方法就会抛出异常
          typeHandlerRegistry.register(typeHandlerPackage);
        } else {
          //获取 <typeHandler> 标签上的三个属性
          String javaTypeName = child.getStringAttribute("javaType");
          String jdbcTypeName = child.getStringAttribute("jdbcType");
          String handlerTypeName = child.getStringAttribute("handler");
          //下面三个 resolve 方法都允许参数 null 时返回 null
          Class<?> javaTypeClass = resolveClass(javaTypeName);
          JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
          Class<?> typeHandlerClass = resolveClass(handlerTypeName);
          //如果存在 javaType 属性配置
          if (javaTypeClass != null) {
            //如果没有设置 jdbcType 属性
            if (jdbcType == null) {
              typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
            } else {
              //三个属性都配置时
              typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
            }
          } else {
            //此处处理逻辑和上面 package 类似,会处理 @MappedTypes 注解
            typeHandlerRegistry.register(typeHandlerClass);
          }
        }
      }
    }
}

这里主要分析 <typeHandler> 配置时的区别,这些区别还要从 typeHandlerRegistry.register 不同的实现中查看具体的情况。

# 2.1 只有 typeHandlerClass

此时调用的 register 方法如下:

public void register(Class<?> typeHandlerClass) {
  boolean mappedTypeFound = false;
  MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
  if (mappedTypes != null) {
    for (Class<?> javaTypeClass : mappedTypes.value()) {
      register(javaTypeClass, typeHandlerClass);
      mappedTypeFound = true;
    }
  }
  if (!mappedTypeFound) {
    register(getInstance(null, typeHandlerClass));
  }
}

这里会判断是否存在 @MappedTypes 注解,存在时会根据配置的 javaTypeClass 循环进行注册,此时调用的方法和下面 2.2 中提供 javaTypeClass 时的逻辑相同,在下面 2.2 中在具体分析。

如果没有该注解,或者注解没有指定 javaTypeClass,此时会调用下面的 register 方法:

public <T> void register(TypeHandler<T> typeHandler) {
  boolean mappedTypeFound = false;
  MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class);
  if (mappedTypes != null) {
    for (Class<?> handledType : mappedTypes.value()) {
      register(handledType, typeHandler);
      mappedTypeFound = true;
    }
  }
  // @since 3.1.0 - try to auto-discover the mapped type
  if (!mappedTypeFound && typeHandler instanceof TypeReference) {
    try {
      TypeReference<T> typeReference = (TypeReference<T>) typeHandler;
      register(typeReference.getRawType(), typeHandler);
      mappedTypeFound = true;
    } catch (Throwable t) {
      // maybe users define the TypeReference with a different type and are not assignable, so just ignore it
    }
  }
  if (!mappedTypeFound) {
    register((Class<T>) null, typeHandler);
  }
}

这里仍然不死心的再次判断 @MappedTypes 注解,假如不存在该注解或者没有配置具体的类型,就会执行到第 2 个 if 判断,这里涉及到了一个特殊的类型 TypeReference<T>。这个类里面会通过反射的方式获取子类设置的泛型类型,以此类型作为 javaTypeClass,举一个常见的子类如下:

public class DateTypeHandler extends BaseTypeHandler<Date> {
  //省略
}

//上面继承的 BaseTypeHandler,BaseTypeHandler 继承了 TypeReference
public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> {
  //省略
}

也就是说配置 TypeReference<T> 的子类时,不提供 javaType 也可以,比如配置日期这个:

<typeHandlers>
  <typeHandler handler="org.apache.ibatis.type.DateTypeHandler"/>
</typeHandlers>

如果不了解这种细节,就很不容易完全掌握 <typeHandler> 的配置。

继续上面逻辑,如果不是 TypeReference<T> 的子类,就会使用 null,在后面逻辑中会看到,这种情况下,实际上没有注册上,也不会起到真正的作用。

# 2.2 当提供 typeHandlerClassjavaTypeClass

此时不会处理 @MappedTypes 注解,在 2.1 情况中如果存在 @MappedTypes 注解并且配置了参数,就会和这里往下的逻辑相同。这里的 register 方法如下:

public void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
  register(javaTypeClass, getInstance(javaTypeClass, typeHandlerClass));
}

这里会调用 getInstance 方法,通过 javaTypeClasstypeHandlerClass 参数去创建类型处理器的实例,实例化代码如下:

public <T> TypeHandler<T> getInstance(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
  //如果提供了 javaTypeClass
  if (javaTypeClass != null) {
    try {
      //优先通过有参数的构造方法创建
      Constructor<?> c = typeHandlerClass.getConstructor(Class.class);
      return (TypeHandler<T>) c.newInstance(javaTypeClass);
    } catch (NoSuchMethodException ignored) {
      //如果该类型处理器没有提供带参数的构造方法,忽略这个错误
    } catch (Exception e) {
      throw new TypeException("Failed invoking constructor for handler " + typeHandlerClass, e);
    }
  }
  try {
    //然后通过默认无参的构造方法实例化
    Constructor<?> c = typeHandlerClass.getConstructor();
    return (TypeHandler<T>) c.newInstance();
  } catch (Exception e) {
    //如果没有默认无参的构造方法就抛出异常
    throw new TypeException("Unable to find a usable constructor for " + typeHandlerClass, e);
  }
}

通过这里实例化的逻辑可以知道,前面 DateTypeHandler 中,如果配置了 javaType 反而会多一步根据有参数构造方法实例化的操作,而且会因为没有该构造方法而抛出异常(被忽略了),不提供才是最合适的配置方式。

再前面的 EnumOrdinalTypeHandler 中只有带参数的构造方法,如果不提供 javaType 就会因为没有默认无参的构造方法而抛出异常,所以使用这个类型处理器必须配置 javaType

继续看实例化后 register 方法的调用:

private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
  MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
  if (mappedJdbcTypes != null) {
    for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
      register(javaType, handledJdbcType, typeHandler);
    }
    if (mappedJdbcTypes.includeNullJdbcType()) {
      register(javaType, null, typeHandler);
    }
  } else {
    register(javaType, null, typeHandler);
  }
}

这里会处理 @MappedJdbcTypes 注解,如果存在该注解,就会带上 jdbcType 注册,否则 jdbcType 按照 null 处理。接下来调用的 register 方法就和 2.3 类似了,继续在 2.3 中分析。

# 2.3 当这三个属性都提供时

此时会按照 2.2 中的逻辑实例化类型处理器,然后调用最终的 register 方法:

private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
  if (javaType != null) {
    Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
    if (map == null || map == NULL_TYPE_HANDLER_MAP) {
      map = new HashMap<JdbcType, TypeHandler<?>>();
      TYPE_HANDLER_MAP.put(javaType, map);
    }
    map.put(jdbcType, handler);
  }
  ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
}

从这段代码,我们可以看到 TYPE_HANDLER_MAP 存储了最终的配置信息,该 MAP 的数据结构类似下面的 JSON 形式:

{
  javaType1: {
    jdbcType1: handler,
    jdbcType2: handler,
    jdbcType3: handler
  },
  javaType2: {
    jdbcType1: handler,
    jdbcType2: handler
  },
  ...
}

这个结构是因为一个 Java 类型可以对应数据库中的不同类型,以最常见的 java.lang.String 为例,初始化的情况下,该类的结构如下:

{
  java.lang.String: {
    null:        StringTypeHandler,
    CLOB:        StringTypeHandler,
    LONGVARCHAR: StringTypeHandler,
    VARCHAR:     StringTypeHandler,
    NCLOB:       StringTypeHandler,
    CHAR:        StringTypeHandler,
    NCHAR:       StringTypeHandler,
    NVARCHAR:    StringTypeHandler
  }
}

从这个结构和上面的逻辑中都能看出,javaType 是必须提供的,否则不会起作用,而提供 javaType 的方式总结如下:

  1. 通过 <typeHandler> 配置 javaType
  2. 通过 @MappedTypes 注解指定 javaType,并且可以指定多个(例如指定多个相同处理规则的枚举类型);
  3. TypeHandlerTypeReference<T> 的子类,在子类中指定泛型的具体类型;

现在了解如何配置和存储类型处理器的配置了,下面看看最关键的部分,MyBatis 如何查找某个类型对应的类型处理器。

# 2.4 查找类型处理器

TypeHandlerRegistry#getTypeHandler 如下方法中是主要的查找类型处理器的逻辑(参考上面 JSON 串来看)。

private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
  if (ParamMap.class.equals(type)) {
    return null;
  }
  //根据 java 类型获取 jdbcHandlerMap, 例如上面 java.lang.String 对应的 Map
  //后面 4.1 会详细介绍 getJdbcHandlerMap 方法
  Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
  TypeHandler<?> handler = null;
  //先判断该类型是否有 jdbcHandlerMap,对于复杂类型,一般都是 null
  if (jdbcHandlerMap != null) {
    //先根据 jdbcType 类型取
    handler = jdbcHandlerMap.get(jdbcType);
    //如果对应的 jdbcType 类型没有配置
    if (handler == null) {
      //取出 null 对应的处理器
      handler = jdbcHandlerMap.get(null);
    }
    //如果没有 null 对应的处理器
    if (handler == null) {
      // #591
      //这里的方法会判断 jdbcHandlerMap 里面所有配置的处理器是不是都是同一个
      //如果只有 1 个,或者都是同一个,基本上可以认为就该使用这个处理器
      handler = pickSoleHandler(jdbcHandlerMap);
    }
  }
  // type drives generics here
  return (TypeHandler<T>) handler;
}

这里来归纳一下上面的逻辑:

  1. 优先使用指定 jdbcType 配置的处理器
  2. 其次使用 null 对应的处理器
  3. 最后使用唯一(pickSole) 配置的处理器,不唯一时返回 null

通过该逻辑可以看出来,除非 javaType 对应的多个 jdbcType 使用不同的类型处理器,否则不要设置 jdbcType。或者在配置 jdbcType 的同时,通过额外增加一个不配置的来作为默认的处理器(null 对应的默认处理器,对应上面逻辑2)。

如果你看完了第二部分的内容,看到类型处理器的配置时,你会知道为什么这么配置,也会知道原来有这么多种配置方式。

在上面代码中,还有一个很重要的方法 getJdbcHandlerMap,这个方法会在 4.1 中详细介绍。

# 3. 配置类型处理器

在 1.3 节中,我们已经配置了类型处理器,在那两个配置中,提供 jdbcType 的那个配置是不合理的,一般情况下,都不要配置 jdbcType

下面介绍所有配置(枚举)类型的方法。

# 3.1 全局配置

全局的配置会对所有使用使用该类型的地方生效,这些位置包含:

  1. 查询结果的映射
  2. INSERT 的 values 参数
  3. UPDATE 的 set 参数
  4. CRUD 方法的查询条件中,2,3,4 都是 #{attr} 方式

全局配置也有很多具体的配置方法,下面一一介绍。

# 3.1.1 <typeHandler> 方式 (opens new window)

和 1.3 第二种一样,配置如下:

<typeHandlers>
  <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"
               javaType="tk.mybatis.enums.enumordinaltypehandler.Sex"/>
</typeHandlers>

增加上面配置后,对枚举的处理,就不使用默认的 EnumTypeHandler 存名字,而使用 EnumOrdinalTypeHandler 存枚举的索引值。

# 3.1.2 <package> 方式 (opens new window)

在 1.3 中提出了一个问题:如果有很多枚举怎么办,难道还要一个个进行配置吗?

如果使用的是低版本(3.4.4 或更低版本),确实还需要配置,但是也不至于一个个都配置,如果项目是以多个模块进行开发,还需要考虑引入其他项目后,如果避免复制粘贴的问题。

通过下面的配置方式,就能统一处理这种类型的问题。

首先,可以规定公司项目中所有类型处理器都放在同一个包名下(可以在不同 jar 包),例如 com.company.typehandler 包中。

在 mybatis-config.xml 按如下配置:

<typeHandlers>
  <package name="com.company.typehandler"/>
</typeHandlers>

通过该配置就可以让 MyBatis 固定扫描某个包,然后处理其中符合要求的类型处理器。这个包下面的类型处理器,需要按下面的方式使用,比如仍然是枚举的 EnumOrdinalTypeHandler

package com.company.typehandler.module1;
//省略import
@MappedTypes({Sex.class})
public class MyEnumOrdinalTypeHandler extends EnumOrdinalTypeHandler {
  public MyEnumOrdinalTypeHandler(Class type) {
    super(type);
  }
}

为了能在类上增加 @MappedTypes({Sex.class}) 注解,这里简单继承了 EnumOrdinalTypeHandler 类。如果你当前项目有多个枚举都需要按这种方式进行处理,都配置到 @MappedTypes 注解中即可。例如:

@MappedTypes({Sex.class, YESNO.class, State.class})
public class MyEnumOrdinalTypeHandler extends EnumOrdinalTypeHandler {

特别注意,MyBatis 会搜索包下和子包下的所有 TypeHandler 的子类,因此你可以通过不同包名区分不同的 TypeHandler,或者用不同的名字进行区分,切记不要存在包名和类名都完全一致的情况。

通过上面这种方式,虽然还需要一些配置,但是已经足够灵活了。除了上面全局配置的方式外,可能还有需要局部针对性配置的时候,下面看看所有局部配置的方式。

# 3.2 局部配置 (opens new window)

根据前面全局配置生效的 4 种位置,就会有很多种不同的局部配置方式。

# 3.2.1 查询结果的映射

结果映射根据构造方法注入和属性注入就会有两种情况,使用 XML 和注解又会有两种,算下来就有 4 中情况。

  1. 使用注解构造参数方式时
    @ConstructorArgs({
        @Arg(column = "id", javaType = Integer.class, id=true),
        @Arg(column = "name", javaType = String.class),
        @Arg(column = "sex", javaType = Sex.class, typeHandler = EnumOrdinalTypeHandler.class),
    })
    @Select("select * from users where id = #{id}")
    User getUser3(Integer id);
    
  2. 使用注解属性赋值时
    @Results({
        @Result(column = "id", property = "id", javaType = Integer.class, id=true),
        @Result(column = "name", property = "name", javaType = String.class),
        @Result(column = "sex", property = "sex", javaType = Sex.class, typeHandler = EnumOrdinalTypeHandler.class),
    })
    @Select("select * from users where id = #{id}")
    User getUser4(Integer id);
    
  3. 使用 XML 构造参数时
    <resultMap id="map1" type="tk.mybatis.enums.result.User">
      <constructor>
        <idArg column="id" javaType="int"/>
        <arg column="name" javaType="string"/>
        <arg column="sex" javaType="tk.mybatis.enums.result.Sex"
             typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
      </constructor>
    </resultMap>
    
    <select id="getUser1" resultMap="map1">
      select * from users where id = #{id}
    </select>
    
  4. 使用 XML 属性赋值时
    <resultMap id="map2" type="tk.mybatis.enums.result.User">
      <id column="id" property="id" javaType="int"/>
      <result column="name" property="name" javaType="string"/>
      <result column="sex" property="sex" javaType="tk.mybatis.enums.result.Sex"
              typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
    </resultMap>
    
    <select id="getUser2" resultMap="map2">
      select * from users where id = #{id}
    </select>
    

上述 4 种配置方式就是按部就班的配置了一个 typeHandler 属性,没有特别的地方。 反而是构造参数方式和属性注入方式的差别更大,要特别注意,使用构造参数时,列的顺序必须和构造参数中的属性顺序一致,这种情况下不存在 property 属性,完全按照配置列的顺序和构造参数进行匹配,使用属性方式配置时,必须提供 property 属性,否则列和属性无法匹配。

# 3.2.2 #{attr} 方式

前面都是返回值的映射,这里是参数值的映射,相比注解或者 XML 语法带来的自动提示,完全手写的 #{attr} 显的不那么直观,你知道 #{attr} 一共支持多少额外配置的参数吗?

官方文档 Parameters (opens new window) 这里根据用法对所有可用参数进行了分类,这部分的设计对应在 SqlSourceBuilder#buildParameterMapping 方法,我们这里主要看 typeHandler 属性的用法。

例如,在 insert 方法的参数中使用,代码如下:

<insert id="insertUser">
  insert into users (id, name, sex) values (
    #{id},
    #{name},
    #{sex, javaType=tk.mybatis.enums.result.Sex,
           typeHandler=org.apache.ibatis.type.EnumOrdinalTypeHandler}
  )
</insert>

SqlSourceBuilder#buildParameterMapping 方法中,首先判断设置所有参数值,最后如果存在 typeHandler 配置,还有下面的逻辑:

if (typeHandlerAlias != null) {
  builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
}

可以看到这里是根据 javaTypetypeHandlerAlias(=typeHandler) 实例化的类型处理器,所以如果 typeHandler 需要 javaType 时,该属性必须配置。

你也看到这里列举的属性级别的配置方式有多麻烦了,虽然全局配置相对方便,但是也不可避免的要配置或实现很多类型处理器,为了让最常用的枚举好用,MyBatis 新版本增加了新的配置方式。

# 4. MyBatis 3.4.5+

在 MyBatis 3.4.3 的更新日志中,有如下的增强功能:

由于 3.4.3 的 JAR 有问题并且已经上传到 Maven 库,在当天又发布了 3.4.4 版本。

但是在 3.4.3 和 3.4.4 版本中的这个功能有问题,这个问题在 3.4.5 中修正了,下面是 3.4.5 版本中和枚举相关的更新内容:

Enhancements:

Bug fixes:

在 3.4.5 中,增加了可配置的默认枚举处理器,还解决了 3.4.3 中类型处理器公共接口的问题。

下面我们先看看这几处更新最关键的代码变化,通过源码来精确掌握新功能的用法。

# 4.1 根据更新代码变化来学习新功能

下面通过对比 3.2.8 和 3.4.5 中获取类型处理器的代码来看如何让默认枚举处理器变成可配置。

为了方便阅读这里的代码,先把 2.3 中的 TYPE_HANDLER_MAP 的一个示例的结构复制过来:

{
  java.lang.String: {
    null:        StringTypeHandler,
    CLOB:        StringTypeHandler,
    LONGVARCHAR: StringTypeHandler,
    VARCHAR:     StringTypeHandler,
    NCLOB:       StringTypeHandler,
    CHAR:        StringTypeHandler,
    NCHAR:       StringTypeHandler,
    NVARCHAR:    StringTypeHandler
  }
}

下面代码中的 jdbcHandlerMap 结构类似下面这样:

{
  null:        StringTypeHandler,
  CLOB:        StringTypeHandler,
  LONGVARCHAR: StringTypeHandler,
  VARCHAR:     StringTypeHandler,
  NCLOB:       StringTypeHandler,
  CHAR:        StringTypeHandler,
  NCHAR:       StringTypeHandler,
  NVARCHAR:    StringTypeHandler
}

# 4.1.1 MyBatis 3.2.8 版本代码

@SuppressWarnings("unchecked")
private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
  //获取类型配置的处理器
  Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = TYPE_HANDLER_MAP.get(type);
  TypeHandler<?> handler = null;
  //如果存在配置
  if (jdbcHandlerMap != null) {
    //优先按照 jdbcType 取对应的处理器
    handler = jdbcHandlerMap.get(jdbcType);
    //如果没有针对 jdbcType 配置的类型处理器
    if (handler == null) {
      //就使用默认 null 的类型处理器,这里可以参考前面 2.3 中的结构
      handler = jdbcHandlerMap.get(null);
    }
  }
  //如果没有提供类型处理器,并且当前类型是枚举
  if (handler == null && type != null && type instanceof Class && Enum.class.isAssignableFrom((Class<?>) type)) {
    //这里写死的 EnumTypeHandler,所以这是默认值
    handler = new EnumTypeHandler((Class<?>) type);
  }
  // type drives generics here
  return (TypeHandler<T>) handler;
}

# 4.1.2 MyBatis 3.4.5 版本代码

//新增的默认枚举处理器字段,只有 setter 方法
private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;

//下面这个方法在前面 2.4 查找类型处理器中有详细说明,这里简化该方法代码
@SuppressWarnings("unchecked")
private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
  //省略前面
  //这里关注提取出来的 getJdbcHandlerMap 方法
  Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
  //省略后面
  return (TypeHandler<T>) handler;
}

//根据类型获取 JdbcHandlerMap,参考前面 2.3 中的 Map 结构
private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMap(Type type) {
  //获取类型对应的处理器 Map
  Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = TYPE_HANDLER_MAP.get(type);
  //为了高效处理 null 的情况,在第一次 null 之后,就会用 NULL_TYPE_HANDLER_MAP 代替结果
  //这样可以避免反复执行下面第二个 if 的逻辑
  if (NULL_TYPE_HANDLER_MAP.equals(jdbcHandlerMap)) {
    return null;
  }
  //如果类型处理器不存在
  if (jdbcHandlerMap == null && type instanceof Class) {
    Class<?> clazz = (Class<?>) type;
    //如果是个枚举类型
    if (clazz.isEnum()) {
      //枚举类型只能有父接口,所以这里会递归获取枚举所有接口配置的类型处理器
      //后面 4.3 中介绍的公共接口和这里有关 ①
      jdbcHandlerMap = getJdbcHandlerMapForEnumInterfaces(clazz, clazz);
      if (jdbcHandlerMap == null) {
        //如果枚举接口中不存在类型处理器配置,就使用默认的枚举类型处理器
        //注册枚举类型和默认枚举类型处理器 ②
        register(clazz, getInstance(clazz, defaultEnumTypeHandler));
        //获取注册后的结果
        return TYPE_HANDLER_MAP.get(clazz);
      }
    } else {
      //如果不是枚举类型,就递归查找所有父类是否配置了类型处理器 ③
      jdbcHandlerMap = getJdbcHandlerMapForSuperclass(clazz);
    }
  }
  //为了高效处理 null 的情况,这里用 NULL_TYPE_HANDLER_MAP 替换了 null,这里和当前方法第一个 if 对应
  TYPE_HANDLER_MAP.put(type, jdbcHandlerMap == null ? NULL_TYPE_HANDLER_MAP : jdbcHandlerMap);
  return jdbcHandlerMap;
}

# 4.1.3 对比版本变化

通过上面两个版本的代码对比,基本上就能掌握新功能的用法,这里来总结一下新版本的变化。

  1. 增加对枚举接口类型处理器的支持,如果枚举继承了某个接口,可以按接口配置类型处理器。如果有很多不同枚举继承了相同的接口,可以统一按照接口的配置进行处理。
  2. 增加 defaultEnumTypeHandler 配置,使得默认的枚举类型处理器可配置。 特别注意的是,可配置并不是让你在 EnumTypeHandlerEnumOrdinalTypeHandler 之间做出选择,你自己也可以实现一个枚举类型处理器来处理。
  3. 增加对普通类型继承形式的支持,如果某个类的父类配置了类型处理器,该类型处理器默认对所有子类有效(子类没有配置时)。

现在我们已经知道了这 3 种新的用法,接下来就通过具体的例子来说明如何使用。

# 4.2 枚举接口的用法 (opens new window)

在看这种用法前,我们在仔细看看上面第一种情况时,调用的 getJdbcHandlerMapForEnumInterfaces 方法:

private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMapForEnumInterfaces(Class<?> clazz, Class<?> enumClazz) {
  //初次调用时,第一个参数类是枚举类,递归调用时,第一个参数类是父接口
  //迭代获取类的所有接口
  for (Class<?> iface : clazz.getInterfaces()) {
    //判断接口是否存在类型处理器配置,根据这里的逻辑,就要求我们必须配置 javaType 参数,
    //否则根据接口无法获取到 jdbcHandlerMap
    Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = TYPE_HANDLER_MAP.get(iface);
    //如果不存在
    if (jdbcHandlerMap == null) {
      //递归获取父接口的类型处理器
      jdbcHandlerMap = getJdbcHandlerMapForEnumInterfaces(iface, enumClazz);
    }
    //如果存在配置
    if (jdbcHandlerMap != null) {
      // Found a type handler regsiterd to a super interface
      HashMap<JdbcType, TypeHandler<?>> newMap = new HashMap<>();
      //迭代接口配置,放到当前枚举对应的 newMap 中
      for (Entry<JdbcType, TypeHandler<?>> entry : jdbcHandlerMap.entrySet()) {
        // Create a type handler instance with enum type as a constructor arg
        //特别注意,实例化类型处理器时,会优先调用有 Class 参数的构造方法,此时类型处理器能获得枚举的信息
        //如果不存在有 Class 参数的构造方法,就会调用默认无参的构造方法,这种情况下不知道枚举类型,无法映射查询结果到对应的枚举类型
        //因此,正常情况下,枚举接口对应的类型处理器都需要有 Class 参数的构造方法
        newMap.put(entry.getKey(), getInstance(enumClazz, entry.getValue().getClass()));
      }
      return newMap;
    }
  }
  return null;
}

从代码分析可以看出来,距离最近并且靠前的接口优先级最高,找不到的情况下才会找接口的父接口。只要有第一个存在的 jdbcHandlerMap 就会返回结果,不会判断所有的接口。

从上面大段注释的地方也可以看出来,我们必须提供一个带有 Class 参数的构造方法,通过构造参数来接收传递进来的枚举类型。

下面针对当前的用法提供一个具体的例子,在这个例子中有一个用户表(user),包含 4 个字段,主键 id,姓名 name,性别 sex,星座 constellations。其中性别(Sex)和星座(Constellations)是枚举类型,这两个枚举类型会按照相同的逻辑进行处理。这里使用的 User 对象如下:

public class User {

  private Integer id;

  private String name;

  private Sex sex;

  private Constellations constellations;

  //省略 getter 和 setter

下面开始创建所有接口和实现。

# 4.2.1 定义接口

定义 LabelValue 接口如下:


public interface LabelValue {

  String getLabel();

  Integer getValue();

}

该接口定义了两个取值的方法,这个接口只是个例子,看完全文后,你应该可以根据自己的需要进行相应的设计,示例枚举接口方法的具体作用看下面的枚举实现。

# 4.2.2 定义两个枚举类

性别 Sex 枚举:

public enum Sex implements LabelValue{
    MALE(1),
    FEMALE(2);

    private Integer value;

    Sex(Integer value) {
        this.value = value;
    }


    @Override
    public String getLabel() {
        return name();
    }

    @Override
    public Integer getValue() {
        return value;
    }

}

星座 Constellations 枚举:

public enum Constellations implements LabelValue {
  WaterCarrier(1),
  Fish(2),
  Ram(3),
  Bull(4),
  Twins(5),
  Crab(6),
  Lion(7),
  Virgin(8),
  Scales(9),
  Scorpion(10),
  Archer(11),
  Goat(12);

  private Integer value;

  Constellations(Integer value) {
    this.value = value;
  }

  @Override
  public String getLabel() {
    return name();
  }

  @Override
  public Integer getValue() {
    return value;
  }

}

这两个枚举都继承了 LabelValue 接口,getLabel 方法返回枚举名,getValue 方法返回枚举值。

下面我们针对 LabelValue 接口实现一个 TypeHandler,使得我们可以将 value 值存入数据库,查询时根据 value 值映射到枚举类型。

# 4.2.3 实现接口对应的类型处理器

在实现类型处理器时,我们不需要从头去实现,最好的方法就是参考 MyBatis 已经提供的大量类型处理器。

比如在这里,我们参考 EnumOrdinalTypeHandler 就能轻松实现一个 LabelValueTypeHandler

//泛型是 E extends LabelValue
//如果这里增加下面的配置,在配置时就不需要指定 javaType,很明显这种情况下使用注解最方便
//注解和 javaType 的关系参考 2.1 节
@MappedTypes(LabelValue.class)
public class LabelValueTypeHandler<E extends LabelValue> extends BaseTypeHandler<E> {
  private Class<E> type;
  //记录枚举值和枚举的对应关系
  private Map<Integer, E> enumMap;

  public LabelValueTypeHandler(Class<E> type) {
    if (type == null) {
      throw new IllegalArgumentException("Type argument cannot be null");
    }
    this.type = type;
    E[] enums = type.getEnumConstants();
    //配置到 <typeHandler> 初始化时,这里的 type 只是一个接口,并不是枚举,所以要特殊判断
    //下面除了第一个 setNonNullParameter 赋值不需要 enumMap,其他 3 个都需要,
    //由于正常情况下实体类不会使用 LabelValue 接口类型,所以这里没有对 null 进行特殊处理
    if (enums != null) {
      //这里将枚举值和枚举类型存入 enumMap,方便后续快速取值
      this.enumMap = new HashMap<>(enums.length);
      for (int i = 0; i < enums.length; i++) {
        this.enumMap.put(enums[i].getValue(), enums[i]);
      }
    }
  }

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
    ps.setInt(i, parameter.getValue());
  }

  @Override
  public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
    int i = rs.getInt(columnName);
    if (rs.wasNull()) {
      return null;
    } else {
      try {
        //取出值对应的枚举类
        return enumMap.get(i);
      } catch (Exception ex) {
        throw new IllegalArgumentException("Cannot convert " + i + " to " + type.getSimpleName() + " by ordinal value.", ex);
      }
    }
  }

  @Override
  public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
    int i = rs.getInt(columnIndex);
    if (rs.wasNull()) {
      return null;
    } else {
      try {
        //取出值对应的枚举类
        return enumMap.get(i);
      } catch (Exception ex) {
        throw new IllegalArgumentException("Cannot convert " + i + " to " + type.getSimpleName() + " by ordinal value.", ex);
      }
    }
  }

  @Override
  public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
    int i = cs.getInt(columnIndex);
    if (cs.wasNull()) {
      return null;
    } else {
      try {
        //取出值对应的枚举类
        return enumMap.get(i);
      } catch (Exception ex) {
        throw new IllegalArgumentException("Cannot convert " + i + " to " + type.getSimpleName() + " by ordinal value.", ex);
      }
    }
  }

}

在上面实现代码中,和参考类 EnumOrdinalTypeHandler 有区别的地方都增加了注释。可以看到实现一个特殊的类型处理器也是很容易的。

# 4.2.4 配置类型处理器

接下来,我们还需要在 mybatis-config.xml 中配置 LabelValueTypeHandler

<typeHandlers>
  <typeHandler handler="tk.mybatis.enums.enuminterfaces.LabelValueTypeHandler"/>
</typeHandlers>

还记得 4.2 开头代码中的注释吗,这里摘抄过来:

//判断接口是否存在类型处理器配置,根据这里的逻辑,就要求我们必须配置 javaType 参数,
//否则根据接口无法获取到 jdbcHandlerMap
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = TYPE_HANDLER_MAP.get(iface);

由于此处要根据接口类型获取类型处理器,因此上面配置 <typeHandler> 时,需要提供 javaType 对应的接口 LabelValue,否则这里会因为找不到而无法使用。

为什么上面没有配置 javaType 呢?

因为按照前面 2.1 中的逻辑,除了配置 javaType 外,还可以通过 @MappedTypes(LabelValue.class) 注解指定 javaType,因此配置会更简单,在前面的 4.2.3 中的实现就提供了该注解,所以此处不需要 javaType

这里配置完成后,这两个枚举类型都可以正常的转换了。

到这里时,应该觉得 MyBatis 的枚举已经很好用了,这里的 <typeHandler> 配置就是全局的,所有相同处理方式的枚举只需要继承该接口即可,不管多少接口,多少模块(jar),一个固定的配置就搞定了,不需要针对各个枚举进行特殊配置,MyBatis 的枚举已经很好用了。

下面继续看默认枚举类型处理器的用法。

# 4.3 默认枚举处理器 (opens new window)

MyBatis 增加 defaultEnumTypeHandler 配置,使得默认的枚举类型处理器可配置。配置方法如下:

<configuration>
  <settings>
    <setting name="defaultEnumTypeHandler" value="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
  </settings>

  <!-- 其他配置 -->
</configuration>

通过 <setting> 配置 defaultEnumTypeHandler 后,配置的值最终覆盖了 TypeHandlerRegistry 中的 defaultEnumTypeHandler 字段。

按照前面分析的逻辑,如果枚举类型的接口不存在对应的枚举类型处理器(不走 4.2 中的方式),才会使用 defaultEnumTypeHandler 方式,所以在枚举没有特殊配置的情况下,现在都会使用 EnumOrdinalTypeHandler 类型处理器了。

MyBatis 的默认枚举处理器并不是让你在 EnumTypeHandlerEnumOrdinalTypeHandler 之间做出选择,你自己也可以实现一个枚举类型处理器来处理。

针对 4.2 中的示例,我们还有一种使用默认枚举处理器的方式。配置方式如下:

<configuration>
  <settings>
    <setting name="defaultEnumTypeHandler" value="tk.mybatis.enums.enuminterfaces2.LabelValueTypeHandler"/>
  </settings>

  <!-- 其他配置 -->
</configuration>

**注意:**这里没有 4.2 中的 <typeHandler> 配置,因为这个配置优先级高于默认枚举处理器。

这种配置方式也很简单,当存在多种枚举转换策略时,可以将 4.2 和 4.3 中的方式结合使用,MyBatis 中的枚举真的很好用了!

到这里 MyBatis 中枚举的用法就结束了,但是上面还提到了继承形式的类型处理器,虽然和枚举无关,这里也举个简单例子说明一下。

# 4.4 继承形式的类型处理器

继承形式的类型处理器是指下面这种情况,例如 java.util.Date 存在默认的类型处理器,但是我实现了一个 MyDate extends Date 类型,这种情况下 MyDate 就会使用 Date 对应的类型处理器。

这种用法可能存在,但是很少见,所以对于这种用法,你知道是怎么回事就可以了,这里也不深究了。

本文前面都在 mybatis-config.xml 配置文件配置的,下面开始进入和 Spring 集成相关的部分。

# 5. Spring 集成

由于 Spring 和 Spring Boot 配置方式不同,这里分别讲解各自的配置方式。

# 5.1 Spring XML 配置 (opens new window)

Spring 集成 MyBatis 的时候,配置 SqlSessionFactoryBean 时如果指定 configLocation,就可以用 MyBatis 配置方式配置,示例如下:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource"/>
  <property name="configLocation" value="classpath:tk/mybatis/enums/spring/mybatis-config.xml"/>
</bean>

如果你真用的这种方式,也没任何问题,如果我只提供这么一个方案就有点说不过去了。下面看看纯 Spring 的配置方式:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource"/>
  <property name="typeHandlers">
    <array>
      <!--很明显这里没法配置 `javaType`,因此必须使用 @MappedTypes(LabelValue.class) 注解-->
      <bean class="tk.mybatis.enums.spring.LabelValueTypeHandler">
        <!--由于没有默认无参的构造方法,所以只能指定构造参数,实际上给这个类型处理器提供一个无参的构造方法会更简单,也没有错-->
        <constructor-arg index="0" value="tk.mybatis.enums.spring.LabelValue"/>
      </bean>
    </array>
  </property>
</bean>

SqlSessionFactoryBean 提供了 typeHandlers 属性配置,因此可以用这种方式进行配置。但是这种方式配置有点特殊的地方,在用这种方式时,无法指定 javaType 类型,因此上面的例子用了 @MappedTypes(LabelValue.class) 注解。 除此之外,配置 LabelValueTypeHandler 因为构造方法的原因,要多配置一个 <constructor-arg>,如果增加一个默认无参的构造方法,例如下面的代码:

@MappedTypes(LabelValue.class)
public class LabelValueTypeHandler<E extends LabelValue> extends BaseTypeHandler<E> {
  private Class<E> type;
  private Map<Integer, E> enumMap;

  public LabelValueTypeHandler() {
  }

  public LabelValueTypeHandler(Class<E> type) {
    //省略其他
  }

此时上面的配置可以简化为:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource"/>
  <property name="typeHandlers">
    <array>
      <bean class="tk.mybatis.enums.spring.LabelValueTypeHandler"/>
    </array>
  </property>
</bean>

这些都是技巧,结合本文所有示例看 2. 深入了解 TypeHandler 类型处理器 时会加深理解,对这种用法会了解的更多。

除了 typeHandlers 配置外,SqlSessionFactoryBean 还提供了 typeAliasesPackage 配置类型处理器的包名,通过扫描包获取包下所有类型处理器,示例如下:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource"/>
  <property name="typeHandlersPackage" value="tk.mybatis.enums.spring"/>
</bean>

如果想在纯 Spring 配置情况下配置默认枚举类型处理,可以用下面的方式:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource"/>
  <property name="configuration">
    <bean class="org.apache.ibatis.session.Configuration">
      <property name="defaultEnumTypeHandler" value="tk.mybatis.enums.spring.LabelValueTypeHandler"/>
    </bean>
  </property>
</bean>

针对 Spring XML 配置方式就这几种,下面看看 Spring Boot 如何配置。

# 5.2 Spring Boot 配置 (opens new window)

和 Spring 类似,如果通过下面配置指定了 mybatis-config.xml 配置文件,就和 MyBatis 配置完全一样。

mybatis.config-location=classpath:tk/mybatis/enums/springboot/mybatis-config.xml

还可以直接配置 mybatis.type-handlers-package 如下:

mybatis.type-handlers-package=tk.mybatis.enums.springboot

还可以按下面的方式配置默认枚举类型处理器:

mybatis.configuration.default-enum-type-handler=tk.mybatis.enums.springboot.LabelValueTypeHandler

如果想要按照类似 mybatis-config.xml 方式配置 <typeHandler>,你就需要使用下面的方式:

@Configuration
public class MyBatisConfig {
  @Bean
  public ConfigurationCustomizer configurationCustomizer(){
    return new ConfigurationCustomizer() {
      @Override
      public void customize(org.apache.ibatis.session.Configuration configuration) {
        //在这里可以对 configuration 进行多种操作
        TypeHandlerRegistry registry = configuration.getTypeHandlerRegistry();
        registry.register(tk.mybatis.enums.springboot.LabelValueTypeHandler.class);
      }
    };
  }
}

其中 ConfigurationCustomizer 在 mybatis 官方 starter 为 org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer,在通用 Mapper 中是 tk.mybatis.mapper.autoconfigure.ConfigurationCustomizer

通过 ConfigurationCustomizer 可以做的事情更多,例如添加拦截器,设置所有 <settings> 中的提供的配置等等。

# 5.3 Spring MVC 中的枚举转换 (opens new window)

本部分内容针对 Spring 默认 JSON 处理器 jackson

到目前位置,DAO 层可以很愉快的使用枚举和数据库进行交互了,但是许多情况下,这些枚举类型还要在前后台之间进行转换。就目前网上可以找到的大部分资料中,很少能看到一个简单的方法针对所有枚举类型进行转换,往往需要配置所有的枚举类型,或者在枚举类型中提供特殊的方法。

例如最常见的一种策略如下:

public enum Sex implements LabelValue {
    MALE(1),
    FEMALE(2);

    private Integer value;

    Sex(Integer value) {
        this.value = value;
    }


    @Override
    public String getLabel() {
        return name();
    }

    @JsonValue
    @Override
    public Integer getValue() {
        return value;
    }

    @JsonCreator
    public static Sex fromValue(Integer value){
        Sex[] values = Sex.values();
        for (int i = 0; i < values.length; i++) {
            if (values[i].value.equals(value)) {
                return values[i];
            }
        }
        return null;
    }
}

在原来的 Sex 枚举中增加了序列化的 @JsonValue 注解,反序列化的 @JsonCreator 注解,如果每个枚举类都按照这种方式配置也可以轻松实现(这种方式比每个枚举单独实现 JsonSerializerJsonDeserializer 方便)。

我们肯定不会满足与此,偷懒是程序员的美德,能否通过实现 JsonSerializerJsonDeserializer 来统一处理枚举呢?

通过 JsonSerializer 利用接口倒是很容易实现序列化操作:

/**
 * 处理 LabelValue 子类的序列化
 */
public static class Serializer extends JsonSerializer<LabelValue> {
  @Override
  public void serialize(LabelValue labelValue, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
          throws IOException, JsonProcessingException {
    jsonGenerator.writeNumber(labelValue.getValue());
  }
}

但是在 JsonDeserializer 中无法通过接口得到枚举的类型,所以很难实现统一的反序列化操作。在花费大量的时间设计和测试后,终于解决了这个问题。

首先实现下面的反序列类:

/**
 * 处理枚举类型的反序列化,特殊处理 LabelValue
 */
public static class LabelValueDeserializer extends JsonDeserializer<LabelValue> {
  private Class<?> enumClass;

  public LabelValueDeserializer(Class<?> enumClass) {
    this.enumClass = enumClass;
  }

  /**
   * 这个方法用不到,由于必须继承 JsonDeserializer 才能使用 ContextualDeserializer,所以有这个空方法
   */
  @Override
  public LabelValue deserialize(JsonParser p, DeserializationContext ctxt)
      throws IOException, JsonProcessingException {
    //获取序列化值
    int intValue = p.getIntValue();
    //遍历返回枚举值
    for (Object enumConstant : enumClass.getEnumConstants()) {
      if (((LabelValue) enumConstant).getValue().equals(intValue)) {
        return (LabelValue) enumConstant;
      }
    }
    //可以根据逻辑返回 null 或者抛出非法参数异常
    return null;
  }
}

前面也提到了在 JsonDeserializer 里无法获取序列化字段对应的 java 类型,但是上面这个实现中,需要传入枚举类型创建,因此 LabelValueDeserializer 肯定需要在运行时动态创建。

jackson 中提供了一个 ContextualDeserializer (opens new window) 接口,这个接口是 JsonDeserializer 可用的附加接口,通过该接口可以获取创建反序列化器时的上下文,通过该接口可以针对不同的类型进行不同的反序列化处理。

ContextualDeserializer 接口方法的参数中,可以获取我们正好想要的类型信息:

@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
    throws JsonMappingException {
  //获取参数类型
  final Class<?> enumClass = ctxt.getContextualType().getRawClass();
  //其他处理
}

综合上面代码,下面是完整的代码:

@Configuration
public class EnumJacksonConfig {

  /**
   * 处理 LabelValue 子类的序列化
   */
  @JsonComponent
  public static class Serializer extends JsonSerializer<LabelValue> {
    @Override
    public void serialize(LabelValue labelValue, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
      //这里使用数值序列化,因此在反序列化时,直接 getIntValue
      jsonGenerator.writeNumber(labelValue.getValue());
    }
  }

  /**
   * 处理枚举类型的反序列化,特殊处理 LabelValue
   */
  @JsonComponent
  public static class EnumDeserializer
      extends JsonDeserializer<Enum> implements ContextualDeserializer {

    @Override
    public Enum deserialize(JsonParser p, DeserializationContext ctxt)
        throws IOException, JsonProcessingException {
      //不使用当前的类进行反序列化
      return null;
    }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
        throws JsonMappingException {
      //获取参数类型
      final Class<?> enumClass = ctxt.getContextualType().getRawClass();
      //如果继承的 LabelValue 接口,使用 LabelValue 反序列化器
      if (LabelValue.class.isAssignableFrom(enumClass)) {
        return new LabelValueDeserializer(enumClass);
      } else {
        //这里返回的默认的枚举策略
        return new DefaultEnumDeserializer(enumClass);
      }
    }
  }

  /**
   * LabelValue 子类枚举的反序列化
   */
  public static class LabelValueDeserializer extends JsonDeserializer<LabelValue> {
    private Class<?> enumClass;

    public LabelValueDeserializer(Class<?> enumClass) {
      this.enumClass = enumClass;
    }

    @Override
    public LabelValue deserialize(JsonParser p, DeserializationContext ctxt)
        throws IOException, JsonProcessingException {
      //获取序列化值
      int intValue = p.getIntValue();
      //遍历返回枚举值
      for (Object enumConstant : enumClass.getEnumConstants()) {
        if (((LabelValue) enumConstant).getValue().equals(intValue)) {
          return (LabelValue) enumConstant;
        }
      }
      //可以根据逻辑返回 null 或者抛出非法参数异常
      return null;
    }
  }

  /**
   * 默认枚举反序列化
   */
  public static class DefaultEnumDeserializer extends JsonDeserializer<Enum> {
    private Class<?> enumClass;

    public DefaultEnumDeserializer(Class<?> enumClass) {
      this.enumClass = enumClass;
    }

    @Override
    public Enum deserialize(JsonParser p, DeserializationContext ctxt)
        throws IOException, JsonProcessingException {
      JsonToken token = p.getCurrentToken();
      //枚举可能是用的 name() 方式,也就是字符串
      if (token == JsonToken.VALUE_STRING) {
        String value = p.getValueAsString();
        for (Object enumConstant : enumClass.getEnumConstants()) {
          if (((Enum) enumConstant).name().equals(value)) {
            return (Enum) enumConstant;
          }
        }
      }
      //也可能使用的 ordinal() 数值方式
      else if (token == JsonToken.VALUE_NUMBER_INT) {
        int intValue = p.getIntValue();
        for (Object enumConstant : enumClass.getEnumConstants()) {
          if (((Enum) enumConstant).ordinal() == intValue) {
            return (Enum) enumConstant;
          }
        }
      }
      //可以根据逻辑返回 null 或者抛出非法参数异常
      return null;
    }
  }

}

如此一来,所有的枚举都能进行统一的处理,不需要每一个都实现或者配置就能使用,到了这一步,全程使用枚举已经没有任何障碍了。

# 6. 总结

一开始我们觉得实体类中每个枚举都需要实现和配置一个类型处理器,使用很不方便,现在我们有了多种手段来尽可能的减少枚举需要的配置,DAO 层的障碍已经解决了。

以前即使我们配置了枚举类型处理器,在前后端交互时无法正确的处理枚举的序列化和反序列化,为了使用枚举我们还需要前后端相关的配置,为了使用一个枚举,需要太多额外的代码。

如今针对 Spring MVC 默认 jackson 有了统一处理枚举的办法,前后端交互使用枚举也不再是问题。

本文枚举接口使用的 LabelValue,这也是为了写本文临时造的接口,看完整篇文章后,你可以根据自己的需要定义不同类型的枚举接口,而不是照办这里的 LabelValue 使用。按照自己需要的接口实现对应的类型处理器和序列化反序列的类,就可以开始享受枚举带来的便利了,系统中不在需要各种魔法数字,不在需要对类型值进行有效性验证。

枚举真的已经很方便了,你要不要试试?

MyBatis 发布的 3.5.0 版本,这是一个大版本,有许多和低版本不兼容的地方,但是关于枚举这部分的内容和 3.4.x 最新版本没有区别,不管是否升级,都不影响本文中的枚举使用。