mybatis自定义枚举类型的转换器以及各种使用场景
1. 背景
- 为什么使用枚举?
像性别、四季(春夏秋冬)、星期等,这些固定不变的一组信息我们一般使用枚举,这样我们定义枚举类后,在数据存储方面节省了数据库的存储空间,同时也增强了后台代码的可读性 - 为什么需要枚举转换器?
看这个给前端返回的json,dogSex的值是0,如果这样给前端返过去,肯定会让前端崩溃吧,你总不能让前端页面展示个0吧,还是你想让前端自己处理0把0变成汉字女展示?,所以属性枚举就来了,问题也就来了,怎么用?
2. 前期准备 + 问题
2.1 创建枚举 SexEnum
- ① 创建SexEnum.java,如下(下面会改,所以此处不贴代码,直接上图了):
- ② 修改属性类型:
则会个时候,如果你以为完事了,那肯定是你再开玩笑!
2.2 出现的问题
- 没有类型转换器的话,肯定会报下面的问题
com.liu.susu.enums.SexEnum cannot be cast to com.liu.susu.enums.config.IEnum
3. 自定义——枚举类型转换器
- 主要3个文件:
3.1 IEnum.java
- 代码如下:
package com.liu.susu.enums.config; /** * @description * @Author susu **/ public interface IEnum<E extends Enum<?>, T> { Object getValue(); String getName(); }
3.2 MybatisEnumHandler.java
- 代码如下:
package com.liu.susu.enums.config.handler; import cn.hutool.core.util.ClassUtil; import com.liu.susu.enums.SexEnum; import com.liu.susu.enums.config.IEnum; import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedTypes; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Map; import java.util.Set; /** * @FileName MybatisEnumHandler * @Description 枚举类型处理器 * 自动将实体类中类型为 MyEnum 子类的属性,转换为枚举 * @Author susu **/ //此注解告诉 mybatis 遇到此 SexEnum.class类型时,使用此处理器处理 @MappedTypes(value = {SexEnum.class}) public class MybatisEnumHandler<E extends Enum<E> & IEnum> extends BaseTypeHandler<E> { public static String enumPackage; /** * 动态修改@MappedTypes(value = {})的value值 * 原理:类的注解,是动态代理生成的类,通过获取代理对象,反射修改其值 */ static { try { MappedTypes annotation = MybatisEnumHandler.class.getAnnotation(MappedTypes.class); InvocationHandler invocationHandler = Proxy.getInvocationHandler(annotation); Field memberValues = invocationHandler.getClass().getDeclaredField("memberValues"); memberValues.setAccessible(true); // Map values = (Map) memberValues.get(invocationHandler); // Set<Class<?>> classes = ClassUtil.scanPackageBySuper(enumPackage, IEnum.class); // Class[] allMyEnums = classes.toArray(new Class[classes.size()]); // values.put("value", allMyEnums); /** * classes.size() > 0 判断一下,不然如果没有一个枚举继承 IEnum,启动报错 */ Set<Class<?>> classes = ClassUtil.scanPackageBySuper(enumPackage, IEnum.class); Map values = (Map) memberValues.get(invocationHandler); if (classes.size() > 0) { Class[] allMyEnums = classes.toArray(new Class[classes.size()]); values.put("value", allMyEnums); } } catch (NoSuchFieldException | IllegalAccessException e) { e.printStackTrace(); } } private final Class<E> type; /** * construct with parameter. */ public MybatisEnumHandler(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 { // ps.setString(i, parameter.getValue()); ps.setObject(i, parameter.getValue()); } @Override public E getNullableResult(ResultSet rs, String columnName) throws SQLException { String code = rs.getString(columnName); return rs.wasNull() ? null : this.codeOf(this.type, code); } @Override public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException { String code = rs.getString(columnIndex); return rs.wasNull() ? null : this.codeOf(this.type, code); } @Override public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { String code = cs.getString(columnIndex); return cs.wasNull() ? null : this.codeOf(this.type, code); } public <T extends Enum<?> & IEnum> T codeOf(Class<T> enumClass, String code) { T[] enumConstants = enumClass.getEnumConstants(); for (T t : enumConstants) { if (t.getValue().equals(code)) { return t; } } return null; } }
3.3 MainConfig.java
- 代码如下:
package com.liu.susu.enums.config; import com.liu.susu.enums.config.handler.MybatisEnumHandler; import org.apache.ibatis.type.TypeHandler; import org.mybatis.spring.boot.autoconfigure.MybatisProperties; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.context.annotation.Configuration; import org.springframework.util.StringUtils; /** * @FileName MainConfig * @Description MybatisProperties后置处理器,配置MyBatis的枚举处理器typeHandle * 相当用户在yml中配置: mybatis.type-handlers-package * @Author susu **/ @Configuration @ConditionalOnClass({TypeHandler.class, MybatisProperties.class}) public class MainConfig implements BeanPostProcessor { @Value("${susu.mybatis.enumPackage:null}") String enumPackage; public static final String HANDLER_PACKAGE = "com.liu.susu.enums.config.handler"; @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof MybatisProperties) { if (!StringUtils.hasText(enumPackage)) { enumPackage = getMainClassPackage(); } MybatisEnumHandler.enumPackage = enumPackage; MybatisProperties properties = (MybatisProperties) bean; String src = properties.getTypeHandlersPackage(); if (src != null) { // mybatis.type-handlers-package可以接受多个路径,分隔符有多种,英文逗号为其中一种 src += "," + HANDLER_PACKAGE; } else { src = HANDLER_PACKAGE; } properties.setTypeHandlersPackage(src); } return bean; } /** * 获取main方法启动类所在的包 * @return */ public static String getMainClassPackage() { StackTraceElement[] stackTraceElements = new RuntimeException().getStackTrace(); for (StackTraceElement stackTraceElement : stackTraceElements) { if ("main".equals(stackTraceElement.getMethodName())) { String mainClassName = stackTraceElement.getClassName(); return mainClassName.substring(0, mainClassName.lastIndexOf(".")); } } return ""; } }
3.4 使用注意
- 有几个小的注意点,直接看图吧:
① 创建的枚举需要implements IEnum
② 让枚举使用自己的类型处理器
③ 注意包路径修改成自己的:
3.5 参考地址
- 上面自定义枚举类型转换器的代码参考github上的大佬的代码,拿来简单修改即可用,想直接下载的根据下面的链接:
https://github.com/haerxiong/MybatisAutoEnum. - 好了,接下来就是crud的应用了,简单都说说吧,需要的请继续……
4. 使用——自定义的枚举类型转换器
4.1 对于查询
4.1.1 给前端返回的 json
1. 如果不对json做处理
- 直接先看图:
如果不处理,返回的是 GIRL,这不是我们想要的结果,我们希望给前端返回是汉字”女孩“
2. 处理返回的json(对枚举属性项的返回)
(1)不解析枚举的情况
- 对于上面的情况,我这边的处理是,给 SexEnum.java 加
toString()
方法 +SerializerFeature.WriteEnumUsingToString
,如下:
① 直接 toString() 先试试:
看看效果:
这个看着没毛病,但是有的多余,关键是 json 里面有”=“,对前端来说json格式不对,解析困难,所以我们只需要把toString()
方法改一下即可
② 改后,如图:
好了,简单粗暴,但完美解决!
(2)解析枚举的情况
- 如果你觉得上面真的是完美解决,那就真的错了,上面枚举没报错,那是真的巧合,接下来我们换个枚举的key试试,不用0和1,用01,02试试,问题就出现了
报错日志如下:
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `com.liu.susu.enums.SexEnum` from String "02": not one of the values accepted for Enum class: [GIRL, BOY]; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `com.liu.susu.enums.SexEnum` from String "02": not one of the values accepted for Enum class: [GIRL, BOY] at [Source: (PushbackInputStream); line: 1, column: 24] (through reference chain: com.liu.susu.pojo.entity.DogEntity["dogSex"])]
解决问题——解析枚举
- 对于上面的问题,就需要我们再接收json时,对枚举进行解析
- 代码如下:
① EnumUtils .javapackage com.liu.susu.enums.utils; import com.liu.susu.enums.config.IEnum; import lombok.extern.slf4j.Slf4j; import java.lang.reflect.Method; /** * description * @author susu **/ @Slf4j public class EnumUtils { public static <T extends Enum<T>> T valueOf(Class<T> enumType, Object value) { try { Method method = enumType.getMethod("values"); Enum[] enums = (Enum[])method.invoke(enumType); for(int i = 0; i < enums.length; i++) { T e = (T) enums[i]; if (((IEnum)e).getValue().equals(value)) { return e; } } return null; } catch (Exception e) { log.error("method values is not found in " + enumType.getClass().getName(), e); return null; } } }
② MyEnumDeserializer.java
package com.liu.susu.enums.external;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.liu.susu.enums.SexEnum;
import com.liu.susu.enums.utils.EnumUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import java.io.IOException;
/**
* description 解析枚举用
* @author susu
**/
@Slf4j
public class MyEnumDeserializer extends JsonDeserializer<Enum<? extends Enum<?>>> {
public MyEnumDeserializer() {
}
public Enum<? extends Enum<?>> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
try {
String enumValue = jsonParser.getText();
if (StringUtils.isNotEmpty(enumValue)){
return getMyEnum(jsonParser.getCurrentName(), jsonParser.getText());
}else {
return null;
}
} catch (Exception var5) {
throw new RuntimeException("枚举解析错误,字段属性为:" + jsonParser.getCurrentName()
+ ", 传的枚举value为:" + jsonParser.getText());
}
}
/**
* description :根据实体属性 和 接收到的值,返回对应的枚举
*/
public Enum<? extends Enum<?>> getMyEnum(String propertyKey, String enumKey) {
return EnumUtils.valueOf(SexEnum.class, enumKey);
}
}
③ 实体加注解:@JsonDeserialize(using = MyEnumDeserializer.class)
- 效果图:
3. 修改后的代码
- ① 枚举
package com.liu.susu.enums; import com.liu.susu.enums.config.IEnum; /** * @Description: * @Author susu */ public enum SexEnum implements IEnum { GIRL("01","女孩"), BOY("02","男孩"); private String value; private String name; SexEnum(String value, String name) { this.value = value; this.name = name; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return name; } }
- ② mapper.xml文件
- ③ controller
package com.liu.susu.controller.pet; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.serializer.SerializerFeature; import com.liu.susu.service.pet.DogService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; /** * description 狗狗增删改查,领养等业务处理 * @author susu **/ @Controller @RequestMapping("/pet/dog") @Slf4j public class DogController { @Autowired private DogService dogService; @ResponseBody @GetMapping("/selectAllDogs") public String selectAllDogs(){ return JSONObject.toJSONString(dogService.selectAllDogs(), SerializerFeature.WriteEnumUsingToString); } }
4.1.2 接收前端的查询条件
1. 如果dto新实体(属性都是String)
- 这个接收参数就无所谓了,前端给啥就接啥就行了,这个不说了,关键是下面的
2. 如果一直用同一个实体DogEntity
-
如果你的controller参数是DogEntity(属性是枚举类型的实体),需要简单注意一点点(.value别忘了就ok,接收到前端的数据也不用处理,会自动解析的,把数字解析成枚举)
-
看一下效果:
4.2 对于新增
4.2.1 对于接收前端传来的参数值
- 这个同查询是一样的,直接给图了
4.2.2 对于xml中设定的默认值
-
直接看图:
'${@com.liu.susu.enums.SexEnum@GIRL.getValue()}'
4.3 对于修改
- 到这了,没必要说了吧……
4.4 指定 typeHandler(麻烦、不必须)
- 可以在xml文件里指定,但是没必要(除非不生效的情况,可以试试这种情况)
比如新增可以用下面的方式:
<!--3.新增狗狗-->
<insert id="insertDog" parameterType="com.liu.susu.pojo.entity.DogEntity">
insert into dog (dog_name,dog_age,dog_sex)
values
(
#{dogName,jdbcType=VARCHAR},
#{dogAge,jdbcType=INTEGER},
<!-- #{dogSex.value,jdbcType=VARCHAR}-->
#{dogSex,jdbcType=VARCHAR,
javaType=com.liu.susu.enums.SexEnum,
typeHandler=com.liu.susu.enums.config.handler.MybatisEnumHandler}
)
</insert>
5. 附代码
- 项目下载地址:
1.springbood+mybatis项目demo 2.mybatis自定义枚举类型的转换器以及各种使用场景