关于java:Mybatis技术内幕SpringBoot下自定义枚举的TypeHandler及原理

68次阅读

共计 16493 个字符,预计需要花费 42 分钟才能阅读完成。

原比较简单的形式自定义 TypeHandler

http://followtry.cn/2016-08-1…

背景

因 Mybatis 默认的 Enum 的 TypeHandler 仅反对 org.apache.ibatis.type.EnumTypeHandler 或者org.apache.ibatis.type.EnumOrdinalTypeHandler。但因为很多业务中定义类型应用的是枚举,而数据库中存储的字段是 int 或 varchar 类型。个别不应用枚举默认的 name 或者 ordinal 作为数据库内的值存储。因而在很多应用应用 mybatis 存储枚举时都须要手动取出枚举的 int 值(以取出 int 类型自定义 code 属性为例),在后续保护起来不容易。因而想通过自定义的枚举类型来实现对 int 和映射枚举之间的双向转换。

自定义 EnumTypeHandler 实际计划

如果应用自定义的枚举处理器,须要枚举都实现一个固定的接口,通过该接口办法来获取 int 值

自定义枚举须要实现的接口

接口名为 BaseBizEnum

/**
 * @author followtry
 * @since 2021/8/9 3:30 下午
 */
public interface BaseBizEnum {Integer getCode();
}

实现接口的自定义枚举

自定义枚举为 AgreementType,实现了 BaseBizEnum,其 getCode 办法被标记上 @Override 注解

import com.google.common.collect.Maps;
import lombok.Getter;
import java.util.Map;
import java.util.Optional;

public enum AgreementType implements BaseBizEnum{
    /***/
    QUICK_PAY(1,"免密领取"),
    ;

    private final Integer code;
    @Getter
    private final String desc;
    private static Map<Integer,AgreementType> itemMap = Maps.newHashMap();
    static {for (AgreementType typeEnum : AgreementType.values()) {itemMap.put(typeEnum.getCode(),typeEnum);
        }
    }
    AgreementType(Integer code, String desc) {
        this.code = code;
        this.desc = desc;
    }
    // 重写了接口 BaseBizEnum 的办法
    @Override
    public Integer getCode() {return code;}
    public static AgreementType ofNullable(Integer code) {return itemMap.get(code);
    }
}

有了自定义枚举后,就须要有自定义枚举的类型来解析该枚举

定义枚举类处理器

对于同类型的枚举,能够定义基类的解决类实现通用的逻辑。

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

/**
 * @author followtry
 * @since 2021/8/9 3:38 下午
 */
public class BizEnumTypeHandler<E extends BaseBizEnum> extends BaseTypeHandler<E> {

    private Class<E> type;

    // 初始化时定义枚举和 code 的映射关系
    private final Map<Integer,E> enumsMap = new HashMap<>();

    public BizEnumTypeHandler(Class<E> type) {if (type == null) {throw new IllegalArgumentException("Type argument cannot be null");
        }
        this.type = type;
        for (E enumConstant : type.getEnumConstants()) {enumsMap.put(enumConstant.getCode(),enumConstant);
        }
        if (this.enumsMap.size() == 0) {throw new IllegalArgumentException(type.getSimpleName() + "does not represent an enum type.");
        }
    }

    // 在申请 Sql 执行时转换参数
    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, E e, JdbcType jdbcType) throws SQLException {preparedStatement.setInt(i,e.getCode());
    }

    // 解决返回后果
    @Override
    public E getNullableResult(ResultSet resultSet, String columnName) throws SQLException {if (resultSet.wasNull()) {return null;}
        int code = resultSet.getInt(columnName);
        return getEnum(code);
    }

    private E getEnum(Integer code) {
        try {return getEnumByValue(code);
        } catch (Exception ex) {
            throw new IllegalArgumentException("Cannot convert" + code + "to" + type.getSimpleName() + "by ordinal value.", ex);
        }
    }

    protected E getEnumByValue(Integer code) {return enumsMap.get(code);
    }

    @Override
    public E getNullableResult(ResultSet resultSet, int columnIndex) throws SQLException {if (resultSet.wasNull()) {return null;}
        int code = resultSet.getInt(columnIndex);
        return getEnum(code);
    }

    @Override
    public E getNullableResult(CallableStatement callableStatement, int columnIndex) throws SQLException {if (callableStatement.wasNull()) {return null;}
        int code = callableStatement.getInt(columnIndex);
        return getEnum(code);
    }
}

