SpringBoot整合Mybatis实现自动转换枚举类型

背景

  在做之前项目的时候,里面充斥很多不明的变量,一般来说状态,标志等等属性都需要使用Int或者固定字符串来标识,比如0代表可用,1代表禁用,或者是可用不可用,随着人员的增加,萝卜酸菜各有所爱,有些人可能会使用1代表可用,0代表不可用。还有的人不喜欢使用0,直接用12来代替。使用字符串就更加坑爹了,比如你使用可用不可用,他使用可用禁用。虽然知道你要表达的意思,但是给前端人员的时候就十分难受了,难道要写nif else,这无疑来说是一种灾难。所以我们需要制定一个好点的方案,讲其统一进行管理,比较简单是直接写个constant接口,将所需要的规范的信息全部放入里面。另一种比较规范的做法是定义枚举,这就是接下来我们所要展开叙述的。


如何进行扩展

最为一款优秀的ORM框架,类型转换是不可或缺的核心组成部分,既然被称之为对象关系映射,那就一定会有对象属性与数据库表字段进行映射的手段,所以我们需要查看源码寻找类型映射的部分。

打开源码包最终发现如下有一个叫type的包:
在这里插入图片描述
这里面包含了大多数基本类型的处理类,大多数都是TypeHandler为后缀的,这无疑就是我们需要的类了。
里面好像包含了Enum的处理类型,一个是org.apache.ibatis.type.EnumOrdinalTypeHandler,另外一个是org.apache.ibatis.type.EnumTypeHandler。它们有什么作用呢?

我们看下源码:

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

  private final Class<E> type;

  public EnumTypeHandler(Class<E> type) {
    if (type == null) {
      throw new IllegalArgumentException("Type argument cannot be null");
    }
    this.type = type;
  }

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

  @Override
  public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
    String s = rs.getString(columnName);
    return s == null ? null : Enum.valueOf(type, s);
  }

  @Override
  public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
    String s = rs.getString(columnIndex);
    return s == null ? null : Enum.valueOf(type, s);
  }

  @Override
  public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
    String s = cs.getString(columnIndex);
    return s == null ? null : Enum.valueOf(type, s);
  }
}

注意到:setNonNullParameter,包装我们的PreparedStatement进行SQL的插值操作。看到这里,我们发现这个默认的实现,会引用enumname,也就是enumtoString,这显然不是我们所需要的,另一个org.apache.ibatis.type.EnumOrdinalTypeHandler也不是我们所需要的,它只能处理Int,String这两种类型,如果是复杂的类型,比如:

  DELETE(9, "删除");

这样就不支持了,所以我们需要自定义实现。

可以通过观察源码的其他类型,发现全部都是继承BaseTypeHandler这个类。
我们可以看下这个类中有些什么:

public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> {

  protected Configuration configuration;

  public void setConfiguration(Configuration c) {
    this.configuration = c;
  }

  @Override
  public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
    if (parameter == null) {
      if (jdbcType == null) {
        throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
      }
      try {
        ps.setNull(i, jdbcType.TYPE_CODE);
      } catch (SQLException e) {
        throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . " +
                "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. " +
                "Cause: " + e, e);
      }
    } else {
      try {
        setNonNullParameter(ps, i, parameter, jdbcType);
      } catch (Exception e) {
        throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . " +
                "Try setting a different JdbcType for this parameter or a different configuration property. " +
                "Cause: " + e, e);
      }
    }
  }

  @Override
  public T getResult(ResultSet rs, String columnName) throws SQLException {
    T result;
    try {
      result = getNullableResult(rs, columnName);
    } catch (Exception e) {
      throw new ResultMapException("Error attempting to get column '" + columnName + "' from result set.  Cause: " + e, e);
    }
    if (rs.wasNull()) {
      return null;
    } else {
      return result;
    }
  }

  @Override
  public T getResult(ResultSet rs, int columnIndex) throws SQLException {
    T result;
    try {
      result = getNullableResult(rs, columnIndex);
    } catch (Exception e) {
      throw new ResultMapException("Error attempting to get column #" + columnIndex+ " from result set.  Cause: " + e, e);
    }
    if (rs.wasNull()) {
      return null;
    } else {
      return result;
    }
  }

  @Override
  public T getResult(CallableStatement cs, int columnIndex) throws SQLException {
    T result;
    try {
      result = getNullableResult(cs, columnIndex);
    } catch (Exception e) {
      throw new ResultMapException("Error attempting to get column #" + columnIndex+ " from callable statement.  Cause: " + e, e);
    }
    if (cs.wasNull()) {
      return null;
    } else {
      return result;
    }
  }
  public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

  public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException;

  public abstract T getNullableResult(ResultSet rs, int columnIndex) throws SQLException;

  public abstract T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException;
}

