前言
在Mybatis中,SqlSession是一个重要的接口类,在SqlSession
对象中定义了一系列的增删改查方法,对于数据库的操作,最后都会调用SqlSession中的方法,例如selectList
、selectOne
、update
等等。
但是我们在项目中使用Mybatis时,并没有去操作过SqlSession对象,而是通过创建了一个个的Mapper接口类,以及对应的xml文件,就完成了对数据库的操作。
在底层,这其实是框架通过jdk动态代理
,为我们提供了每个Mapper接口的代理类
,来实现了增强功能。
下面就来解析一下整个流程的原理,例如Mapper接口类以及xml文件的初始化、动态代理的调用等。方便自己的学习,也希望能够帮助到大家,谢谢~
初始化-解析配置文件
<package>标签配置方式
在项目中,一般不会只有一个Mapper接口,一般是在一个包里面,存放着一系列的各种模块的Mapper接口以及xml文件,例如 mapper包 或者 dao包。
如果使用xml的方式进行全局配置的话,一般引入Mapper接口会使用<package>
标签,如下
xml
<configuration>
<mappers>
<package name="com.xxx.mapper"/>
</mappers>
</configuration>
使用这种方式,就可以扫描到所配置的包下,所有的Mapper接口类。下面来看一下他的原理。
Mybatis中对于配置文件的解析,是通过SqlSessionFactoryBuilder
类的build方法来完成的,过程中解析了配置文件,创建了SqlSessionFactory工厂对象
。解析过程中,经历了XMLConfigBuilder
解析核心配置文件、XMLMapperBuilder
解析映射配置文件等等。
在XMLConfigBuilder
解析核心配置文件的过程中,自然会对核心配置文件中配置的<mappers>
标签进行解析,主要是通过mapperElement()
方法来完成的,下面看一下源码:
因为这里主要是讲
<package>
配置方式的解析过程,所以<mapper>
标签的解析过程就省略了,感兴趣的朋友可以参考这篇文章(Mybatis源码 - 初始化流程 加载解析配置文件)。
java
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 {
//解析其它配置方式,例如<mapper resource=""> <mapper class="">等标签
}
}
}
}
第5行:获取到了标签的name
属性的值,其实就是com.xxx.mapper
。
第6行:调用了全局配置对象Configuration
的addMappers()
方法,传入了上面获取到的包路径,完成了解析。
java
//Mybatis全局配置类
public class Configuration {
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
}
可以看到,Configuration类中,是调用了一个成员变量mapperRegistry
的addMappers()
方法,将包路径再次进行了传入。我们先来看看,mapperRegistry是什么。
MapperRegistry类结构
从下面的源码中可以看出,在MapperRegistry类中,维护着一个Map集合knownMappers
,该集合的key为Class<?>
类对象,value为MapperProxyFactory<?>
,这里其实是Mapper接口的代理工厂对象,正是通过这个对象,来生产了对应Mapper接口的代理类。
该类中也提供了一系列针对knownMappers
的操作方法,例如addMapper
、getMapper
、hasMapper
等等,包括了对knownMappers的增删改查、以及判断操作等。
java
public class MapperRegistry {
private final Configuration config;
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
public MapperRegistry(Configuration config) {
this.config = config;
}
//省略方法的具体实现
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {}
public <T> boolean hasMapper(Class<T> type) {}
public <T> void addMapper(Class<T> type) {}
public Collection<Class<?>> getMappers() {}
public void addMappers(String packageName, Class<?> superType) {}
public void addMappers(String packageName) {}
}
总结:MapperRegistry的主要作用,就是维护了一个Map集合,Map集合中存放着所有Mapper接口的类对象以及对应的代理工厂对象。
MapperRegistry.addMappers()
addMappers(String packageName)
java
public class MapperRegistry {
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}
}
通过上面的源码可以看出,调用了MapperRegistry.addMappers()之后,又调用了一个重载方法addMappers(String packageName, Class<?> superType)
。该方法的源码如下:
addMappers(String packageName, Class<?> superType)
java
public class MapperRegistry {
private final Configuration config;
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
public void addMappers(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
}
-
第6-7行,创建了一个解析工具类
ResolverUtil
,调用了它的find方法,传入了packageName
参数。这一步其实是解析了传入的包路径下的所有资源,并且将.class
文件,添加到了一个set集合中。可以看一下下面的源码:javapublic ResolverUtil<T> find(Test test, String packageName) { //getPackagePath方法逻辑:将.替换为了/ 并返回 //return packageName == null ? null : packageName.replace('.', '/'); String path = getPackagePath(packageName); try { List<String> children = VFS.getInstance().list(path); //遍历查找出来的资源,最后只返回以.class结尾的文件。 for (String child : children) { if (child.endsWith(".class")) { addIfMatching(test, child); } } } catch (IOException ioe) { log.error("Could not read package: " + packageName, ioe); } return this; }
-
第8-11行,通过
resolverUtil
获取到了刚封装好的set集合,进行遍历。将每一个Mapper接口的类对象都调用了另一个重载方法addMapper(Class<T> type)
。下面看一下该方法的源码。
addMapper(Class<T> type)
java
public <T> void addMapper(Class<T> 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));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
- 可以看出,这个方法实际上就是进行了一系列的判断,然后将Mapper接口的类对象以及对应的代理工厂对象,封装到了Map集合
knownMappers
中。 - 第9-10行,创建了
MapperAnnotationBuilder
对象,并且调用了它的parse()
方法,这里实际上是对Mapper接口中方法上面标注的注解进行了解析,例如@Select
等。- 在这个对象的
parse()
方法中,也对包路径进行了替换,从.
替换成了/
,同时拼接了.xml
,使用这种方式定位到了xml文件,然后通过XMLMapperBuilder
对xml文件进行了解析,保存到了全局配置对象Configuration
中。 - 这也解释了为什么
xml文件必须和Mapper接口处在同包位置
下。
- 在这个对象的
总结:整个
MapperRegistry.addMappers()
执行完毕之后,就算是完成了初始化过程,将配置的包路径下的所有Mapper接口及其代理工厂对象,封装到了Map集合knownMappers
中。
代理对象创建
文章一开始提到,框架通过jdk动态代理
,为Mapper接口生成了代理对象,实现了增强逻辑。代理对象其实是通过SqlSession
对象的getMapper(Class<T> type)
方法生成的。本小节来看一下这一块逻辑。
SqlSession.getMapper()
先来看一下SqlSession接口的getMapper方法源码,这里直接查看默认实现类 DefaultSqlSession 的源码
java
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
}
这里调用了全局配置对象Configuration
的getMapper(Class<T> type, SqlSession sqlSession)
方法
java
public class Configuration {
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
}
可以看到,这里又用到了MapperRegistry
对象,通过前面的分析,我们知道 该对象中封装了每个Mapper接口以及它的代理工厂对象。
那显然这里的逻辑就是通过传入的type
(Mapper接口的类对象),来获取到它对应的代理工厂对象,然后通过代理工厂对象,创建出对应Mapper接口的代理对象
。
MapperRegistry.getMapper()
java
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) 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);
}
}
- 第2-5行:从Map集合
knownMappers
中,根据type
,获取到了Mapper接口对应的代理工厂对象。并对其进行了判空,如果不存在该代理工厂对象,抛出异常。 - 第7行:调用了代理工厂对象的
newInstance
方法,创建了代理对象。
MapperProxyFactory.newInstance()
java
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
- 可以看出,newInstance方法中,首先创建了一个
MapperProxy
对象,并将相关对象都进行了传入,然后调用了重载的newInstance(MapperProxy<T> mapperProxy)
方法。 - 在重载的
newInstance
方法中,使用了Proxy.newProxyInstance
创建了代理对象。 - MapperProxy类中实现了InvocationHandler接口,所以最后直接将它传入了newProxyInstance方法,来创建代理对象。
代理对象执行
在继续讲解代理对象是如何去执行数据库操作之前,我觉得有必要先来简单介绍一下 jdk动态代理原理,了解这块的大佬可以直接跳过。
jdk动态代理原理
JDK动态代理,是在字节码的层面,不改变原类代码的前提下,去增强这个类的方法,添加一些增强功能。它的实现,必须依赖于接口。也就是说,需要被增强的类,必须继承了某个接口。
例如在Mybatis中,Mapper接口只是一个普通接口,本身并不能去执行和它关联的xml文件中的sql语句,这部分逻辑,其实是写在代理对象中的。
Proxy.newProxyInstance()
Java中使用Proxy类的newProxyInstance方法来创建指定类的代理对象,具体参数有三个: newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
这里对入参做一个说明:
- ClassLoader loader:类加载器,一般传入哪个类加载器都可以,比如当前类的类加载器。
- Class<?>[] interfaces:想要增强的类所实现的接口的类对象数组。比如
Target
类实现了T
接口,那么这里传入的数组里面,有一个T.class
。 - InvocationHandler h:这是一个函数式接口,提供一个
invoke()
方法,具体的增强逻辑就写在这里边。
当想去增强Target
类的run()
方法时,通过Proxy类的newProxyInstance方法来创建出代理对象,当使用代理对象调用run()
方法时,就会调用到InvocationHandler
里边的invoke()
方法中,这样就可以执行到增强逻辑。
用一张图来梳理一下:
下面带来一个jdk动态代理小案例,方便大家理解。
jdk动态代理案例
现有一个接口T
和一个实现类Target
,接口中提供了一个无参抽象方法run
,实现类中实现了这个接口,并执行了逻辑,这里就做一个简单打印。如下
java
//接口
public interface T {
void run();
}
//实现类
public class Target implements T {
@Override
public void run() {
System.out.println("Target.run()方法被执行了...");
}
}
如果相对Target
类中的run
方法进行增强,需要三步:
- 使用
Proxy.newProxyInstance()
创建代理对象。 - 在传入的
InvocationHandler
的invoke()
方法中,编写增强逻辑。 - 获取到代理类,通过代理类调用目标方法。
java
public static void main(String[] param) {
//目标对象
Target target = new Target();
//创建代理对象
T proxyInstance = (T) Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[]{T.class},
(proxy, method, args) -> {
System.out.println("前置增强打印...");
//通过method.invoke()调用目标方法,需要传入目标对象以及调用参数
Object result = method.invoke(target, args);
System.out.println("后置增强打印...");
//返回调用结果
return result;
}
);
//通过代理对象调用目标方法
proxyInstance.run();
}
//打印结果
前置增强打印...
Target.run()方法被执行了...
后置增强打印...
执行数据库操作
从上面的解析中,我们可以明白两件事:
- 在初始化流程中,创建了Mapper接口的代理对象。在创建过程中,传入的
InvocationHandler
,是一个封装好的对象MapperProxy
,它实现了InvocationHandler
接口。 - jdk动态代理中,通过代理对象调用目标方法,实际是调用的
InvocationHandler
中的invoke()
方法。
依据上面两点,我们可以得出:
当通过Mapper接口的代理对象调用方法时,会执行到
MapperProxy
对象的invoke()
方法中。
下面看一下源码:
java
@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);
}
}
- 第4-6行:对Object自带的方法进行过滤,例如
equals、hahscode、toString
等,不需要增强。 - 第7行:通过
cachedInvoker()
方法,创建了一个MapperMethodInvoker
接口对象,这里创建的是实现类PlainMethodInvoker
的对象,接下来调用了该对象的invoke()
方法,将相关参数进行了传入。
看一下源码
java
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);
}
}
- 第4-7行,在构造器中可以看出,是将上一步中的
method
对象封装后进行了传入,并赋值给了mapperMethod
。- 源码:new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()
- 第10-12行:这里是调用了
mapperMethod
这个成员变量的execute
方法,而mapperMethod
这个成员变量的值,是在上一步创建MapperMethodInvoker
接口对象时进行了赋值。
下面看一下MapperMethod
的源码
java
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);
}
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);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
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;
}
}
这个方法就没什么好说的了。通过判断sql的commandType
,决定是去执行增删改查哪种操作,然后调用对应方法,传入SqlSession和参数。
sql的commandType,也是在MapperMethod
的构造器中,进行了SqlCommand
对象的构建,感兴趣的朋友可以自己翻一下,这里就不贴源码了。
再跟进去会发现,实际还是调用的SqlSession
中的方法,后面的逻辑,感兴趣的朋友可以参考这篇文章(Mybatis源码 - SqlSession.selectOne(selectList)执行流程解析 一二级缓存 参数设置 结果集封装),这里就不再赘述。
到这里,整个解析流程就结束了。感谢你的阅读,如果有不对的地方,欢迎在评论区指正!谢谢~