背景
某个业务的CRUD操作。
异常信息
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.executor.ExecutorException: No constructor found in com.example.grpclearn.MyTest matching [java.math.BigInteger, java.lang.String]
at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:77) ~[mybatis-spring-1.3.2.jar:1.3.2]
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446) ~[mybatis-spring-1.3.2.jar:1.3.2]
at com.sun.proxy.$Proxy67.selectList(Unknown Source) ~[na:na]
at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:230) ~[mybatis-spring-1.3.2.jar:1.3.2]
at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:139) ~[mybatis-3.4.6.jar:3.4.6]
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:76) ~[mybatis-3.4.6.jar:3.4.6]
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59) ~[mybatis-3.4.6.jar:3.4.6]
at com.sun.proxy.$Proxy70.all(Unknown Source) ~[na:na]
at com.example.grpclearn.TestApplication.init(TestApplication.java:31) ~[classes/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:84) ~[spring-context-5.3.16.jar:5.3.16]
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) ~[spring-context-5.3.16.jar:5.3.16]
at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:95) ~[spring-context-5.3.16.jar:5.3.16]
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515) ~[na:na]
at java.base/java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:264) ~[na:na]
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java) ~[na:na]
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]
Caused by: org.apache.ibatis.executor.ExecutorException: No constructor found in com.example.grpclearn.MyTest matching [java.math.BigInteger, java.lang.String]
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createByConstructorSignature(DefaultResultSetHandler.java:668) ~[mybatis-3.4.6.jar:3.4.6]
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createResultObject(DefaultResultSetHandler.java:621) ~[mybatis-3.4.6.jar:3.4.6]
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createResultObject(DefaultResultSetHandler.java:594) ~[mybatis-3.4.6.jar:3.4.6]
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getRowValue(DefaultResultSetHandler.java:396) ~[mybatis-3.4.6.jar:3.4.6]
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValuesForSimpleResultMap(DefaultResultSetHandler.java:355) ~[mybatis-3.4.6.jar:3.4.6]
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValues(DefaultResultSetHandler.java:330) ~[mybatis-3.4.6.jar:3.4.6]
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSet(DefaultResultSetHandler.java:303) ~[mybatis-3.4.6.jar:3.4.6]
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSets(DefaultResultSetHandler.java:196) ~[mybatis-3.4.6.jar:3.4.6]
at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:64) ~[mybatis-3.4.6.jar:3.4.6]
at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:79) ~[mybatis-3.4.6.jar:3.4.6]
at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:63) ~[mybatis-3.4.6.jar:3.4.6]
at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:326) ~[mybatis-3.4.6.jar:3.4.6]
at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156) ~[mybatis-3.4.6.jar:3.4.6]
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109) ~[mybatis-3.4.6.jar:3.4.6]
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:83) ~[mybatis-3.4.6.jar:3.4.6]
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:148) ~[mybatis-3.4.6.jar:3.4.6]
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141) ~[mybatis-3.4.6.jar:3.4.6]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433) ~[mybatis-spring-1.3.2.jar:1.3.2]
... 21 common frames omitted
环境
数据库使用的是mysql的5.7版本;springboot版本是2.6.4;MySQL connector使用的版本是8.0.11;mybatis springboot使用的版本是1.3.2;项目中还使用到了lombok插件
MySQL
5.7.26-29-log
select version();
SpringBoot
2.6.4
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
MySQL connector
8.0.11
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
Mybatis springboot
1.3.2
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
源码部分
配置yaml
spring:
application:
name: test-server
profiles:
active: dev
datasource:
url: jdbc:mysql://localhost:3306/bh_server?socketTimeout=10000&serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
SQL脚本
CREATE TABLE `my_test` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id',
`app_name` varchar(45) NOT NULL DEFAULT '' COMMENT '应用名称',
PRIMARY KEY (`id`)
) ENGINE=InnoDB
实体类
import lombok.*;
@Data
@Builder
@ToString
public class MyTest {
/**
* 主键ID
*/
private Long id;
/**
* 应用名称
*/
private String appName;
}
Mapper类
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface MyTestMapper {
@Select("select * from my_test limit 10")
List<MyTest> all();
}
测试类
@Autowired
private MyTestMapper mapper;
public void init(){
List<MyTest> res = mapper.all();
System.out.println(res);
}
问题排查
发现问题的是没有找到构造方法No constructor found in com.example.grpclearn.MyTest matching [java.math.BigInteger, java.lang.String]
问题1——哪里来的BigInteger
针对于这条消息来说我们MyTest
实体类中压根就不存在BigInteger
类型的值。正常理解mysql中的bigint
对应java类中的数据类型就是为Long
为什么会是BigInteger
呢?查询下bigint
的取值范围,-2^63 (-9223372036854775808) ~ 2^63 - 1 (9223372036854775807),java中Long
的数值范围-2^63 (-9223372036854775808) ~ 2^63 - 1 (9223372036854775807),这么看也是没有问题的,但是问题的本质出现在了我们sql的脚本上加了一个无符号的限制unsigned
,这样的话数值范围也就出现了变化,变成了0 ~ 2^64 - 1(18446744073709551615),那么这时候,java中Long
数据类型也就没有办法接收这个值,不得不用BigInteger
来进行接收。进一步查看com.mysql.cj.MysqlType
枚举。
/**
* BIGINT[(M)] [UNSIGNED] [ZEROFILL]
* A large integer. The signed range is -9223372036854775808 to 9223372036854775807. The unsigned range is 0 to 18446744073709551615.
*
* Protocol: FIELD_TYPE_LONGLONG = 8
*
* SERIAL is an alias for BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE.
*/
BIGINT("BIGINT", Types.BIGINT, Long.class, MysqlType.FIELD_FLAG_ZEROFILL, MysqlType.IS_DECIMAL, 19L, "[(M)] [UNSIGNED] [ZEROFILL]"),
/**
* BIGINT[(M)] UNSIGNED [ZEROFILL]
*
* @see MysqlType#BIGINT
*/
BIGINT_UNSIGNED("BIGINT UNSIGNED", Types.BIGINT, BigInteger.class, MysqlType.FIELD_FLAG_UNSIGNED | MysqlType.FIELD_FLAG_ZEROFILL, MysqlType.IS_DECIMAL, 20L,
"[(M)] [UNSIGNED] [ZEROFILL]"),
这里就可以看出来,BIGINT_UNSIGNED直接对应的就是BigInteger
。
问题2——印象中即使是加了unsigned也是没有问题的啊
顺着报错的信息找问题吧,at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createByConstructorSignature(DefaultResultSetHandler.java:668) ~[mybatis-3.4.6.jar:3.4.6]
,查看下这个报错的原因。
DefaultResultSetHandler
private Object createByConstructorSignature(ResultSetWrapper rsw, Class<?> resultType, List<Class<?>> constructorArgTypes, List<Object> constructorArgs,
String columnPrefix) throws SQLException {
// 查找返回类型的所有构造方法
final Constructor<?>[] constructors = resultType.getDeclaredConstructors();
final Constructor<?> annotatedConstructor = findAnnotatedConstructor(constructors);
if (annotatedConstructor != null) {
return createUsingConstructor(rsw, resultType, constructorArgTypes, constructorArgs, columnPrefix, annotatedConstructor);
} else {
// 循环所有构造方法
for (Constructor<?> constructor : constructors) {
// 是否允许构造
if (allowedConstructor(constructor, rsw.getClassNames())) {
return createUsingConstructor(rsw, resultType, constructorArgTypes, constructorArgs, columnPrefix, constructor);
}
}
}
throw new ExecutorException("No constructor found in " + resultType.getName() + " matching " + rsw.getClassNames());
}
private boolean allowedConstructor(final Constructor<?> constructor, final List<String> classNames) {
// 获取构造方法中的所有数据类型
final Class<?>[] parameterTypes = constructor.getParameterTypes();
// 实体类中构造方法的数据类型和db返回的数据类型是否匹配
if (typeNames(parameterTypes).equals(classNames)) return true;
if (parameterTypes.length != classNames.size()) return false;
for (int i = 0; i < parameterTypes.length; i++) {
final Class<?> parameterType = parameterTypes[i];
if (parameterType.isPrimitive() && !primitiveTypes.getWrapper(parameterType).getName().equals(classNames.get(i))) {
return false;
} else if (!parameterType.isPrimitive() && !parameterType.getName().equals(classNames.get(i))) {
return false;
}
}
return true;
}
这时候就发现,实体中的数据类型和db中返回的数据类型并不匹配,所以这段代码执行就抛出了ExecutorException
异常。通过网上查的一些资料说是mybatis的版本太低了。我看下这时候mybatis的版本是3.4.6。我升级了下mybaits的版本,果然解决了这个问题。从3.5.0版本后,createByConstructorSignature
方法有所改动。
DefaultResultSetHandler 3.5.0
private Object createByConstructorSignature(ResultSetWrapper rsw, Class<?> resultType, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) throws SQLException {
final Constructor<?>[] constructors = resultType.getDeclaredConstructors();
final Constructor<?> defaultConstructor = findDefaultConstructor(constructors);
if (defaultConstructor != null) {
return createUsingConstructor(rsw, resultType, constructorArgTypes, constructorArgs, defaultConstructor);
} else {
for (Constructor<?> constructor : constructors) {
// 这步变更为根据jdbc的类型来进行处理
if (allowedConstructorUsingTypeHandlers(constructor, rsw.getJdbcTypes())) {
return createUsingConstructor(rsw, resultType, constructorArgTypes, constructorArgs, constructor);
}
}
}
throw new ExecutorException("No constructor found in " + resultType.getName() + " matching " + rsw.getClassNames());
}
private boolean allowedConstructorUsingTypeHandlers(final Constructor<?> constructor, final List<JdbcType> jdbcTypes) {
final Class<?>[] parameterTypes = constructor.getParameterTypes();
if (parameterTypes.length != jdbcTypes.size()) {
return false;
}
for (int i = 0; i < parameterTypes.length; i++) {
// 判断构造方法中的类型和jdbc对应的数据类型是否匹配
if (!typeHandlerRegistry.hasTypeHandler(parameterTypes[i], jdbcTypes.get(i))) {
return false;
}
}
return true;
}
升级版本后也解决了问题,又往上追了下代码,又发现了新的问题。at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createResultObject(DefaultResultSetHandler.java:621) ~[mybatis-3.4.6.jar:3.4.6]
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix)
throws SQLException {
// 返回值类型
final Class<?> resultType = resultMap.getType();
final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory);
final List<ResultMapping> constructorMappings = resultMap.getConstructorResultMappings();
if (hasTypeHandlerForResultObject(rsw, resultType)) {
return createPrimitiveResultObject(rsw, resultMap, columnPrefix);
} else if (!constructorMappings.isEmpty()) {
return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs, columnPrefix);
} else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {
// 判断是否有默认的构造方法
return objectFactory.create(resultType);
} else if (shouldApplyAutomaticMappings(resultMap, false)) {
return createByConstructorSignature(rsw, resultType, constructorArgTypes, constructorArgs, columnPrefix);
}
throw new ExecutorException("Do not know how to create an instance of " + resultType);
}
这里面也就出现新的问题,为什么没有走,默认的构造方法。
问题3——为什么没有走默认的构造方法?
我把项目进行打了下包,看下MyTest
类编译后的结果。
package com.example.grpclearn;
public class MyTest {
private Long id;
private String appName;
MyTest(final Long id, final String appName) {
this.id = id;
this.appName = appName;
}
public static MyTest.MyTestBuilder builder() {
return new MyTest.MyTestBuilder();
}
public Long getId() {
return this.id;
}
public String getAppName() {
return this.appName;
}
public void setId(final Long id) {
this.id = id;
}
public void setAppName(final String appName) {
this.appName = appName;
}
public boolean equals(final Object o) {
if (o == this) {
return true;
} else if (!(o instanceof MyTest)) {
return false;
} else {
MyTest other = (MyTest)o;
if (!other.canEqual(this)) {
return false;
} else {
Object this$id = this.getId();
Object other$id = other.getId();
if (this$id == null) {
if (other$id != null) {
return false;
}
} else if (!this$id.equals(other$id)) {
return false;
}
Object this$appName = this.getAppName();
Object other$appName = other.getAppName();
if (this$appName == null) {
if (other$appName != null) {
return false;
}
} else if (!this$appName.equals(other$appName)) {
return false;
}
return true;
}
}
}
protected boolean canEqual(final Object other) {
return other instanceof MyTest;
}
public int hashCode() {
int PRIME = true;
int result = 1;
Object $id = this.getId();
int result = result * 59 + ($id == null ? 43 : $id.hashCode());
Object $appName = this.getAppName();
result = result * 59 + ($appName == null ? 43 : $appName.hashCode());
return result;
}
public String toString() {
Long var10000 = this.getId();
return "MyTest(id=" + var10000 + ", appName=" + this.getAppName() + ")";
}
public static class MyTestBuilder {
private Long id;
private String appName;
MyTestBuilder() {
}
public MyTest.MyTestBuilder id(final Long id) {
this.id = id;
return this;
}
public MyTest.MyTestBuilder appName(final String appName) {
this.appName = appName;
return this;
}
public MyTest build() {
return new MyTest(this.id, this.appName);
}
public String toString() {
return "MyTest.MyTestBuilder(id=" + this.id + ", appName=" + this.appName + ")";
}
}
}
这时候发现只有MyTest(final Long id, final String appName)
构造方法,并没有默认的构造方法。进行添加@NoArgsConstructor
注解,这时候编译器直接编译不通过,还需要再添加一个@AllArgsConstructor
。或者去掉@Builder
注解,因为@Builder
注解会生成一个全部参数的构造方法。
import lombok.*;
@Data
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class MyTest {
/**
* 主键ID
*/
private Long id;
/**
* 应用名称
*/
private String appName;
}