存在一个configuration全局配置属性对象,进行了一些非空的校验,实际完成功能的是下面的子类。


如何实现

有了上面的分析,我们就很好办的了,我们就仿照EnumTypeHandler来写一个,可以直接"照搬"过来。但是我们得考虑一下,我们需要如何做到通用,也就是我们写完一个枚举就可以实现自动的映射,而不需要重复TypeHandler

通过封装、继承、多态的特性。我们可以定义一套接口,让需要定义的枚举类实现它,这样我们就可以根据类型判断,是否超类是否为该接口,为什么不是直接通过接口来扫描实现类呢,因为JDK没有这样的实现,根据父类获取所有子类。

1. 定义枚举接口

	public interface BaseEnum<E extends Enum<E>, T> {

    //接口实现类装载容器,方便快速获取全部子类,所有实现子类必须使用静态块将其注册进来
    Set<Class<?>> subClass = Sets.newConcurrentHashSet();

    /**
     * 真正与数据库进行映射的值
     *
     * @return
     */
    T getValue();

    /**
     * 显示的信息
     *
     * @return
     */
    String getDisplayName();
	}

2. 实现一个State枚举

public enum State implements BaseEnum<State, Integer> {
    /**
     * 正常状态
     */
    NORMAL(0, "正常"),


    /**
     * 删除状态
     */
    DELETE(9, "删除");

    private final int value;

    private final String description;

    static {
        subClass.add(State.class);
    }

    State(int value, String description) {
        this.value = value;
        this.description = description;
    }

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

    @Override
    public String getDisplayName() {
        return description;
    }
}

3. 实现GeneralTypeHandler处理类

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.*;
@Slf4j
public final class GeneralTypeHandler<E extends BaseEnum> extends BaseTypeHandler<E> {

    private Class<E> type;

    private E[] enums;


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

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
        //BaseTypeHandler 进行非空校验
        log.debug("index : {}, parameter : {},jdbcType : {} ", i, parameter.getValue(), jdbcType);

        if (jdbcType == null) {
            ps.setObject(i, parameter.getValue());
        } else {
            ps.setObject(i, parameter.getValue(), jdbcType.TYPE_CODE);
        }

    }

    @Override
    public E getNullableResult(ResultSet rs, String columnName) throws SQLException {

        Object code = rs.getObject(columnName);

        if (rs.wasNull()) {
            return null;
        }

        return getEnmByCode(code);
    }

    @Override
    public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        Object code = rs.getObject(columnIndex);
        if (rs.wasNull()) {
            return null;
        }

        return getEnmByCode(code);
    }

    @Override
    public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        Object code = cs.getObject(columnIndex);
        if (cs.wasNull()) {
            return null;
        }

        return getEnmByCode(code);
    }

    private E getEnmByCode(Object code) {

        if (code == null) {
            throw new NullPointerException("the result code is null " + code);
        }

        if (code instanceof Integer) {
            for (E e : enums) {
                if (e.getValue() == code) {
                    return e;
                }
            }
            throw new IllegalArgumentException("Unknown enumeration type , please check the enumeration code :  " + code);
        }


        if (code instanceof String) {
            for (E e : enums) {
                if (code.equals(e.getValue())) {
                    return e;
                }
            }
            throw new IllegalArgumentException("Unknown enumeration type , please check the enumeration code :  " + code);
        }

        throw new IllegalArgumentException("Unknown enumeration type , please check the enumeration code :  " + code);
    }
}