当初枚举解决的基类有了,就须要通过继承该基类实现自定义枚举的解决类

创立 AgreementTypeEnumTypeHandler 类

import com.autonavi.aos.tmp.api.enums.AgreementType;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;

 // 指定解决的映射枚举类的 class
@MappedTypes(value = {AgreementType.class})
// 指定返回后果时哪些 jdbc 类型的值须要转换
@MappedJdbcTypes(value = {JdbcType.INTEGER,JdbcType.TINYINT,JdbcType.SMALLINT})
public class AgreementTypeEnumTypeHandler extends BizEnumTypeHandler<AgreementType>{
    // 在以后类实例化时即给父类的构造方法指定枚举类
    public AgreementTypeEnumTypeHandler() {super(AgreementType.class);
    }
}

定义完如上的代码后,还须要 Mybatis 在初始化时能将其扫描到并注册进 Mybatis 的 TypeHandler 注册器能力实现解析。

扫描自定义的 TypeHandler 类

因为应用的是 SpringBoot 和 Mybatis 集成的形式,所以在 application.properties 文件中须要指定扫描的目录,以便能辨认到AgreementTypeEnumTypeHandler.

mybatis.type-handlers-package=cn.followtry.typehandler
mybatis.type-aliases-package=cn.followtry.typehandler
mybatis.configuration.map-underscore-to-camel-case=true

如上步骤实现后,就能够实现枚举字段在存储进 DB 前,主动转换为 int 类型。从 db 中查问出的对应字段,主动转为枚举类型。

示例 Sql 配置

建表语句

