// 1、创建一个SqlSessionFactory的 建造者 ,用于创建SqlSessionFactory // SqlSessionFactoryBuilder中有大量的重载的build方法,可以根据不同的入参,进行构建 // 极大的提高了灵活性,此处使用【创建者设计模式】 SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
这是一个创建者设计模式的经典应用:
// 使用builder构建一个sqlSessionFactory,此处我们基于一个xml配置文件
// 此过程会进行xml文件的解析,过程相对比较复杂
SqlSessionFactory sqlSessionFactory = builder.build(Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatis-config.xml"));
源码部分:这里有众多的重载build方法,我们调用的build方法,会是如下大流程
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
....
}
从上边的return看,其实核心的代码又回归到了build(parser.parse())这个构造器。
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
其实,本质上,无论你做了多少工作,你使用xml也好,不使用xml也好,最终都是需要一个Configuration实例,这里保存了所有的配置项。
当然我们可以独立去使用Configuration类构造实例,不使用xml。
例如:
Configuration configuration = new Configuration();
// 创建一个数据源
PooledDataSource pooledDataSource = new PooledDataSource();
pooledDataSource.setDriver("com.mysql.cj.jdbc.Driver");
pooledDataSource.setUrl("jdbc:mysql://127.0.0.1:3306/ydlclass?characterEncoding=utf8&serverTimezone=Asia/Shanghai");
pooledDataSource.setUsername("root");
pooledDataSource.setPassword("root");
Environment environment = new Environment("env",new JdbcTransactionFactory(),new PooledDataSource());
configuration.setEnvironment(environment);
等同于:
xml的解析过程就是将xml文件转化为Configuration对象,他在启动的时候执行,也就意味着修改配置文件就要重启。所以本环节的重点就到了。
2、配置文件的解析parser.parse()这个方法了,这就是在解析xml配置文件。
在build方法中我们看到了如下代码:
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
这一步就是构造一个解析器,根据我们的入参构建一个文档解析器。使用了sax进行xml的解析,我们在讲javaee的时候讲过。
当然,我们要把重点放在parse()方法上:
public Configuration parse() {
...省略不重要的代码
//此处就是解析的核心代码
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
咱们进入这个方法,慢慢分析,其中的内容很多,很明显看到这个方法就是在解析每一个标签。
private void parseConfiguration(XNode root) {
try {
// 处理properties标签
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
// 处理别名的标签
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
// 处理environments标签
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
// 处理mappers标签
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
其实我们看到这里就大致明白了mybaits解析xml的时机和方法了。从这里我们也能基本看出来一个配置文件内能使用的标签,以及书写标签的顺序,因为这个解析过程也是有顺序的,我们随便列出几个标签看看配置文件张什么样子。
3、mapper文件的解析过程
我们暂且忽略掉其他标签的处理,以mappers标签为例继续深入探索:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
// 循环遍历他的孩子节点
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
//
String mapperPackage = child.getStringAttribute("name");
// 直接将包名加入配置项
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
// 如果是resource属性,就通过resource获取资源并解析
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
// 如果是url属性,就通过url获取资源并解析
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
// 如果是url属性,就通过url获取资源并解析
} else if (resource == null && url == null && mapperClass != null) {
Class> mapperInterface = Resources.classForName(mapperClass);
// 注册一个mapper
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
这里就是想尽办法,获取mapper的配置信息,然后做下一步处理,其实无论是哪一种方式都是会在Configuration的mapperRegistry (mapper注册器中)中注册一个mapper。其实就是将mapper的class信息放在一个名为knownMappers的hashmap中,以便后续使用。当然他的值一个一个代理工厂,这玩意能帮我们获取一个mapper的代理对象,更详细的后续说。
private final Map(1)对于package属性, MapperProxyFactory>> knownMappers = new HashMap<>();
直接将包名注册到配置中,然后调用MapperRegistry的addMappers方法,通过扫描文件的方式将这个包地下的class添加进kownMappers中。
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}
public void addMappers(String packageName, Class> superType) {
ResolverUtil> resolverUtil = new ResolverUtil<>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set>> mapperSet = resolverUtil.getClasses();
for (Class> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
publicvoid addMapper(Class type) { mapperRegistry.addMapper(type); }
我们可以从MapperRegistry中看到,addMapper的整个过程。
publicvoid addMapper(Class type) { if (type.isInterface()) { if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { knownMappers.put(type, new MapperProxyFactory<>(type)); ...省略不重要的代码 } }
我们不妨MapperProxyFactory的代码粘贴出来看看,里边维护了一个接口和方法缓存(这个后边会讲,目前他是一个空的Map),这个代理工厂确实有方法帮我们生成代理对象(newInstance)。我们看到了代理设计模式。
public class MapperProxyFactory(2)对与class属性的处理{ private final Class mapperInterface; private final Map methodCache = new ConcurrentHashMap<>(); public MapperProxyFactory(Class mapperInterface) { this.mapperInterface = mapperInterface; } public Class getMapperInterface() { return mapperInterface; } public Map getMethodCache() { return methodCache; } @SuppressWarnings("unchecked") protected T newInstance(MapperProxy mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } public T newInstance(SqlSession sqlSession) { final MapperProxy mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } }
这个属性我们配置的是一个class,
在resource属性以及url属性中没有看到configuration.addMapper()这个方法的影子,这两个属性都是以配置文件的方式加载,自然要解析mapper配置文件了。
我们看到了XMLMapperBuilder这个类的parse()方法。很明显这个方法configurationElement是用来解析配置文件的。
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
而在这个方法中bindMapperForNamespace我们看到了注册mapper的代码:
private void bindMapperForNamespace() {
...其他代码省略
configuration.addLoadedResource("namespace:" + namespace);
configuration.addMapper(boundType);
}
}
}
(4)mapper的具体解析
创建一个解析器:
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
这个方法就是对每一个标签的解析:
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
4、通过sqlSession获取一个代理对象
通过sqlSessionFactory获取另一个session,此处使用【工厂设计模式】 SqlSession sqlSession = sqlSessionFactory.openSession();
接下来就要研究mapper的代理对象生成的过程了,此处使用【代理设计模式】
// 4、通过sqlSession获取一个代理对象,此处使用【代理设计模式】 UserMapper mapper = sqlSession.getMapper(UserMapper.class);
我们可以走到DefaultSqlSession,这是SqlSession的一个子类,从open方法中我们很看到默认创建的sqlSession是他的子类DefaultSqlSession
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
...省略不重要的代码
return new DefaultSqlSession(configuration, executor, autoCommit);
}
从实现类的getMapper中得知:
publicT getMapper(Class type) { return configuration.getMapper(type, this); }
publicT getMapper(Class type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); }
从注册器中获取mapper工厂,并创建代理对象:
publicT getMapper(Class type, SqlSession sqlSession) { final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory ) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } try { // 创建代理对象: return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } }
那我们可以聊一聊MapperProxyFactory这个类了,他持有一个接口和一个methodCache,这个接口当然是为了生成代理对象使用的类。
public class MapperProxyFactory{ private final Class mapperInterface; private final Map methodCache = new ConcurrentHashMap<>(); }
在创建对象的时候创建了一个MapperProxy:
public T newInstance(SqlSession sqlSession) {
final MapperProxy mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
MapperProxy就是我们的InvocationHandler,也是创建代理对象必须的,其中的核心方法是invoke,会在调用代理对象的方法时调用:
public class MapperProxyimplements InvocationHandler, Serializable { private static final long serialVersionUID = -4724728412955527868L; private static final int ALLOWED_MODES = MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC; private static final Constructor lookupConstructor; private static final Method privateLookupInMethod; private final SqlSession sqlSession; private final Class mapperInterface; private final Map methodCache; public MapperProxy(SqlSession sqlSession, Class mapperInterface, Map methodCache) { this.sqlSession = sqlSession; this.mapperInterface = mapperInterface; this.methodCache = methodCache; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else { return cachedInvoker(method).invoke(proxy, method, args, sqlSession); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } }
核心方法当然是invoke了,将来调用代理对象的方法,其实就是在执行此方法。
5、方法调用当我们执行代理的对象的方法时:
ListallUser = mapper.findAllUser(12);
invoke方法会被调用,这里会判断他调用的是继承自Object的方法还是实现的接口的方法,我们的重点放在:
cachedInvoker(method).invoke(proxy, method, args, sqlSession);
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
// 这里就知道了methodCache的左右了,方法存在缓存里避免频繁的创建
MapperMethodInvoker invoker = methodCache.get(method);
if (invoker != null) {
return invoker;
}
// 缓存没有就创建一个
return methodCache.computeIfAbsent(method, m -> {
// 这里是处理接口的默认方法
if (m.isDefault()) {
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
// 核心代码在这里
} else {
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
核心是(普通的方法调用者):
new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
在PlainMethodInvoker传入了一个MapperMethod(方法的包装类),根据接口,方法签名,和我们的配置生成一个方法的包装,这里有SqlCommand(用来执行sql),还有我们的方法签名。
public class MapperMethod {
private final SqlCommand command;
private final MethodSignature method;
public MapperMethod(Class> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
}
他还有个核心方法,就是执行具体的sql啦!
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATe: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
// 一下部分省略
return result;
}
回头看:
cachedInvoker(method).invoke(proxy, method, args, sqlSession);
本质调用的就是PlainMethodInvoker这个子类的invoke方法,而他确实调用mapperMethod.execute方法。到此就明白了整个流程
private static class PlainMethodInvoker implements MapperMethodInvoker {
private final MapperMethod mapperMethod;
public PlainMethodInvoker(MapperMethod mapperMethod) {
super();
this.mapperMethod = mapperMethod;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
}
6、继续了解
(1)一级缓存
我们不妨再excute方法中随机找一个select语句深入挖掘:
result = executeForMany(sqlSession, args);executeForMany(sqlSession, args);
看方法:
privateObject executeForMany(SqlSession sqlSession, Object[] args) { List result; ...省略其他代码 result = sqlSession.selectList(command.getName(), param); return result; }
这里就能看到本质上使用的是sqlSession.selectList(command.getName(), param);这距离我们熟悉的越来越近了:
@Override publicList selectList(String statement, Object parameter, RowBounds rowBounds) { try { // MappedStatement,使用执行器执行sql MappedStatement ms = configuration.getMappedStatement(statement); return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
MappedStatement其实就是我们mapper.xml的解析结果,放了很多的信息,
public final class MappedStatement {
private String resource;
private Configuration configuration;
private String id;
private Integer fetchSize;
private Integer timeout;
private StatementType statementType;
private ResultSetType resultSetType;
private SqlSource sqlSource;
private Cache cache;
private ParameterMap parameterMap;
private List resultMaps;
private boolean flushCacheRequired;
private boolean useCache;
private boolean resultOrdered;
private SqlCommandType sqlCommandType;
...省略
executor是真正执行sql的一个执行器,他是一个接口,有具体的实现比如抽象类baseExecutor,实现SimpleExecutor:
public interface Executor {
int update(MappedStatement ms, Object parameter) throws SQLException;
List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
Cursor queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
List flushStatements() throws SQLException;
void commit(boolean required) throws SQLException;
void rollback(boolean required) throws SQLException;
...省略
}
我们继续看 executor.query,在抽象类baseExecutor他是这么实现的:
public abstract class baseExecutor implements Executor
@Override publicList query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); // 请注意这里创建了key CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); }
这里有一个核心方法queryFromDatabase
@Override publicList query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); if (closed) { throw new ExecutorException("Executor was closed."); } if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); } List list; try { queryStack++; // 这里有【一级缓存】的影子,优先去缓存中取 list = resultHandler == null ? (List ) localCache.getObject(key) : null; if (list != null) { handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { queryStack--; } if (queryStack == 0) { for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } // issue #601 deferredLoads.clear(); if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } } return list; }
从这里我们能看到【以及缓存的影子了】localCache,通过doQuery查询的结果,会放在localCache中。
private(2)原生的jdbcList queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List list; localCache.putObject(key, EXECUTION_PLACEHOLDER); try { // 核心方法 list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { localCache.removeObject(key); } localCache.putObject(key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; }
我们继续看实现:
public class SimpleExecutor extends baseExecutor
我们进入上边提及的doQuery,这类看到以下原生jdbc的影子了,比如stmt;
@Override publicList doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); // 获取prepareStatement stmt = prepareStatement(handler, ms.getStatementLog()); return handler.query(stmt, resultHandler); } finally { closeStatement(stmt); } }
这里有几个方法:
获取prepareStatement,这里看到了Connection:
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
// 创建PreparedStatement
stmt = handler.prepare(connection, transaction.getTimeout());
// 填充占位符
handler.parameterize(stmt);
return stmt;
}
继续深入:
public abstract class baseStatementHandler implements StatementHandler
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
ErrorContext.instance().sql(boundSql.getSql());
Statement statement = null;
try {
// 创建PreparedStatement
statement = instantiateStatement(connection);
setStatementTimeout(statement, transactionTimeout);
setFetchSize(statement);
return statement;
} catch (SQLException e) {
closeStatement(statement);
throw e;
} catch (Exception e) {
closeStatement(statement);
throw new ExecutorException("Error preparing statement. Cause: " + e, e);
}
}
继续深入,我们看到了熟悉的connection.prepareStatement(sql):
public class PreparedStatementHandler extends baseStatementHandler
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
String sql = boundSql.getSql();
... 省略
} else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
return connection.prepareStatement(sql);
} else {
return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
}
}
这里就是对占位符进行替换
@Override
public void parameterize(Statement statement) throws SQLException {
parameterHandler.setParameters((PreparedStatement) statement);
}
@Override
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
List 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);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
当目前位置我们已经从头对mybatis的源码露了一遍,有帮助三连。



