概述及背景
实际项目中,我们经常要处理一些枚举类型的数据。例如:订单的状态就分为已下单,已付款,已发货,订单完成等等很多状态,数据库中我们一般只存储的一个数字表示各种状态。但是,前台显示的话就需要显示名称给用户看,所以这中间就涉及到一个转化。我们见过太多了在前端通过 if, else 写死判断的。这样初期没啥,后期难以扩展及维护。针对这种情况,我们思考下能不能直接将枚举直接返回给前端,让前端显示时取value, 保存时传入key?这样的前端就不用写一堆判断了。
项目结构
项目的结构如上图所示,主要有7个部分,其中 1,4,7 是通过MyBatis操作数据库所必须的。我们只做简要分析,其余如通用的类型处理GeneralEnumHandler 和重写TypeHandlerRegistry类将是我们重点分析的对象。
枚举类基类
public interface BaseEnum<E extends Enum<?>, T> { /** * 真正与数据库进行映射的值 * @return */ T getKey(); /** * 显示的信息 * @return */ String getValue(); }
枚举类基类作为万能的模板,主要定义了获取key和value的方法,供子类实现。然后其传入的泛型<E extends Enum<?>, T>一个是枚举类自身,一个是枚举的key的类型。
通用类型处理器的设计
// 所有的自定义类型处理器都需要实现TypeHandler或者继承BaseTypeHandler类。 public class GeneralEnumHandler<E extends BaseEnum> extends BaseTypeHandler<E> { private Class<E> type; private E[] enums; //设置参数到preparedStatement @Override public void setNonNullParameter(PreparedStatement preparedStatement, int i, E e, JdbcType jdbcType) throws SQLException { if (jdbcType == null) { preparedStatement.setObject(i, e.getKey()); } else { preparedStatement.setObject(i, e.getKey(), jdbcType.TYPE_CODE); } } //设置参数到preparedStatement @Override public void setNonNullParameter(PreparedStatement preparedStatement, int i, E e, JdbcType jdbcType) throws SQLException { if (jdbcType == null) { preparedStatement.setObject(i, e.getKey()); } else { preparedStatement.setObject(i, e.getKey(), jdbcType.TYPE_CODE); } } //根据ResultSet返回的key,找到其对应的enum @Override public E getNullableResult(ResultSet resultSet, String s) throws SQLException { if (resultSet.wasNull()) { return null; } Object key = resultSet.getObject(s); return locateEnumsStatus(key); } // 省略部分方法。 /** * @param key * @return */ private E locateEnumsStatus(Object key) { if (key instanceof Integer) { for (E anEnum : enums) { if (anEnum.getKey() == key) { return anEnum; } } throw new IllegalArgumentException("未知的枚举类型:" + key + ",请核对" + type.getSimpleName()); } if (key instanceof String) { for (E anEnum : enums) { if (anEnum.getKey().equals(key)) { return anEnum; } } throw new IllegalArgumentException("未知的枚举类型:" + key + ",请核对" + type.getSimpleName()); } throw new IllegalArgumentException("未知的枚举类型:" + key + ",请核对" + type.getSimpleName()); } }
如上处理后,我们就可以在配置文件或者映射文件中配置使用通用的处理器了。
我们只需要在配置文件中配置(1-1配置)
<typeHandlers> <typeHandler handler="com.jay.chapter3.handler.GeneralEnumHandler" javaType="com.jay.chapter3.enums.SexEnum"/> </typeHandlers>
或者在映射文件中配置
<result property="sexEnum" column="sex" typeHandler="com.jay.chapter3.Handler.GeneralEnumHandler"/>
需要注意的是要在用到枚举类的实体中引入枚举类对象。如下,
public class Student { /** * 性别 */ private SexEnum sexEnum; //省略getter,setter方法 }
如上,定义一个通用的类型处理器来处理枚举还是比较方便的。只是美中不足的是,如果我们有很多枚举类,那么我们就需要在配置文件中定义很多(1-1配置),着实有点繁琐。那么我思考下如何减少配置呢?哈哈,答案就是对枚举类实现自动扫描。但是,MyBatis 的框架默认是不支持的。下面我们就来看看源码。
TypeHandlerRegistry源码
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)); } }
如上,我们MyBatis框架是通过register(Class<?> typeHandlerClass)将类型处理器注册到注册机中。但是,我们也知道,MappedTypes 元数据只支持配置一个个具体的Class对象,这显然不符合我们的需求。那么我们的重写点就从MappedTypes元数据开始,使其可以支持包名配置,然后,在通过包扫描其下面的所有枚举类,循环注入即可。
重写MappedTypes元数据
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface MappedTypes { Class<?>[] value() default {}; // 增加包名配置 String[] basePackage() default {}; }
需要注意的是重写框架的类,包名路径要与框架保持完全一致。 然后在通用类型处理器GeneralEnumHandler中类名上添加如下注解即可
// 该包名是枚举类下的包名路径 @MappedTypes(basePackage = {"com.jay.chapter3.enums"})
接着我们来重写下TypeHandlerRegistry的register方法。
重写TypeHandlerRegistry
public void register(Class<?> typeHandlerClass) { boolean mappedTypeFound = false; MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class); if (mappedTypes != null) { if (mappedTypes.value().length > 0) { for (Class<?> javaTypeClass : mappedTypes.value()) { register(javaTypeClass, typeHandlerClass); mappedTypeFound = true; } } if (mappedTypes.basePackage().length > 0) { for (String packageName : mappedTypes.basePackage()) { // 扫描并注册包下所有继承于superType的类型 ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>(); resolverUtil.find(new ResolverUtil.IsA(Object.class), packageName); //获取该包下所有满足条件的class对象 Set<Class<? extends Class<?>>> mTypes = resolverUtil.getClasses(); for (Class<? extends Class<?>> javaTypeClass : mTypes) { // 注册枚举类以及其所使用的类型处理器 register(javaTypeClass,typeHandlerClass); mappedTypeFound = true; } } } } if (!mappedTypeFound) { register(getInstance(null, typeHandlerClass)); } }
如上,包扫描那个分支的主要流程有三个:
1.扫描出包下所有继承于superType(一般是Object.class)类型的class 对象
2.获取该包下所有满足条件的class对象
3.循环注册枚举类以及其所使用的类型处理器。
重写完成之后,接着我们来看看文件中的配置:
<typeHandlers> <package name="com.jay.chapter3.handler"/> </typeHandlers>
下面我们就来测试一下:测试代码如下:
@Test public void testSelectId() { String resource = "chapter3/mybatis-cfg.xml"; try { InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession session = factory.openSession(); Student3Mapper mapper = session.getMapper(Student3Mapper.class); Student student = mapper.selectStudentById(1); System.out.println("------->student={}"+student); } catch (IOException e) { e.printStackTrace(); } }
测试结果如下:
总结
本文通过一个小小的demo 示范了如果如何自定义类型处理器,然后,通过重写MyBatis中的MappedTypes和TypeHandlerRegistry两个类,实现了对枚举类所在包的扫描已经注册。希望对读者朋友们有所帮助。
源代码
https://github.com/XWxiaowei/MyBatisLearn/tree/master/mybatisDemo