MybatisPlus支持Entity里使用枚举做为属性类型的源码解析

本文揭示了如何在Mybatis Plus中利用枚举类和@EnumValue注解实现数据库查询时的自动映射,包括配置、扫描过程和构造方法细节。

前几天同事提的一个疑问,为啥能这么用?由于我也没这么用过,于是带着好奇心去探索了一下并整理了此文章。


场景


枚举类:

@Getter
@AllArgsConstructor
public enum PublishStatus {
    PUBLISHED(1, "已发布"), CANCEL(2, "已取消"), DELETED(3, "已删除");
    
    @EnumValue
    private final int code;
    private String name;
}

Entity实体类:

public class PublishInfo implements Serializable {

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

	private String name;

    private PublishStatus status; //枚举类型

在Service里查询DB数据,如果表里这行记录的status=1,那么映射成Entity的status属性如下:
在这里插入图片描述


实现条件

1.mybatis-plus配置里指定枚举路径;

mybatis-plus:
  type-enums-package: com.xxx.xxx.enums
  mapper-locations: classpath*:mapper/**/*.xml

2.枚举类里使用@EnumValue注解标记要映射的枚举属性,如上。


3.Entity里使用该枚举做为属性类型,如上。


源码解析

这里只贴关键代码

public class MybatisSqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {

    @Override
    public void afterPropertiesSet() throws Exception {
        notNull(dataSource, "Property 'dataSource' is required");
        state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
            "Property 'configuration' and 'configLocation' can not specified with together");

        this.sqlSessionFactory = buildSqlSessionFactory();
    }

    protected SqlSessionFactory buildSqlSessionFactory() throws Exception {

        // TODO 自定义枚举类扫描处理
        if (hasLength(this.typeEnumsPackage)) {
            
            // 取得类型转换注册器
            TypeHandlerRegistry typeHandlerRegistry = targetConfiguration.getTypeHandlerRegistry();
            classes.stream()
                .filter(Class::isEnum)
                .filter(MybatisEnumTypeHandler::isMpEnums)
                .forEach(cls -> typeHandlerRegistry.register(cls, MybatisEnumTypeHandler.class));
        }

        return sqlSessionFactory;
    }

扫描配置的枚举路径下所有class文件,并MybatisEnumTypeHandler::isMpEnums匹配是否有@EnumValue注解:

@Deprecated
public class MybatisEnumTypeHandler<E extends Enum<?>> extends BaseTypeHandler<Enum<?>> {
   /**
     * 判断是否为MP枚举处理
     *
     * @param clazz class
     * @return 是否为MP枚举处理
     * @since 3.3.1
     */
    public static boolean isMpEnums(Class<?> clazz) {
        return clazz != null && clazz.isEnum() && (IEnum.class.isAssignableFrom(clazz) || findEnumValueFieldName(clazz).isPresent());
    }

   /**
     * 查找标记标记EnumValue字段
     *
     * @param clazz class
     * @return EnumValue字段
     * @since 3.3.1
     */
    public static Optional<String> findEnumValueFieldName(Class<?> clazz) {
        if (clazz != null && clazz.isEnum()) {
            String className = clazz.getName();
            return Optional.ofNullable(CollectionUtils.computeIfAbsent(TABLE_METHOD_OF_ENUM_TYPES, className, key -> {
                Optional<Field> optional = Arrays.stream(clazz.getDeclaredFields())
                    .filter(field -> field.isAnnotationPresent(EnumValue.class))
                    .findFirst();
                return optional.map(Field::getName).orElse(null);
            }));
        }
        return Optional.empty();
    }

能匹配到的枚举类,会注册到TypeHandlerRegistry。

typeHandlerRegistry.register(cls, MybatisEnumTypeHandler.class)

注册的过程中,有一个关键代码 “c.newInstance(javaTypeClass)”,即MybatisEnumTypeHandler实例化时构造方法里传入了当前枚举类。

public <T> TypeHandler<T> getInstance(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
    if (javaTypeClass != null) {
      try {
        Constructor<?> c = typeHandlerClass.getConstructor(Class.class);
        return (TypeHandler<T>) c.newInstance(javaTypeClass);
      } catch (NoSuchMethodException ignored) {
        // 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);
    }
  }

来看看MybatisEnumTypeHandler的构造方法:

public MybatisEnumTypeHandler(Class<E> type) {
        if (type == null) {
            throw new IllegalArgumentException("Type argument cannot be null");
        }
        this.type = type;
        MetaClass metaClass = MetaClass.forClass(type, REFLECTOR_FACTORY);
        String name = "value";
        if (!IEnum.class.isAssignableFrom(type)) {
            name = findEnumValueFieldName(this.type).orElseThrow(() -> new IllegalArgumentException(String.format("Could not find @EnumValue in Class: %s.", this.type.getName())));
        }
        this.invoker = metaClass.getGetInvoker(name);
    }

通过findEnumValueFieldName()方法获取@EnumValue注解标记要映射的枚举属性,结合上面场景name就等于枚举类PublishStatus的code属性。然后拿到MethodInvoder,结合上面场景,this.invoker等于枚举类PublishStatus的getCode()方法。


每次映射拿值时,就通过构造方法里赋值的this.invoker的invoke()方法,即枚举类PublishStatus的getCode()方法拿值。

   private Object getValue(Object object) {
        try {
            return invoker.invoke(object, new Object[0]);
        } catch (ReflectiveOperationException e) {
            throw ExceptionUtils.mpe(e);
        }
    }

Mybatis填充属性值时,getValue()方法就能拿到 枚举类里对应的值了,并赋值给Entity属性值。

  @Override
  public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
    setNonNullParameter(ps, i, parameter, jdbcType);
  }
  
   @Override
   public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType)
        throws SQLException {
        if (jdbcType == null) {
            ps.setObject(i, this.getValue(parameter));
        } else {
            // see r3589
            ps.setObject(i, this.getValue(parameter), jdbcType.TYPE_CODE);
        }
   }

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值