MyBatis遇到了No constructor found in xxx No constructor found

王守钰 32 2022-06-26

背景

某个业务的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;

}

# Mybatis