回顾
上一篇对MyBatis的数据源进行了分析,研究了MyBatis自身支持的两种数据源UnpooledDataSource与PooledDataSource
下面来看一下MyBatis对于事务是如何进行的
Transaction
在开发中,控制数据库的事务是一项非常重要的工作,而MyBatis则是使用了Transaction接口对数据库事务进行了抽象
可以看到Transaction事务接口支持5种方法
- commit:提交事务
- rollback:回滚事务
- close:关闭数据库连接
- getConnection:获取对应的数据库连接对象
- getTime:获取事务超时时间
而对于事务的管理,跟数据源一样采用的同样是抽象工厂模式来进行管理,先来看看事务的实现类型
可以看到,MyBatis自身有两种事务接口的实现类型
- ManagedTransaction
- JdbcTransaction
JdbcTransaction
JdbcTransaction有4个重要属性
- connection:数据库连接
- DataSource:数据源
- TransactionIsolatinLevel:事务隔离等i
- autoCommit:是否自动提交
对于其构造方法,一般会采用第一个构造方法,而第一个构造方法会初始化除了Connection之外的三种属性
获取连接
获取连接对应的是getConnection方法
public Connection getConnection() throws SQLException {
//如果为null,调用openConnection进行打开连接
if (connection == null) {
openConnection();
}
return connection;
}
可以看到,获取连接的主要操作委派到了openConnection方法中
源码如下
protected void openConnection() throws SQLException {
if (log.isDebugEnabled()) {
log.debug("Opening JDBC Connection");
}
//从数据源中获取
connection = dataSource.getConnection();
if (level != null) {
//并设置隔离等级
connection.setTransactionIsolation(level.getLevel());
}
setDesiredAutoCommit(autoCommit);
}
可以看到,连接是从数据源DataSource进行获取的
提交、回滚、关闭
从源码上可以看到,对于提交、回顾、关闭,全部都是基于connection自身去做的,这里就不详细说明了
ManagedTransaction
可以看到,ManagedTransaction有4个成员属性
- dataSource:数据源
- level:事务隔离机制
- connection:数据库连接
- closeConnection:cloConnection配置
从构造方法上看,其同样也是除了connection属性外,其他都会进行初始化,因此我们也可以猜到其获取连接也是根据数据源DataSource来进行创建的
可以看到,其逻辑是一样的
并且ManagedTransaction是不支持事务提交、回滚的!!!!!! 其仅仅支持事务的关闭连接!!!!!!
仅仅支持事务连接关闭,并且事务的连接关闭会受closeConnect影响
这里也不再赘述了
可以看到两个事务之前的区别
- JdbcTransaction支持事务提交、回滚,并且里面的操作都是Connection原生去实现的
- ManagedTransaction不支持事务提交、回滚,不过支持修改closeConnect属性,也就是支持修改关闭Connection方法的逻辑
TransactionFactory
而对于工厂,对应的抽象工厂接口是TransactionFactory
并且对于TransactionFactory接口有2种抽象方法
- setProperties:设置一些工厂的配置(不需要一定去实现,默认实现为空)
- newTransaction:创建新的事务
而对应MyBatis支持的两种事务类型,MyBatis对于事务工厂同样也是有对应两个接口工厂实现类
- JdbcTransactionFactory:创建JdbcTransaction
- ManagedTransactionFactory:创建ManagerTransaction
JdbcTransactionFactory
JdbcTranSactionFactory用于创建JdbcTransaction
其源码也很简单, 直接new创建JdbcTransaction完事,但这里JdbcTransactinFactory并不支持对配置进行改动
ManagedTransactionFactory
ManagedTransactionFactory是创建ManagedTransaction的
其源码也并不复杂,对于创建事务也是直接new来创建的,但跟JdbcTransactionFactory不同的是,ManagedTransactionFactory重写实现了setProperties方法,但仅仅支持closeConnection这个配置
binding模块
绑定模块是MyBatis中的核心模块之一,其功能是让Mapper接口跟XML配置文件进行绑定!!!
绑定的好处是开发人员不需要执行对数据库操作的时候才发现错误,而是在初始化的时候,就能提供信息报错!!!!!!(假如没有绑定模块,使用接口执行数据库操作的时候才去找配置文件,那么也只有等待到执行数据库操作的时候才会报错)
还记得我们怎么使用MyBatis的吗?
- 定义一个接口
- 接口绑定resource对应目录下的配置文件,或者使用注解
- 使用session.getMapper来获取对应接口的代理对象,用代理对象去拦截方法(这也是为什么MyBatis不用实例化接口也能使用接口的原因,关键就在于代理对象的产生)
我们先认识binding模块的核心接口的关系
- MapperRegistry:Mapper接口以及MapperRegistryFactory的注册中心
- MapperMethod:
- MapperProxy:Mapper接口的代理对象
MapperRegistry
MapperRegistry是注册中心,负责Mapper接口(用Class对象来表示)以及MapperRegistryFactory的注册的
MapperRegistry的两个成员属性
- Configuration:配置类,存储配置信息,后面再详细分析Configuration里面的内容,反正里面就是一大堆的配置信息
- knownMappers:记录了Mapper接口与其对应MapperProxyFactory的关系,说白了就是Mapper接口与创建其代理对象的工厂的映射关系,这也是为什么说MapperRegistry是注册Mapper与其MapperProxyFactory的注册中心
注册Mapper与对应的MapperProxyFactory
对应的方法为addMapper,其方法的源码如下
public <T> void addMapper(Class<T> type) {
//首先判断是不是接口
//只能代理Mapper接口
if (type.isInterface()) {
//判断该Mapper接口是不是已经绑定了
//这里只是使用knownMappers.containsKey方法来判断而已
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
//loadCompleted代表下面是否加载完全
boolean loadCompleted = false;
try {
//往底层的knownMappers中去添加Mapper接口与对应的MapperProxyFactory
//相当于注册了Mapper接口
knownMappers.put(type, new MapperProxyFactory<>(type));
//使用MapperAnnotationBuilderParser对Mapper接口进行解析
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
//进行解析
parser.parse();
//解析成功,才变为true
loadCompleted = true;
} finally {
//可以看到这里并没有捕捉异常
//当解析失败了之后
if (!loadCompleted) {
//解析失败,把注册了的Mapper接口移除掉
knownMappers.remove(type);
}
}
}
}
从代码上可以看到,大概的步骤如下所示
- 先判断是不是Mapper接口,就是简单判断是不是接口类型,必须是接口类型才会继续进行下面的操作
- 判断当前的Mapper接口是不是已经注册绑定了
- 如果没有注册绑定,进行注册绑定
- 使用Mapper接口去创建MapperProxyFactory
- 添加进底层的knownMappers容器中
- 使用MapperAnnotationBuilder来进行解析
- 如果MapperAnnotationBuilder解析失败,将会移除之前的注册!!!!!!
这里关键的部分有几个,MapperProxyFactory是什么?MapperAnnotationBuilder用来干什么的?
这里先跳过,因为MapperAnnotationBuilder是涉及到XML解析和注解处理的
获取Mapper接口的代理对象
在需要执行某SQL时,会先调用MapperRegistry.getMapper的方法获取了Mapper接口的代理对象,下面就来看看方法的逻辑
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//从MapperRegistry注册中心的底层knownMapper容器里面去获取指定接口类型的MapperProxyFactory
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
//判断是否为空
if (mapperProxyFactory == null) {
//如果没有对应的MapperProxyFactory,证明没有绑定,抛错
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
//拥有对应的MapperProxyFactory类型
try {
//使用对应的MapperProxyFactory去创建代理对象
//并且是使用上进来的SqlSession
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
- 首先从底层得到knownMapper容器里面去获取指定接口类型的MapperProxyFactory
- 判断MapperProxyFactory是否为空
- 如果为空,证明没有绑定,抛错处理
- 如果不为空,使用MapperProxyFactory去创建代理对象!
从这里就可以看到,MapperProxyFactory是负责去创建Mapper的代理对象的,下面就来看看这个MapperProxyFactory怎么回事
MapperProxyFactory
成员属性有两个
- mapperInterface:该MapperProxyFactory为哪个接口类型创建代理对象
- methodCache:方法缓存
同时也可以看到,唯一的构造方法仅仅只是初始化了指定Mapper接口而已
对于创建代理对象,从MapperRegistry也可以看到其调用的newInstance方法
可以看到,该方法是使用sqlSession、mapper接口,以及方法缓存去创建了一个MapperProxy对象,然后调用了重载方法(在红色框框的上面重载方法),使用JDK的动态代理去创建了一个被MapperProxy代理的Mapper接口对象并返回!!!
所以对于接下来我们使用Mapper进行的方法,实际上都会被MapperProxy给拦截!!
MapperProxy
MapperProxy其实就是Mapper接口的JDK动态代理的对象
可以看到其实现了InvocationHandler,就证明了其是一个代理对象,那么对于代理对象,关键点就在invoke方法上!不过在此之前还是说明一下一些关键成员属性的作用
- sqlSession:关联了的SqlSession,既连接会话
- mapperInterface:Mapper接口对应的Class对象
- mehodCache:用于缓存MapperMehodInvoker,其实本质上是缓存MethodHandler或者MapperMethod的,其中key是Mapper接口方法对应的Method对象,而Value则是对应的MethodHandler或者MapperMethod,两者会完成参数转换以及SQL语句的执行功能!但这里要注意的一点是两者并不会记录任何状态相关的信息,所以是可以在多个代理对象之间共享的!!!说白了其是一个方法缓存,用来缓存整个代理方法的逻辑,暴露给其他代理对象之间进行共享使用
那么MethodHandler和MapperMethod有什么区别呢?
其实在JDK1.8之前只有MapperMethod的,对应的就是接口中的方法;但JDK1.8之后,接口支持了默认方法,所以也就多出了MethodHandler,并且为了保持原来的架构,也就是只有一个方法缓存,所以必须要进行多一层封装,把两个类型都抽象成了MapperMethodInvoker!!!!!!
下面就可以看一下invoke方法的逻辑了,源码如下
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//判断执行的是不是Object的方法
if (Object.class.equals(method.getDeclaringClass())) {
//如果是Object的方法就不进行拦截了
return method.invoke(this, args);
} else {
//如果不是Object的方法,使用cachedInvoker获取缓存中的方法缓存然后进行invoke
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
- 对Object的方法不进行拦截
- 对非Object的方法进行拦截
下面就来看看如何从缓存中取得方法缓存,源码如下,从中可以看到,缓存可以避免多次去重复创建对应的MethodInvoker(代理的细节其实位于MethodInvoker里面!!!),因为代理对象的细节是要变的(不同方法要执行的代理细节不一样,比如SQL不同),因此MyBatis又封装了一层MethodInvoker出来来统一抽象的代理细节,并且采用缓存技术来减少该代理细节的不断创建
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
//调用computeIfAbsent方法
//该方法用来判断methodCache中是否有method对应的methodMapperInvoker
//如果没有就会执行lambda的方法,此时m代表key,也就是现在传进去的method
//lambda的方法是一个Funcation函数式接口,用来创建了一个新methodMapperInvoker并返回
return MapUtil.computeIfAbsent(methodCache, method, m -> {
//判断方法是不是默认方法,也就是有没有默认的实现
if (m.isDefault()) {
//如果有默认的实现
try {
//
if (privateLookupInMethod == null) {
//返回DefaultMethodInvoker(JDK8)
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
//返回DefaultMethodInvoker(JDK9)
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
} else {
//如果不是默认方法,那就是一般的抽象方法
//返回PlainMethodInvoker
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
可以看到,cachedInvoker方法其实就是从底层的方法缓存中取,如果没有就会去新建,并且根据接口方法的类型去进行创建,规则如下
- 如果接口方法有默认实现,说白了就是有default修饰符,创建DefaultMethodInvoker,并且将其放入缓存中(DefaultMethodInvoker对应的就是MethodHandler)
- 如果接口方法没有默认实现,只是一个普通的抽象方法,创建PlainMethodInvoker,并且将其放入缓存中(PlainMethodInvoker对应的就是MapperMethod)
下面就来看看DefaultMethodInvoker与PlainMethodInvoker
PlainMethodInvoker
可以看到MapperMethodInvoker是MapperProxy里面的一个内部接口,而PlainMethodInvoker是MapperProxy的一个内部类并且实现了MapperMethodInvoker接口,关键其内部组装了MapperMethod,并且重写的invoke方法调用了MapperMethod的execute方法
对于PlainMethodInvoker,其本质上就是组装了MapperMethod来,所以关键在于这个MapperMethod是什么!
MapperMethod
MapperMethod中封装了Mapper接口中对应方法的信息(一对一),以及对应SQL语句的信息,说白了其实真正的代理细节位于MapperMethod这里(对于接口的抽象方法),包括去解析注解上的SQL语句
成员属性有两个
- SqlCommand:记录了SQL语句的名称和类型
- MethodSignature:记录了Mapper接口中对应方法的信息
下面对着两个成员属性进行分析一下
SqlCommand
SqlCommand是MapperMethod里面的一个静态内部类,有两个成员属性
- name:记录SQL语句的名称,形式为接口名称 + . + 方法名称
- Type:记录SQL语句的执行类型
下面就看看其构造方法,源码如下
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
//获取方法名字
final String methodName = method.getName();
//获取被代理的方法所属的接口Class,其实不就是mapperInterface吗?
final Class<?> declaringClass = method.getDeclaringClass();
//使用MappedStatement去获取SQL的信息
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
configuration);
//如果获取不到,也就是没有绑定的SQL信息
if (ms == null) {
//判断是不是@Flush注解
if (method.getAnnotation(Flush.class) != null) {
name = null;
//将类型标记为Flush
type = SqlCommandType.FLUSH;
} else {
//如果不是@Flush注解,抛错,表示没有绑定方法
throw new BindingException("Invalid bound statement (not found): "
+ mapperInterface.getName() + "." + methodName);
}
} else {
//如果获取到了绑定的SQL信息
//获取SQL信息来进行自身初始化
//比如SQL的id、SQL的类型
name = ms.getId();
type = ms.getSqlCommandType();
//如果是Unknown类型,会进行抛错
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
说白了整个构造方法其实就做了一件事情,从配置信息中去获取MappedStatement,然后从里面去获取SQL参数并自身进行初始化,如果在这里获取不到MappedStatement,则会处理@Flush注解,该注解是用来刷新Statement的,特别是批量处理的时候(后面会分析),用于提前提交前面的SQL然后更新数据库信息的!!!!!
从这里可以看到,对于@Flush注解,MyBatis是不会记录在配置文件上的,而是在第一次执行该方法的时候,通过缓存的方法解析出来!
这里还有一个关键点就是MappedStatement的获取,对应的方法为resolveMappedStatement,源码如下
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
//生成StatementId,格式为Mapped接口.方法名
String statementId = mapperInterface.getName() + "." + methodName;
//从配置文件中去寻找有没有对应的MappedStatement
if (configuration.hasStatement(statementId)) {
return configuration.getMappedStatement(statementId);
}
//如果Mapper接口与方法所在的接口相同,那就代表没有当前Mapper接口没有了,返回null
//交由上层去处理,就很奇怪为什么上层有了Mapper接口还要去获取方法所在的Class对象
//原来是考虑到接口的继承
else if (mapperInterface.equals(declaringClass)) {
return null;
}
//如果此时配置上没有,那就可能出现在父类接口上了
//获取当前Mapper接口的所有父类接口
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
//遍历从父类上去找!
if (declaringClass.isAssignableFrom(superInterface)) {
//递归从父类上去寻找
MappedStatement ms = resolveMappedStatement(superInterface, methodName,
declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}
}
可以看到,MyBatis是支持Mapper接口进行继承的,并且对于方法对应的SQL信息如果在当前接口没有找到,就会尝试去父类去找,直到父类找完或者遇到了方法的所属接口。。。。。。
说白了,SqlCommond其实就是配置文件上的SQL信息,根据statementId去寻找对应的SQL(statementID格式是Mapper接口名.方法名)
MethodSignature
下面看另外一个属性,MethodSignature,其记录了绑定的方法的一系列信息,比如参数列表、返回值等
它也是MapperMethod里面的一个静态内部类,不过再认识它之前,先认识一下ParamNameResolver,顾名思义这个属性是用来处理Mapper接口中定义的方法的参数列表的,比如参数名称与参数位置索引之间的关系,还有@Param注解的解析,还有实参与参数名称的映射关系,其中一个ParamNameResolver就去处理一个方法的参数列表
在ParamNameResolver中有三个成员属性,如下
- hasParamAnnotation:是否有@Param注解
- names:用来存储参数的位置索引与参数名字的映射关系
- useActualParamName:是否使用真实的参数名字
比较重要的属性就是names了,是存储参数的位置索引与参数名字的映射关系,参数名称是通过@Param注解来指定的,如果没有指定的@Param注解,那么参数的名称也是参数索引,可以看到,其是一个SortMap,也就是可以进行排序的SortMap
而且,这里要注意,对于RowBounds类型或者ResultHandler类型的参数是不会被记录到name集合中的,所以对于如果参数列表中出现了这两个参数,可能会导致乱序的现象,这里提一下,RowBounds是用来分页的,而ResultHandler是用来处理结果集的
下面就看看ParamNameResolver是如何进行初始化的,初始化的操作都在构造方法上,源码如下
public ParamNameResolver(Configuration config, Method method) {
//从配置上获取是否要使用真实参数名字,默认为True
this.useActualParamName = config.isUseActualParamName();
//获取方法的参数列表
final Class<?>[] paramTypes = method.getParameterTypes();
//获取方法上的参数列表上的注解
//这里是一个二维数组,因为一个参数是可以使用多个注解的
//比如paramAnnotations[0][0]代表第一个参数的第一个注解
//paramAnnotations[0][1]代表第一个参数的第二个注解
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
//使用TreeMap来进行存储参数索引与参数名字的映射关系
final SortedMap<Integer, String> map = new TreeMap<>();
int paramCount = paramAnnotations.length;
// 遍历参数
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
//判断当前参数是不是特定的类型,也就是前面所说的RowBounds和ResultHandler类型
if (isSpecialParameter(paramTypes[paramIndex])) {
// 跳过了RowBounds与ResultHandler类型
continue;
}
String name = null;
//接下来就遍历当前参数里面的注解了
for (Annotation annotation : paramAnnotations[paramIndex]) {
//找到@Param注解
if (annotation instanceof Param) {
//将hasParamAnnotation标识为true
hasParamAnnotation = true;
//获取@Param注解里面的value属性!!!
name = ((Param) annotation).value();
break;
}
}
//判断name此时是否为null
if (name == null) {
// 如果为null证明没有@Param注解,或者@Param没有使用上value属性
//这时候都要MyBatis去生成另外的名字
//判断是不是使用真实的参数名字
if (useActualParamName) {
//调用getActualParamName去寻找参数名字
//这个是JDK1.8才能做到的
//原理是找到在Class文件中定义的参数名字
//但这里的参数名字一般都不会是我们写的,而是arg0、arg1的形式
//只有给编译器开启了-parameters参数才会开启
name = getActualParamName(method, paramIndex);
}
//如果找不到,一般来说就是关闭了useActualParamName选项
if (name == null) {
// 可以看到,名称直接使用容器的size属性,相当于是当前的索引值了
//乱序的问题就是从这里发生,假如一个参数列表 [a,b,c]
//b是RowBound类型的,因此会产生两个键值对
//(0,0)与(2,1)【因为b跳过了,导致size不变,但索引值没跳】
//不过一般我们采用getActualParamName就不会有这种问题
name = String.valueOf(map.size());
}
}
//最后就是已索引为Key,name为Value添加进names容器中
map.put(paramIndex, name);
}
//最后初始化names属性
names = Collections.unmodifiableSortedMap(map);
}
步骤大概如下
-
获取方法上的参数列表
-
获取方法上的参数列表的注解列表
-
创建TreeMap来进行储存
-
遍历参数列表,判断是不是RowBound和ResultHandler类型,如果是则跳过不做处理,如果不是则遍历该参数的注解,找到@Param类型,如果有@Param注解则会找到其Value属性当作当前参数的name,如果此时还是没有找到name,判断有没有开启useActualParamName(使用真实参数名字【Class文件上的】),如果开启了,就使用JDK1.8自带的查找参数名称的功能,一般为arg0、arg1这种形式,如果没开启,就会使用TreeMap的Size来作为名字了(也是在这一步会产生乱序的问题)
-
将参数与其名字形成键值对添加进TreeMap中
-
-
初始化names属性
下面来看一下ParamNameResolver是如何获取name的,对应的方法为getNamedParams,该方法接收的参数是用户传入的实参列表,并且将实参与其对应名称进行关联,源码如下
public Object getNamedParams(Object[] args) {
//获取names集合的size
final int paramCount = names.size();
//判空
if (args == null || paramCount == 0) {
return null;
}
//判断是不是有开启@Param注解
else if (!hasParamAnnotation && paramCount == 1) {
//如果没有开启@Param注解,并且容器里面只有一个参数
//直接取第一个
Object value = args[names.firstKey()];
return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
} else {
//否则的话,即代表有多个参数或者开启了@Param注解
//使用ParamMap来存储
final Map<String, Object> param = new ParamMap<>();
int i = 0;
//遍历整个names集合,将其化成一个Set集合
for (Map.Entry<Integer, String> entry : names.entrySet()) {
//添加进params中,此时使用名字作为key,实参列表对应的实参作为value
param.put(entry.getValue(), args[entry.getKey()]);
// 最后要判断是否要为参数创建默认的参数名称
//默认的参数名称的格式为:"param+(第几个进来的参数,默认从1开始)"
final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
// 如果names集合中没有该默认的参数名称才添加
//这是为了避免将某个参数的@Param的value给覆盖了!!!!!!
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
可以看到,这里是生成实参与形参名字的映射关系,在我们使用MySQL中给Mapper传入实参,实参还需要映射到SQL中,这一层的映射关系就是在这里完成的
步骤如下
- 判断之前进来解析的参数列表是否只有一个,如果只有一个,直接返回传进来的实参
- 如果不止一个或者使用了@Param注解,则是需要进行对应映射处理的
- 创建一个ParamMap来进行存储
- 遍历整个names集合,此时names集合里面的键值对是,key为索引,而value为参数名字,此时添加进ParamMap中,但此时得到key为参数的名字,而value为实参列表中对应参数索引的实参!!!此时就生成好了参数名称与实参的映射关系,然后还需要去添加一个默认的参数名称与实参的映射关系,默认的参数名称是param+序号(序号代表了第几个被添加进ParamMap中的,从1开始),但这个默认的参数名称不是一定要添加的,还需要判断有没有跟@Param指定的参数名称发生冲突,假如发生了冲突就不会去添加!!!!!!
现在讲完了ParamNameResolver了,回到我们的MapperSignature,回到构造方法上,源码如下
public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
//解析方法的返回值类型,前面已经在泛型分析上讲过了TypeParameterResolver了
//因为返回值可能是一个泛型
Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
//如果返回值类型是一个Class类型
if (resolvedReturnType instanceof Class<?>) {
this.returnType = (Class<?>) resolvedReturnType;
} else if (resolvedReturnType instanceof ParameterizedType) {
this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
} else {
this.returnType = method.getReturnType();
}
//初始化returnsVoid、returnsMany、returnsCursor和returnsOptional
//这几个属性都是判断类型而已
this.returnsVoid = void.class.equals(this.returnType);
this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
this.returnsCursor = Cursor.class.equals(this.returnType);
this.returnsOptional = Optional.class.equals(this.returnType);
//mapKey是针对返回值属性为map并且使用了@MapKey注解的
//getMapKey仅仅只是解析了@MapKey注解里面的value属性而已
//@MapKey的作用是可以将List转化为Map,并且指定元素中的属性来作为key,
//而List中的元素成为对应的value
//一般采用唯一键来作为Key
this.mapKey = getMapKey(method);
//判断是否返回map是根据有没有@MapKey注解的!!!
this.returnsMap = this.mapKey != null;
//初始化rowsBoundsIndex与resultHandlerIndex
this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
//初始化paramNameResolver
this.paramNameResolver = new ParamNameResolver(configuration, method);
}
可以看到,构造方法都是初始化
- 初始化返回值(使用TypeParameterResolver处理返回值,因为返回值是有泛型的)
- 初始化returnsVoid、returnsMany、returnsCursor与returnsOptional属性,这4个属性都只是判断返回值类型而已,对应的是Void、Collection、Cursor、Optional类型而已
- 判断是不是使用Map来作为返回值,依据是是否使用了@MapKey注解,并且如果使用了@MapKey注解会进行解析,把mapKey给初始化
- 初始化rowBoundsIndex与resultHandlerIndex字段
- 初始化paramNameResolver对象
下面说明一下rowBoundsIndex与resultHandlerIndex的初始化吧
回忆一下我们的paramNameResolver是对RowBounds与ResultHandler类型是跳开的,不会做这两种参数类型的映射关系,因此在上层就有必要去记录这两种类型的参数的位置
而且我们可以看到获取这两种类型的参数的位置使用的都是getUniqueParamIndex方法,源码如下
private Integer getUniqueParamIndex(Method method, Class<?> paramType) {
Integer index = null;
//获取该方法的所有参数类型列表
final Class<?>[] argTypes = method.getParameterTypes();
//遍历去参数列表类型去进行查找
for (int i = 0; i < argTypes.length; i++) {
//如果找到了
if (paramType.isAssignableFrom(argTypes[i])) {
//判断之前是否出现过,没有出现就设为当前位置
//出现过就抛错处理,因为不支持多个
if (index == null) {
index = i;
} else {
throw new BindingException(method.getName() + " cannot have multiple " + paramType.getSimpleName() + " parameters");
}
}
}
return index;
}
从方法上可以看到,内容仅仅只是遍历参数类型列表,但只允许出现一个,如果出现多个位置会报错,说白了就是出现了多个RowBounds类型、或者ResultHandler类型的参数
看完真个初始化过程后,下面就看最核心的execute方法吧,因为execute方法才是整个代理的核心,代理对象MapperProxy仅仅只是执行了execute方法,不过我们也可以大致猜到execute方法是干什么的,因为现在已经解析完了SQL,解析完了实参,剩下的就是调用SqlSession去注入实参执行相应的SQL语句了
源码如下
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
//根据不同的SQL语句类型去调用SqlSession相应方法
switch (command.getType()) {
//insert方法
case INSERT: {
//实参映射,虽然执行的是convertArgsToSqlCommandParam
//但其本质仅仅只是执行上面的getNamedParams方法
Object param = method.convertArgsToSqlCommandParam(args);
//sqlSession执行insert方法
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
//update方法
case UPDATE: {
//实参映射
Object param = method.convertArgsToSqlCommandParam(args);
//执行SqlSession的update方法
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
//delete方法
case DELETE: {
//实参映射
Object param = method.convertArgsToSqlCommandParam(args);
//执行sqlSession的delete方法
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
//select方法
case SELECT:
//处理返回值为void并且ResultSet要使用ResultHandler处理的方法
//这种类型比较奇葩,明明是一个select,却返回值为void
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
//因为返回值为void,所以result为null
result = null;
}
//处理返回值为Collection的select
else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
}
//处理返回值为Map的的select
else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
}
//处理返回值为cursor的select
else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
}
//下面就是对于返回单个对象select的处理了
else {
//实参映射
Object param = method.convertArgsToSqlCommandParam(args);
//调用sqlSession的selectOne方法
result = sqlSession.selectOne(command.getName(), param);
//判断需要的返回值是不是Optional对象
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
//使用结果集创建Optional对象
result = Optional.ofNullable(result);
}
}
break;
//flush方法
case FLUSH:
//直接执行sqlSession的flushStatements方法
result = sqlSession.flushStatements();
break;
//其他类型方法不支持,抛错
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
//如果结果集为null,并且返回值类型不是void类型会报错
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
可以看到,execute方法其实就是枚举了所有的执行情况进行对应的调用SqlSession的方法
- Insert类型:实参映射,然后调用sqlSession的insert方法,然后交由rowCountResult处理MySQL执行返回结果(处理的行数、处理结果)
- Update类型:实参映射,然后调用SqlSession的update方法,然后交由rowCountResult处理MySQL执行返回结果(处理的行数、处理结果)
- Delete类型:实参映射,然后调用SqlSession的delete方法,然后交由rowCountResult处理MySQL执行返回结果(处理的行数、处理结果)
- Select类型:最复杂,需要根据返回值类型来去调用,Collectin类型、Void类型、Map类型、单个对象类型都是不同的调用方法
这里有个关键点,可以看到,对于返回的结果result,是使用Object类型来接收的
SqlSession方面的先不讲,等后面到了更上层的时候再进行分析SqlSession,下面来看一下对于insert、update、delete方法是如何处理MySQL返回结果的(MySQL对于DML语句的增删改返回的都是受到影响的行数),对应的方法为rowCountResult,源码如下
private Object rowCountResult(int rowCount) {
final Object result;
//如果返回值是void类型
if (method.returnsVoid()) {
//返回null
result = null;
}
//如果返回类型是Integer
else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) {
//不做处理,直接返回
result = rowCount;
}
//如果返回值是Long类型
else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) {
//强转从null
result = (long) rowCount;
}
//如果是布尔类型
else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) {
//返回处理结果是否大于0
result = rowCount > 0;
} else {
//其他类型直接抛错
throw new BindingException("Mapper method '" + command.getName() + "' has an unsupported return type: " + method.getReturnType());
}
return result;
}
其实处理返回结果仅仅只是根据其返回值类型来做处理而已
- 如果是void型,返回null
- 如果是int型,直接返回处理的行数
- 如果是long型,强转成Long后返回
- 如果是boolean型,返回处理的行数是否大于0
- 不支持其他类型处理,直接抛错
下面来看对于查询方法是如何做的
首先来看第一种情况,返回值为void型并且需要使用ResultHandler来进行处理,对应的方法为executeWithResultHandler(ResultHandler其实就是映射,说白了就是resultMap标签),源码如下
private void executeWithResultHandler(SqlSession sqlSession, Object[] args) {
//从配置文件上获取SQL语句对应的MappedStatement对象
MappedStatement ms = sqlSession.getConfiguration().getMappedStatement(command.getName());
if (!StatementType.CALLABLE.equals(ms.getStatementType())
&& void.class.equals(ms.getResultMaps().get(0).getType())) {
throw new BindingException("method " + command.getName()
+ " needs either a @ResultMap annotation, a @ResultType annotation,"
+ " or a resultType attribute in XML so a ResultHandler can be used as a parameter.");
}
//实参映射
Object param = method.convertArgsToSqlCommandParam(args);
//判断参数列表有没有RowsBounds类型的参数
if (method.hasRowBounds()) {
//如果有rowBounds,位置对应之前构造函数找到的rowBoundIndex!
//从args实参中找到rowBounds
RowBounds rowBounds = method.extractRowBounds(args);
//执行select方法
sqlSession.select(command.getName(), param, rowBounds, method.extractResultHandler(args));
} else {
//执行select方法
sqlSession.select(command.getName(), param, method.extractResultHandler(args));
}
}
逻辑很简单,找到SQL信息、实参映射、判断需不需要进行分页、执行SqlSession的select方法
第二种情况,针对Collection集合类型的,对应的方法是executeForMany
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
//实参映射
Object param = method.convertArgsToSqlCommandParam(args);
//分页
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
//调用selectList方法
result = sqlSession.selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.selectList(command.getName(), param);
}
// 如果需要的返回值类型并不是返回Result的子类
//说白了不是List接口
if (!method.getReturnType().isAssignableFrom(result.getClass())) {
//判断是不是数据类型,转换为数组类型
if (method.getReturnType().isArray()) {
//这里是采用遍历填充的方式来创建数组的
//不过关键在于如何找对数组对应类型
return convertToArray(result);
} else {
//不是数组类型,转换为其指定的类型,一般来说是一个Set集合
//这里是使用到我们学习的MetaObject,将Result添加进MetaObject中
return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
}
}
return result;
}
转换成集合也很简单,实参映射、分页、调用selectList方法,最后做结果集的处理,因为selectList方法返回的是一个List集合,对与其他类型的Collection要进行处理,比如Set或者数组
转换成数组的的代码如下
private <E> Object convertToArray(List<E> list) {
//获取方法的返回值类型里面的元素类型
//说白了就是获取数组中元素的类型
Class<?> arrayComponentType = method.getReturnType().getComponentType();
//使用该类型创建数组
Object array = Array.newInstance(arrayComponentType, list.size());
//判断是不是基本数组类型的数组
if (arrayComponentType.isPrimitive()) {
//如果是基本数据类型,需要进行遍历填充,不能直接转换!
for (int i = 0; i < list.size(); i++) {
Array.set(array, i, list.get(i));
}
//如果是基本数据类型直接返回
return array;
} else {
//如果不是
//使用list的toArray方法来转换
return list.toArray((E[]) array);
}
}
转换成其他Collection集合的代码如下
private <E> Object convertToDeclaredCollection(Configuration config, List<E> list) {
//从使用MetaObject来进行转换
Object collection = config.getObjectFactory().create(method.getReturnType());
MetaObject metaObject = config.newMetaObject(collection);
metaObject.addAll(list);
return collection;
}
第三种情况,对于Map集合
private <K, V> Map<K, V> executeForMap(SqlSession sqlSession, Object[] args) {
Map<K, V> result;
//实参映射
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
//分页
RowBounds rowBounds = method.extractRowBounds(args);
//调用selectMap
result = sqlSession.selectMap(command.getName(), param, method.getMapKey(), rowBounds);
} else {
result = sqlSession.selectMap(command.getName(), param, method.getMapKey());
}
return result;
}
实参映射、分页、调用selectMap方法
第四种情况,对于Cursor游标
private <T> Cursor<T> executeForCursor(SqlSession sqlSession, Object[] args) {
Cursor<T> result;
//实参映射
Object param = method.convertArgsToSqlCommandParam(args);
//分页
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
//调用selectCursor方法
result = sqlSession.selectCursor(command.getName(), param, rowBounds);
} else {
result = sqlSession.selectCursor(command.getName(), param);
}
return result;
}
可以看到,返回Currsor类型跟返回Map类型基本一致,实参映射之后、分页,最后交由SqlSession的selectCursor方法去完成处理
DefaultMethodInvoker
DefaultMethodInvoker也一样,是MapperProxy的一个内部类,并且实现了MapperMethodInvoker接口,关键其内部组装了MethodHandler,重写了invoke方法,调用了methodHandler的bind方法再执行invokeWithArguments方法
所以关键在于MethodHandler,MethodHandler是方法句柄,是JDK1.7提供的,用于支持动态方法的调用!所以并不是MyBatis自己封装的