现在我们基本组件已经实现完全了,剩下就是需要将GeneralTypeHandlerMybatis进行关联起来,所以接下来我们分析下,TypeHandler的处理流程


TypeHandler的注册

注册流程图:
在这里插入图片描述

register是依赖TypeHandlerRegistry这个对象的,所以我们需要取得这个对象,万幸的是,TypeHandlerRegistry是属于Configuration的一位成员变量而存在,那么我们只需要获取到Configuration就可以进行注册啦。前面我们不是讲到BaseTypeHandler中就存在Configuration的引用么,所以我们可以将TypeHandlerRegistry,在构造器中注入到Configuration中。但是我们这里结合SpringBoot,所以我们需要优雅的处理一下,使用SpringBoot的方式进行注册。

SpringBoot整合Mybatis的时候为我们提供了一个自定义的Configuration回调,我们只需要实现一个接口,就可以获取Configuration对象,在它的基础上进行添砖加瓦。

接口长这样:

public interface ConfigurationCustomizer {
  /**
   * Customize the given a {@link Configuration} object.
   * @param configuration the configuration object to customize
   */
  void customize(Configuration configuration);
}

实现自定义ConfigurationCustomizer

@Component
@Slf4j
public class RegisterEnumHandlerConfig implements ConfigurationCustomizer {

    @Override
    public void customize(Configuration configuration) {
        log.debug("ConfigurationCustomizer init....");
        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();

        try {
            final List<Class<?>> allAssignedClass = ClassUtil.getAllAssignedClass(BaseEnum.class);
            allAssignedClass.forEach((clazz) -> typeHandlerRegistry.register(clazz, GeneralTypeHandler.class));
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

ClassUtil的作用是获取BaseEnum的全部子类,只有将所有的子类与GeneralTypeHandler进行了映射,这样我们才能达到自动识别枚举的效果。

ClassUtil 的实现

public class ClassUtil {
    /**
     * 获取当前类的所有实现子类
     *
     * @param superClass
     * @return
     * @throws ClassNotFoundException
     */
    public static List<Class<?>> getAllAssignedClass(Class<?> superClass) throws ClassNotFoundException {
        List<Class<?>> classes = new ArrayList<>();
        for (Class<?> c : getClasses(superClass)) {
            if (superClass.isAssignableFrom(c) && !superClass.equals(c)) {
                classes.add(c);
            }
        }
        return classes;
    }

    public static List<Class<?>> getClasses(Class<?> cls) throws ClassNotFoundException {
        String pk = cls.getPackage().getName();
        String path = pk.replace(".", "/");
        ClassLoader classloader = Thread.currentThread().getContextClassLoader();
        URL url = classloader.getResource(path);
        return getClasses(new File(url.getFile()), pk);
    }

    private static List<Class<?>> getClasses(File dir, String pk) throws ClassNotFoundException {
        List<Class<?>> classes = new ArrayList<>();
        if (!dir.exists()) {
            return classes;
        }
        for (File file : dir.listFiles()) {
            if (file.isDirectory()) {
                classes.addAll(getClasses(file, pk + "." + file.getName()));
            }
            String fileName = file.getName();
            if (fileName.endsWith(".class")) {
                classes.add(Class.forName(pk + "." + fileName.substring(0, fileName.length() - 6)));
            }
        }
        return classes;
    }

    public static void main(String[] args) throws ClassNotFoundException {
        for (Class<?> c : getAllAssignedClass(BaseEnum.class)) {
            System.out.println(c);
        }
    }
}

另一种获取子类的方式:
可以在定义的接口中设置一个集合类,没当子类实现的接口的时候,通过子类的静态代码块,将该类的Class注入进去

public interface BaseEnum<E extends Enum<E>, T> {
   //接口实现类装载容器,方便快速获取全部子类,所有实现子类必须使用静态块将其注册进来
    Set<Class<?>> subClass = Sets.newConcurrentHashSet();
}
public enum State implements BaseEnum<State, Integer> {  
    static {
        subClass.add(State.class);
    }
}

  • 2
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 20
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值