CREATE TABLE `test_agreement_info` (
    `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
    `agreement_type` tinyint NULL COMMENT '协定类型',
    `name` varchar(100) NULL COMMENT '协定名称',
    PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET=utf8mb4 COMMENT='协定信息';

Mybatis 的 Mapper 接口

import cn.followtry.AgreementType;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface TestAgreementInfoMapper {@Insert("insert into test_agreement_info(name,agreement_type) value(#{name},#{agreementType})")
    boolean insert(@Param("name") String name, @Param("agreementType") AgreementType agreementType);

    @Select("select * from test_agreement_info where name = #{name}")
    List<TestAgreementModel> selectByName(@Param("name") String name);
}

测试 Controller 代码

@RestController
@RequestMapping(value = "/ws/tc/test", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public class TestController2 {
    @Autowired
    private TestAgreementInfoMapper testAgreementInfoMapper;
    @GetMapping(value = "insertTest")
    public Object insertTest(String name, AgreementType agreementType) {System.out.println("name="+name+",agreementType="+agreementType.name());
        return testAgreementInfoMapper.insert(name,agreementType);
    }
    @GetMapping(value = "getTest")
    public Object getTest(String name) {return testAgreementInfoMapper.selectByName(name);
    }
}

服务启动起来后通过接口调用别离插入和查问数据

http://localhost:8080/ws/tc/test/insertTest?name=zhangsan&agreementType=QUICK_PAY
http://localhost:8080/ws/tc/test/getTest?name=zhangsan

以上代码会将插入时的 QUICK_PAY 转为 1 存储在 db 中,查问时会将 1 转为枚举 AgreementType 的实例 QUICK_PAY

上面的内容默认读者已理解 SpringBoot 的 AutoConfiguration 机制,理解 Mybatis 的加载机制,理解 Mybatis 的 Configuration 和 SQLSessionFactory 的初始化过程。

原理解析

在利用启动时,SpringBoot 会驱动 Mybatis 进行初始化,并将扩大的枚举类加载进 Mybatis 的内核机制中。

在 Sql 执行时,Mybatis 会动静将枚举参数替换为 int 类型,并将返回后果中的对应 int 类型转换为对应的枚举

首先,初始化阶段

加载MybatisAutoConfiguration

SpringBoot 会主动加载 MybatisAutoConfiguration 类,依照 Spring 的规定会将该类进行 Bean 的转换和注册。

Spring 通过 MybatisAutoConfiguration#sqlSessionFactory 办法初始化 SqlSessionFactoryBean 的参数设置,此时会将配置的参数解析设置给其属性。

参数如下

mybatis.type-handlers-package=cn.followtry.typehandler
mybatis.type-aliases-package=cn.followtry.typehandler
mybatis.configuration.map-underscore-to-camel-case=true

在办法的最初 SqlSessionFactoryBean 会调用 getObject 办法 (Spring 的 FactoryBean 机制) 执行 SessionFactory 的实例化,此时会初始化默认配置和扫描曾经配置的 TypeHandler 包cn.followtry.typehandler

    if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
    }

SqlSessionFactoryBean#buildSqlSessionFactory 办法内,有如下代码片段将自定义的 AgreementTypeEnumTypeHandler 解析并实例化注册到注册器中。

    if (hasLength(this.typeHandlersPackage)) {scanClasses(this.typeHandlersPackage, TypeHandler.class).stream().filter(clazz -> !clazz.isAnonymousClass())
          .filter(clazz -> !clazz.isInterface()).filter(clazz -> !Modifier.isAbstract(clazz.getModifiers()))
          .filter(clazz -> ClassUtils.getConstructorIfAvailable(clazz) != null)
          .forEach(targetConfiguration.getTypeHandlerRegistry()::register);
    }

scanClasses办法是针对将指定包下的指定类型的子类都收集到候选的汇合中。

targetConfiguration.getTypeHandlerRegistry()::register会将过滤后的 TypeHandler 子类即 AgreementTypeEnumTypeHandler 注册进容器。

注册代码如下


  /**
  *  先查看 `AgreementTypeEnumTypeHandler` 上的 MappedTypes 注解。有注解的以注解注入。*  仅仅反对以后类上的注解,不反对父类上的。如果没有指定 MappedTypes 注解,则无奈判断该处理器解决哪个枚举,只能在 xml 配置 Sql 时指定 TypeHandler
  */
  public void register(Class<?> typeHandlerClass) {
    boolean mappedTypeFound = false;
    MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
    if (mappedTypes != null) {for (Class<?> javaTypeClass : mappedTypes.value()) {register(javaTypeClass, typeHandlerClass);
        mappedTypeFound = true;
      }
    }
    if (!mappedTypeFound) {register(getInstance(null, typeHandlerClass));
    }
  }
  public void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) {register(javaTypeClass, getInstance(javaTypeClass, typeHandlerClass));
  }
  
  //register 办法参数上调用了本办法,在本办法内通过构造方法反射生成 TypeHandler 的实例
  public <T> TypeHandler<T> getInstance(Class<?> javaTypeClass, Class<?> typeHandlerClass) {if (javaTypeClass != null) {
      try {
          // 如果存在带有 class 参数的构造方法,则应用其生成实例,否则应用无参构造方法生成实例
        Constructor<?> c = typeHandlerClass.getConstructor(Class.class);
        return (TypeHandler<T>) c.newInstance(javaTypeClass);
      } catch (NoSuchMethodException ignored) {// ignored} catch (Exception e) {throw new TypeException("Failed invoking constructor for handler" + typeHandlerClass, e);
      }
    }
    try {
        // 因为本示例中的 AgreementTypeEnumTypeHandler 只有无参构造方法,因而只能通过此处代码生成实例
      Constructor<?> c = typeHandlerClass.getConstructor();
      return (TypeHandler<T>) c.newInstance();} catch (Exception e) {throw new TypeException("Unable to find a usable constructor for" + typeHandlerClass, e);
    }
  }


  
  public <T> void register(Class<T> type, JdbcType jdbcType, TypeHandler<? extends T> handler) {register((Type) type, jdbcType, handler);
  }

  // 注册最终会调用该办法将 AgreementTypeEnumTypeHandler 注册进 typeHandlerMap 中,typeHandlerMap 实质是个 Map,用来作为 typeHandler 的容器
  // 而对于没有指定类型的 TypeHandler,则注册进 allTypeHandlersMap 中,在 sql 配置中指定后能力应用。//typeHandlerMap 容器的 key 为 MappedTypes 注解指定的枚举类,value 为 MappedJdbcTypes 指定的 jdbc 类型和 AgreementTypeEnumTypeHandler 实例的映射
  private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {if (javaType != null) {Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
      if (map == null || map == NULL_TYPE_HANDLER_MAP) {map = new HashMap<>();
        typeHandlerMap.put(javaType, map);
      }
      map.put(jdbcType, handler);
    }
    allTypeHandlersMap.put(handler.getClass(), handler);
  }

额定看下 Mybatis 默认加载的处理器(非全副)

public TypeHandlerRegistry() {register(Boolean.class, new BooleanTypeHandler());
    register(boolean.class, new BooleanTypeHandler());
    register(JdbcType.BOOLEAN, new BooleanTypeHandler());
    register(JdbcType.BIT, new BooleanTypeHandler());

    register(Byte.class, new ByteTypeHandler());
    register(byte.class, new ByteTypeHandler());
    register(JdbcType.TINYINT, new ByteTypeHandler());

    register(Short.class, new ShortTypeHandler());
    register(short.class, new ShortTypeHandler());
    register(JdbcType.SMALLINT, new ShortTypeHandler());

    register(Integer.class, new IntegerTypeHandler());
    register(int.class, new IntegerTypeHandler());
    register(JdbcType.INTEGER, new IntegerTypeHandler());

    register(Long.class, new LongTypeHandler());
    register(long.class, new LongTypeHandler());

    register(Float.class, new FloatTypeHandler());
    register(float.class, new FloatTypeHandler());
    register(JdbcType.FLOAT, new FloatTypeHandler());

    register(Double.class, new DoubleTypeHandler());
    register(double.class, new DoubleTypeHandler());
    register(JdbcType.DOUBLE, new DoubleTypeHandler());
    ....
}

那初始化加载完后,什么时候设置参数将枚举转为 int 呢,接着往下看。

执行 Sql,将枚举参数转为 int

从新将咱们的 Sql 代码搬过去, 依据 Mybatis 的机制,如果参数应用 #{} 而非 ${} 则会应用PreparedStatement,如果应用${}, 则 Mybatis 中的类型处理器是不失效的,此处不留神的话可能会踩坑。

@Mapper
public interface TestAgreementInfoMapper {@Insert("insert into test_agreement_info(name,agreement_type) value(#{name},#{agreementType})")
    boolean insert(@Param("name") String name, @Param("agreementType") AgreementType agreementType);

    @Select("select * from test_agreement_info where name = #{name}")
    List<TestAgreementModel> selectByName(@Param("name") String name);
}

@Data
public class TestAgreementModel {
    private String name;

    private AgreementType agreementType;
}

对于 Mybatis 的参数解决接口 ParameterHandler, 其有默认实现DefaultParameterHandler, 而参数的转换就是在DefaultParameterHandler#setParameters 中实现的。

代码如下

  @Override
  public void setParameters(PreparedStatement ps) {ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    //Mybatis 通过 Param 注解解析到的参数映射,因为咱们没在 xml 配置中指定 jdbc 类型和 TypeHandler 类型,因而在此办法外部获取 TypeHandler 是 UnknownTypeHandler
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {for (int i = 0; i < parameterMappings.size(); i++) {ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {value = null;} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {value = parameterObject;} else {MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
          // 此处 parameterMapping 没有解析到设置的 TypeHandler 和 JdbcType,因为压根就没设置,然而不障碍 Mybatis 推断出应用的 TypeHanler,对于没有配置 TypeHandler 的,Mybatis 有默认实现 UnknownTypeHandler
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
              // 应用的 UnknownTypeHandler 来设置未获取到相干配置的参数
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {throw new TypeException("Could not set parameters for mapping:" + parameterMapping + ". Cause:" + e, e);
          }
        }
      }
    }
  }

在应用 UnknownTypeHandler 设置参数外部,还会依据 java 类型和 jdbc 类型再找一次以后枚举类型的处理器。代码如下

public class UnknownTypeHandler extends BaseTypeHandler<Object> {
  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
      throws SQLException {TypeHandler handler = resolveTypeHandler(parameter, jdbcType);
    //AgreementTypeEnumTypeHandler 实例调用 setParameter 办法,就进入了咱们自定义的 AgreementTypeEnumTypeHandler 中,依照咱们的逻辑将枚举通过 getCode 办法转为了 int 类型
    handler.setParameter(ps, i, parameter, jdbcType);
  }

  // 获取的具体枚举的类型处理器
  private TypeHandler<?> resolveTypeHandler(Object parameter, JdbcType jdbcType) {
    TypeHandler<?> handler;
    if (parameter == null) {handler = OBJECT_TYPE_HANDLER;} else {
        // 如果 jdbcType 还是为 null,则取其惟一的一个类型处理器实例。其实在 Mybatis 加载时多个 jdbcType 对应的 TypeHandler 实例是雷同的
        // 因而就能获取到 AgreementTypeEnumTypeHandler 实例
      handler = typeHandlerRegistry.getTypeHandler(parameter.getClass(), jdbcType);
      // check if handler is null (issue #270)
      if (handler == null || handler instanceof UnknownTypeHandler) {handler = OBJECT_TYPE_HANDLER;}
    }
    // 此处返回的是 AgreementTypeEnumTypeHandler 实例
    return handler;
  }
}

对于 Insert 的枚举参数,通过下面的一系列代码的执行,曾经实现了枚举和 int 类型的转换了。接下来再通过查问的办法,看下返回后果是怎么将 int 转换为枚举的。

执行查问 Sql,将 int 转为枚举

在执行查问时,同样会先调用 SimpleExecutor#prepareStatement 办法,其外部调用 ParameterHandler#setParameters 实现参数的转换。
参数转换完后,执行 sql 并获取到了 jdbc 返回的后果 ResultSet,而后将 ResultSet 转为指定的类型。

解决 ResultSet 后果有个 ResultSetHandler 接口,在默认实现 DefaultResultSetHandler#handleResultSets 接口中将 ResultSet 转换为理论的类型实例。

DefaultResultSetHandler#getRowValue办法是理论将一行 Sql 数据映射为后果类型的入口。

DefaultResultSetHandler#getRowValue 办法外部,会调用 DefaultResultSetHandler#createResultObject 办法获取映射类型的值(如果后果类型有 TypeHandler,应用已定义的 TypeHandler 来获取)或者会应用默认的构造方法生成对象实例 (对于自定义的对象)。对于本示例中的TestAgreementInfoMapper.selectByName 办法的返回类型为 List<TestAgreementModel>, 因而每行数据都会新建一个TestAgreementModel 对象实例,此时实例的属性都还是 null。

// 创立行数据对象的外围办法如下
  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)) {
        // 如果返回一列数据,如间接返回 AgreementType,则会通过 createPrimitiveResultObject 办法间接获取到值
      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);
    }
    throw new ExecutorException("Do not know how to create an instance of" + resultType);
  }

对于新生成的 TestAgreementModel 对象实例,mybatis 会生成新的 MetaObject 实例,MetaObject中存有原对象以及对原对象类的属性和 setter,getter 办法以及构造方法的解析信息。解析信息存储在类 org.apache.ibatis.reflection.Reflector 中,拜访门路比拟深(MetaObject.BeanWrapper.MetaClass.Reflector)

通过 DefaultResultSetHandler#applyAutomaticMappings 实现主动映射,主动映射的机制应用了缓存,key 为驼峰格局的属性名都转为大写,value 为属性名。通过将列名的下划线去掉并都转为大写字母,就能够从缓存中查找对应的 java 对象属性名了。对于没有明确指定映射关系的属性,mybatis 会将其映射关系都封装在 UnMappedColumnAutoMapping 中,UnMappedColumnAutoMapping的属性包含column,property,typeHandler,primitive。对于字段的映射关系,mybatis 做了一级缓存,以防止在下次调用时再次解析,进步性能。

获取到以上的 UnMappedColumnAutoMapping 汇合后,会循环执行该汇合,并调用每个映射 UnMappedColumnAutoMappingTypeHandler#getResult获取到理论的值。如本示例中调用 BizEnumTypeHandler#getNullableResult 办法来执行咱们的自定义的逻辑,获取到转换后的值。通过 metaObject(其中蕴含有以后行 java 实例)的 setValue 办法,通过 method 的反射机制,将值赋值给原始的 java 实例。如此循环,直到UnMappedColumnAutoMapping 汇合循环结束,实现了一行数据的赋值。而后对于下一行持续进行如上操作,直到数据都赋值实现。

在赋值完一行数据(TestAgreementModel(name=zhangsan1, agreementType=QUICK_PAY))后须要将该后果暂存起来,通过调用 ResultHandler#handleResult 办法,将后果对象存储在 ResultHandler 中。而默认实现类 DefaultResultHandler 中应用 List<Object> 用来存储所有的行数据。每个 ResultSet 都有一个 DefaultResultHandler 实例,能够保障并发安全性。

通过 DefaultResultHandler 的援用能够将数据带到外层赋值给返回后果接收者 List<Object> multipleResults,通过multipleResults 将数据带到了 SimpleExecutor 中,而 SimpleExecutor 中的后果会返回给 DefaultSqlSession。SqlSession 有selectOneselectList, 能够判断给利用的 mapper 接口办法返回一个对象后果还是 List 后果或者条件不合乎而报错。

如上形容就实现了 Sql 查问的 ResultSet 后果转为 JavaBean 实例的过程。

正文完
 0