点击关注公众号:互联网架构师,后台回复 2T获取2TB学习资源!
上一篇:Alibaba开源内网高并发编程手册.pdf
问题
昨天用mybatis-plus
写了一段crud
,代码如下:
@Transactional@Overridepublic boolean updateTaskStatus(Integer taskId, TaskStatusEnum taskStatusEnum) {// 查询任务Task task = taskMapper.selectById(taskId);if (Objects.isNull(task)) {throw new IllegalArgumentException("没有查询到任务!");}// 检查状态是否正常if (!task.getStatus().nextStatus().contains(taskStatusEnum)) {throw new IllegalStateException("不能修改当前任务的状态!");}// 状态正常就修改状态到下一个状态task.setStatus(taskStatusEnum);// 更新任务状态int result = taskMapper.updateById(task);return result > 0;}
结果一直报错:
java.sql.SQLException: Incorrect integer value: 'COMPLETED' for column 'status' at row 1
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:129)
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:916)
at com.mysql.cj.jdbc.ClientPreparedStatement.execute(ClientPreparedStatement.java:354)
at
这个报错的意思是说,我的数据表中对应的status表字段类型是integer value
,但是传进来的值却是COMPLETED
字符串;
通过不断地测试,发现该BUG出现的现象如下:
❝
1.项目重启后,该报错出现在执行
updateById()
这一段代码上;2.报错后如果继续调用该接口,
selectById()
将出现以下报错:❞
org.apache.ibatis.executor.result.ResultMapException: Error attempting to get column 'status' from result set. Cause: java.lang.IllegalArgumentException: No enum constant com.example.awesomespring.enums.TaskStatusEnum.0
at org.apache.ibatis.type.BaseTypeHandler.getResult(BaseTypeHandler.java:87)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.applyAutomaticMappings(DefaultResultSetHandler.java:561)
at
提前申明一下,项目中已经配置了该枚举类型对应的TypeHandler
;
排查过程
DEBUG
自定义TypeHandler
出现第一个报错的时候,第一时间想到的就是
TaskStatusEnum
枚举没有匹配到对应的TypeHandler
,所以在自定义TypeHandler
中打上断点,再次发起请求后,出现了第二个报错,并且该线程并没有进入TypeHandler
的断点当中;此后无论请求多少次,始终在selectById()
上报错;为此不得不重启项目后重新断点,重启项目后第一次请求,在
selectById()
后进入断点,成功拿到解析后的结果task
;并在执行updateById()
前后都未进入断点,此后无论如何请求都没有进入断点;根据上述现象,我有以下两个判断:
❝
1.可能在
selectById()
执行过程中引入了变量导致updateById()
没有找到对应的TypeHandler
;2.可能是
updateById()
产生的错误影响了全局配置,导致后续无论如何都无法找到TypeHandler
;❞
深入源码
我们都知道,
mybatis-plus
也是基于mybatis
实现的,所以mybatis
的那一套理论我们还是用得上的;mybatis
在处理参数和结果集的时候都需要通过TypeHandler
来处理;在
mybatis-plus
中,我们可以找到MybatisParameterHandler.setParameters()
中的这一段代码:TypeHandler typeHandler = parameterMapping.getTypeHandler();
通过
Debug
我们发现它最终拿到的是UnknownTypeHandler
:UnknownTypeHandler 查看
TypeHandlerRegistry
通过上述手段,我们发现
mybatis-plus
确实没有拿到正确的TypeHandler
,这不得不让我们怀疑TypeHandler
是否成功地注册到配置中了,随即我们在Debug
变量表中展开Configuration
对象,准备查看里面的TypeHandler
:TypeHandler 我们在处理完参数后再次查看
TypeHandlerRegistry
,发现该枚举对应的TypeHandler
已经发生改变了:TypeHandlerRegistry 另外值得一提的是,另一个枚举类型对应的
TypeHandler
始终没有改变:TypeHandler 对比枚举类型差异
发现两个枚举类型的不同表现后,我尝试对比一下两个枚举类型的差异:
TaskTypeEnum.java
:
public enum TaskTypeEnum implements IEnum<Integer, String> {QUERY(1, "查询任务"),UPDATE(2, "更新任务");private final Integer code;private final String value;TaskTypeEnum(Integer code, String value) {this.code = code;this.value = value;}@Overridepublic Integer getCode() {return this.code;}@Overridepublic String getValue() {return this.value;}
}
TaskStatusEnum.java
:
public enum TaskStatusEnum implements IEnum<Integer, String> {START(0, "开始") {@Overridepublic List<TaskStatusEnum> nextStatus() {return Arrays.asList(COMPLETED);}},COMPLETED(1, "完成") {@Overridepublic List<TaskStatusEnum> nextStatus() {return Arrays.asList(END);}},END(2, "结束") {@Overridepublic List<TaskStatusEnum> nextStatus() {return null;}};TaskStatusEnum(Integer code, String value) {this.code = code;this.value = value;}private final Integer code;private final String value;@Overridepublic Integer getCode() {return this.code;}@Overridepublic String getValue() {return this.value;}public abstract List<TaskStatusEnum> nextStatus();
}
两者的不同点在于出问题的枚举有一个抽象方法,每个实例都要实现该抽象方法;为此我尝试把第二个枚举改造成和第一个枚举一样,删掉抽象方法后,重新调用接口,竟然真的成功了!
定位问题
对于这种莫名其妙的情况下就把问题解决了,我是不甘心的。当我准备找到问题根源重新DEBUG
时,无意间我发现了一点小小的线索:
这个传进来的枚举类型是TaskStatusEnum$2.class
,它应该是TaskStatusEnum.class
;一开始我以为是spring mvc
在做请求参数解析的时候做了一层包装,我尝试把代码改成这样:
task.setStatus(TaskStatusEnum.valueOf(taskStatusEnum.name()));
结果发现我的猜测是错误的,类型依旧是TaskStatusEnum$2.class
;只有当枚举中没有抽象方法时,类型才是正确的;
并且我们发现,TaskStatusEnum
中所有的实例类型都不一样:
解释现象
为了能够了解这个现象出现的原因,我简单看了一下源码,大概过程如下:
private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMap(Type type) {// 传进来的type为TaskStatusEnum$2.class,jdbcHandlerMap为nullMap<JdbcType, TypeHandler<?>> jdbcHandlerMap = (Map)this.typeHandlerMap.get(type);if (jdbcHandlerMap != null) {return NULL_TYPE_HANDLER_MAP.equals(jdbcHandlerMap) ? null : jdbcHandlerMap;} else {if (type instanceof Class) {Class<?> clazz = (Class)type;// 判断是否时枚举类型if (Enum.class.isAssignableFrom(clazz)) {// TaskStatusEnum$2.class是匿名类,所以找到父类TaskStatusEnum.classClass<?> enumClass = clazz.isAnonymousClass() ? clazz.getSuperclass() : clazz;// 返回null,至于为啥直接返回null,一看代码便知jdbcHandlerMap = this.getJdbcHandlerMapForEnumInterfaces(enumClass, enumClass);if (jdbcHandlerMap == null) {// 给这个类型TaskStatusEnum.class注册上默认的枚举类型处理器this.register(enumClass, this.getInstance(enumClass, this.defaultEnumTypeHandler));// 返回默认的枚举类型处理器EnumTypeHandlerreturn (Map)this.typeHandlerMap.get(enumClass);}} else {jdbcHandlerMap = this.getJdbcHandlerMapForSuperclass(clazz);}}this.typeHandlerMap.put(type, jdbcHandlerMap == null ? NULL_TYPE_HANDLER_MAP : jdbcHandlerMap);return jdbcHandlerMap;}
}
我们可以简单总结一下:
❝
1.
mybatis-plus
处理枚举类型参数时,是直接通过传进来的参数值对应的类型去TypeHandlerRegistry
中查找对应的TypeHandler
的;2.当没有找到该匿名类型对应的
TypeHandler
时,获取了父类类型,但是getJdbcHandlerMapForEnumInterfaces()
显然是从枚举中的接口去找对应的TypeHandler
,这一步让这个匿名类枚举实例完美地错过了它的TypeHandler
;3.最后一步是指定了默认的枚举类型处理器
org.apache.ibatis.type.EnumTypeHandler
,并且执行力register()
操作,那么TypeHandlerRegistry
中TaskStatusEnum.class
对应的TypeHandler
被修改了;❞
根据上述总结,我们对于前面的问题就很好理解了,正是因为匿名枚举类型造成了TypeHandler
被动态修改了,才导致了后面无论如何执行,都无法成功地执行selectById()
,因为在结果集解析时,通过TaskStatusEnum.class
找到的org.apache.ibatis.type.EnumTypeHandler
无法构建TaskStatusEnum
实例;
对比mybatis
当今天我希望通过mybatis
复现该问题时,我发现mybatis
完全没有问题,说明这个问题仅仅出现在mybatis-plus
上面,完全不是mybatis
的锅,也不是枚举的锅;
通过对比发现,mybatis
在项目启动时,就已经把对应的实体类中属性字段类型和TypeHandler
放进缓存中了,在SQL
执行阶段,直接拿出对应的TypeHandler
来处理参数值,它的参数解析是不依赖参数类型的;而Mybatis-plus
是通过参数类型从TypeHandlerRegistry
中取TypeHandler
的,这就导致了获取到不正确的TypeHandler
;
Mybatis
中status
字段对应的参数类型:
metaClass
是已经解析好的实体类元数据,可以直接从里面获取对应的属性字段类型;
Mybatis-Plus
中status
字段对应的数据类型:
mybatis-plus
会将参数包装成ParamMap
类型,导致返回的数据类型是Object.class
,最后匿名枚举类型匹配不到TypeHandler
,导致BUG
出现;
如何避免
现在我们已经知道了导致这个问题的原因了,也就很容易就给出以下解决方案:
❝
1.在使用枚举时,尽可能不要使用抽象方法,导致枚举实例都是匿名类型;这是代价最小的方案;
2.直接给父级接口配置
TypeHandler
;因为它找不到匿名类对应的TypeHandler
就会找父级接口对应的TypeHandler
;这个也算是比较好的解决方案了;3.动态地给所有的匿名类型也配置上
TypeHandler
;代价也很小,调用TypeHandlerRegistry.register
就可以;4.将
mybatis
替换掉mybatis-plus
;这个代价很大,意味着你的项目中要改很多代码以及调整相关配置;❞
来源:juejin.cn/post/7173449167618965534
最后,关注公众号互联网架构师,在后台回复:2T,可以获取我整理的 Java 系列面试题和答案,非常齐全。
正文结束
推荐阅读 ↓↓↓
1.ChatGPT 遭教育部“拉黑”:师生禁用!
2.从零开始搭建创业公司后台技术栈
3.程序员一般可以从什么平台接私活?
4.流程引擎的架构设计
5.为什么国内 996 干不过国外的 955呢?
6.中国的铁路订票系统在世界上属于什么水平?
7.15张图看懂瞎忙和高效的区别!