自定义 MyBatis 通用枚举类型解析器
在使用MyBatis
的过程中,我们经常会使用到枚举类型的数据,
一般在保存数据时只是想将枚举类型的code值存入到数据库中,查询时希望能自动根据code值映射出对应的枚举对象出现,而不是查询出code值然后再手动根据code值找到对应的枚举对象的转换
官方注册方案
官方方案:https://mybatis.org/mybatis-3/zh_CN/configuration.html#typeHandlers
无法对所有枚举类型进行通用注册(有可能是没找到正确的方式,如果有,恳请大家指导)
自动注册方案
实现思路如下:
-
自定义注解用于标识枚举字段
code
值(可以使用Jackson自带的@JsonValue
注解,也可以单独自定义注解),注解标识的字段类型非固定类型,可为Integer
、Long
、String
等其他基本类型或其他类型(其他类型请多测试) -
自定义枚举类型处理器 MyBatisEnumTypeHandler.java 继承自
org.apache.ibatis.type.BaseTypeHandler
,用于处理枚举类型数据的保存和查询使用package com.kws.annotation; import java.lang.annotation.*; /** * @author kws * @date 2024-01-24 13:59 */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface EnumValueMarker { }
package com.kws.annotation; import com.fasterxml.jackson.annotation.JsonValue; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Type; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; /** * @author kws * @date 2024-01-12 20:12 */ public class EnumValueMarkerFinder { // public static final Class<? extends Annotation> ANNOTATION_CLASS = EnumValueMarker.class; public static final Class<? extends Annotation> ANNOTATION_CLASS = JsonValue.class; public static boolean hasAnnotation(Class<?> clazz) { try { return hasAnnotation(clazz, ANNOTATION_CLASS); } catch (Exception e) { return false; } } public static boolean hasAnnotation(Class<?> clazz, Class<? extends Annotation> annotationClass) { try { return findAnnotatedFields(clazz, annotationClass).size() > 0; } catch (Exception e) { return false; } } public static List<Field> findAnnotatedFields(Class<?> clazz, Class<? extends Annotation> annotationClass) { if (!clazz.isEnum()) { throw new RuntimeException("Class " + clazz.getName() + " is not an Enum"); } return Arrays.stream(clazz.getDeclaredFields()).filter(field -> field.isAnnotationPresent(annotationClass)).collect(Collectors.toList()); } public static Field find(Class<?> clazz) { return find(clazz, ANNOTATION_CLASS); } public static Field find(Class<?> clazz, Class<? extends Annotation> annotationClass) { if (!clazz.isEnum()) { throw new RuntimeException("Class " + clazz.getName() + " is not an Enum"); } List<Field> fields = Arrays.stream(clazz.getDeclaredFields()).filter(field -> field.isAnnotationPresent(annotationClass)).collect(Collectors.toList()); if (fields.isEmpty()) { throw new RuntimeException("Enum " + clazz.getName() + " has no field annotated with " + annotationClass.getName()); } if (fields.size() > 1) { throw new RuntimeException(formatMsg(fields.get(0), fields.get(1))); } Field field = fields.get(0); if (field == null) { throw new RuntimeException("Enum " + clazz.getName() + " has no field annotated with " + ANNOTATION_CLASS.getName()); } field.setAccessible(true); return field; } public static String formatMsg(Field field, Field field2) { return String.format("Multiple 'as-value' properties defined ([field %s#%s] vs [field %s#%s])", field.getDeclaringClass().getName(), field.getName(), field2.getDeclaringClass().getName(), field2.getName()); } public static String formatMsg(Type type, String name, Object value) { return String.format("【%s#%s:%s is not exist】", type.getTypeName(), name, value); } }
@Slf4j public class MyBatisEnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> { private final Class<E> type; public MyBatisEnumTypeHandler(Class<E> type) { this.type = type; } @Override public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException { try { Field field = EnumValueMarkerFinder.find(type); Object val = field.get(parameter); if (jdbcType == null) { ps.setObject(i, val); } else { ps.setObject(i, val, jdbcType.TYPE_CODE); } } catch (IllegalAccessException e) { throw new RuntimeException(e); } } @Override public E getNullableResult(ResultSet rs, String columnName) throws SQLException { Object s = rs.getObject(columnName); return findTargetEnum(s, type); } @Override public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException { Object s = rs.getObject(columnIndex); return findTargetEnum(s, type); } @Override public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { Object s = cs.getObject(columnIndex); return findTargetEnum(s, type); } private E findTargetEnum(Object val, Class<E> type) { if (val == null) { return null; } try { Field field = EnumValueMarkerFinder.find(type); for (E enumConstant : type.getEnumConstants()) { Object o = field.get(enumConstant); if (val.equals(o)) { return enumConstant; } } } catch (IllegalAccessException e) { log.error("Handle enum failed...", e); } return null; } }
-
接下来,怎么将自定义的枚举类型处理器用于处理所有枚举类型的数据?
-
为了实现所有的枚举都自动注册通用类型转换器,这里需要自定义一个配置类 CustomizeMyBatisConfiguration.java 并实现
org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer
接口- 实现该接口后,可以获取到
org.apache.ibatis.session.Configuration
配置类, - 使用
Configuration
配置类获取到TypeHandlerRegistry
注册器, - 再使用
TypeHandlerRegistry
注册器将需要处理的枚举类类型解析器注册进去
public class CustomizeMyBatisConfiguration implements ConfigurationCustomizer{ public void customize(Configuration configuration) { // 将自定义的通用枚举类型处理器`MyBatisEnumTypeHandler`注册进去 // Class clazz = null; // 怎么获取到需要处理的枚举类,即字段中标了@JsonValue注解或自定义注解的枚举类? configuration.getTypeHandlerRegistry().register(clazz, new MyBatisEnumTypeHandler<>(clazz)); } }
- 获取所有需要注册到通用枚举类型处理器中的枚举类
- 在
customize
方法中通过Spring
框架中ClassPathScanningCandidateComponentProvider
扫描器在classpath
下扫描出指定包下的枚举类 - 自定义一个类型过滤器
com.kws.mybatis.config.CustomizeMyBatisConfiguration.EnumTypeFilter
,用于在类路径扫描时,过滤出需要处理的枚举类(1.枚举类型 2.枚举类型中含有自定义注解字段)public static class EnumTypeFilter implements TypeFilter { @Override public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) { String typeName = metadataReader.getClassMetadata().getSuperClassName(); if (!ENUM_TYPE.equals(typeName)) { return false; } try { Class<?> clazz = ClassUtils.forName(metadataReader.getClassMetadata().getClassName(), getClass().getClassLoader()); return EnumValueMarkerFinder.hasAnnotation(clazz); } catch (ClassNotFoundException e) { log.error("EnumTypeFilter match failed. Class not found: " + metadataReader.getClassMetadata(), e); } return false; } }
- 过滤出需要处理的枚举类后,通过
TypeHandlerRegistry
将当前枚举类型使用通用的枚举类型处理器注册到类型处理器中 - 具体注册代码如下
@Slf4j @Component public class CustomizeMyBatisConfiguration implements ConfigurationCustomizer { /** * 可改成读取配置文件包路径. * 注意: * 如果需要从配置文件读取,直接通过@Value注解注入不会生效, * 需要实现EnvironmentAware接口,通过EnvironmentAware接口获取配置 */ private static final String BASE_SCAN_PACKAGE = "com.kws"; public static final String ENUM_TYPE = "java.lang.Enum"; @Override @SuppressWarnings({"unchecked", "rawtypes"}) public void customize(Configuration configuration) { ClassPathScanningCandidateComponentProvider classPathScanning = new ClassPathScanningCandidateComponentProvider(false); classPathScanning.addIncludeFilter(new EnumTypeFilter()); Set<BeanDefinition> enumsBeanDefinitions = classPathScanning.findCandidateComponents(BASE_SCAN_PACKAGE); if (CollectionUtils.isEmpty(enumsBeanDefinitions)) { return; } for (BeanDefinition bd : enumsBeanDefinitions) { try { log.info("====== register TypeHandler for Enum ======【{}】", bd.getBeanClassName()); Class clazz = ClassUtils.forName(Objects.requireNonNull(bd.getBeanClassName()), getClass().getClassLoader()); configuration.getTypeHandlerRegistry().register(clazz, new MyBatisEnumTypeHandler<>(clazz)); } catch (Exception e) { log.error("====== Register Mybatis TypeHandler Failed. Enum:【{}】", bd.getBeanClassName(), e); } } } /** * 自定义枚举类型过滤器 <p> * 1.过滤枚举类型 <p> * 2.枚举类型字段必须打了枚举类型注解(或自定义注解) <p> * * @author kws * @date 2024-01-14 17:19 */ public static class EnumTypeFilter implements TypeFilter { @Override public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) { String typeName = metadataReader.getClassMetadata().getSuperClassName(); if (!ENUM_TYPE.equals(typeName)) { return false; } try { Class<?> clazz = ClassUtils.forName(metadataReader.getClassMetadata().getClassName(), getClass().getClassLoader()); return EnumValueMarkerFinder.hasAnnotation(clazz); } catch (ClassNotFoundException e) { log.error("EnumTypeFilter match failed. Class not found: " + metadataReader.getClassMetadata(), e); } return false; } } }
- 完整代码已发布github:
github: enum-mapping
CustomizeMyBatisConfiguration.java
MyBatisEnumTypeHandler.java
- 在
- 实现该接口后,可以获取到