在使用Mybatis时一般都会写个Dao接口,然后调用方法时,总结通过dao.方法完成sql查询,使用时代码如下:
public interface UserMapper {
//根据id查找
User selectById(int id);
//一对多根据id查找
UserAndStudent selectInfo(int id);
//一对多查找
List<StudentAndCourse> selectCourse(int id);
}
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user2 = userMapper.selectById(1);
System.out.println("通过约定查找"+user2);
其输出结果,就是我们在xml配置文件中,写的sql语句的执行结果
现在问题来了:mybatis是怎么通过一个没有实现的接口完成方法调用的,并且sql语句是怎么调用出来的
动态代理
根据之前的学习,想到一个能通过接口完成方法调用的模式—代理模式中的动态代理博主之前写了一篇文章 软件设计模式—代理模式
动态代理可以将接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理,这就是mybatis通过接口完成sql查询的原理
源码分析
首先观察接口创建过程
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
通过sqlSession的getMapper方法,传入的参数是一个被调用的接口类,我们已经大概知道了动态代理的模式,那这个接口还是没有具体的实现类啊,难道这个方法是在代码中被new的么??
继续进入getMapper方法
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
return了之前我们的configuration对象中的Mapper
继续进入
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
原来Mapper是存储在MapperRegistry中,这个正是我们之前加载过的配置文件
继续进入
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);
}
}
观察代码final MapperProxyFactory
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
其knownMappers被定义了一个HashMap,这个map是以class作为键,代理工厂作为值的一个map
接着判断mapperProxyFactory是否为空
不为空,进入下一个方法
动态代理对象生成
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<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
欧吼,return了mapperProxy代理对象
没错,这个newInstance就是我们的反射里面的那个,观察点入newInstance方法,最后调用了一个Proxy.newProxyInstance(),动态代理来了,第一个参数传入类加载器,第二个参数实现接口数组,第三个参数代理实例的调用处理程序
由动态代理的知识我们可以知道,传入的代理类代理了这个接口。
也就是说,当这个接口的方法被调用的时候,都会先调用代理类中的invoke方法。
动态代理对象调用
我们现在已经有了动态代理,它调用方法时会先调用代理类中的invoke方法
那么,现在我们进入动态代理类—MapperProxy
public class MapperProxy<T> implements InvocationHandler, Serializable
他来了,InvocationHandler接口
查看重写的invoke方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
第一个参数:代理实例(不需要管,没用到)
第二个参数:当我们调用接口方法时,因为有代理类所以会调用invoke方法同时将调用的是什么方法传入进来。
第三个参数:是方法调用时的参数
上面的判断先不管,查看代码
final MapperMethod mapperMethod = cachedMapperMethod(method);
根据invoke参数,method是我们的接口方法
所以mapperMethod就是我们的UserMapper的selectById方法
下一步调用mapperMethod.execute(sqlSession, args);
进入方法
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);
}
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;
}
在之前解析xml是,解析了select标签,所以现在的switch语句进入的是case select通过判断语句,我们最后调用的方法如下
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
之前的动态代理,args是我们的传入参数,我们的传入参数的1
接着,有意思的又来了,sqlSession.selectOne(),这是不是…sqlSession自己调用的seleceOne方法…并且第一个参数…好像是namespace+id的组合的一个string把…赶紧打个参数瞅瞅
欧吼,namespace+id组合来了,又回到了sqlSession.selectOne()执行过程了
现在,不妨看看咱们的MapperMethod对象中,name属性怎么来的把
MapperMethod.name
现在再回到我们的invoke方法中的这一段代码
final MapperMethod mapperMethod = cachedMapperMethod(method);
MapperMethod对象是在这里创建的,进入
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
发现一段代码
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
在这里new的,继续进入
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
咱们的command是在这里来的,那继续进入把
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
final String methodName = method.getName();
final Class<?> declaringClass = method.getDeclaringClass();
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
configuration);
if (ms == null) {
if (method.getAnnotation(Flush.class) != null) {
name = null;
type = SqlCommandType.FLUSH;
} else {
throw new BindingException("Invalid bound statement (not found): "
+ mapperInterface.getName() + "." + methodName);
}
} else {
name = ms.getId();
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
来了来了
methodName是方法名,declaringClass是接口名
再进入
resolveMappedStatement(mapperInterface, methodName, declaringClass,configuration);
查看源码
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
String statementId = mapperInterface.getName() + "." + methodName;
if (configuration.hasStatement(statementId)) {
return configuration.getMappedStatement(statementId);
} else if (mapperInterface.equals(declaringClass)) {
return null;
}
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
MappedStatement ms = resolveMappedStatement(superInterface, methodName,
declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}
}
好家伙,一上来就是在拼字符串
String statementId = mapperInterface.getName() + "." + methodName;
然后在判断hasStatement(statementId),看名字可以明白,是判断configuration中有没有statementId这个namespace+id组合最后返回configuration.getMappedStatement(statementId);
回到SqlCommand的构造方法
咱们的ms不为空,总结是最后的
name = ms.getId();
type = ms.getSqlCommandType();
打个断点瞅瞅
现在水到渠成了
调用sqlSession.selectOne的需要对象全有了,接着就是走sqlSession.selectOne的过程了
总结
- Dao接口即Mapper接口。接口的全限定名,就是映射文件中的namespace的值; 接口方法名,就是映射文件中的Mapper的Statement的id值; 接口方法内参数,就是传递给sql的参数。 Mapper接口是没有实现类的,当调用接口方法时,接口全限定名+方法名拼接字符串作为key值,可唯一定位一个MapperStatement
- 在Mybatis中,每一个“select"、“insert”、“update”、“delete” 标签,都会被解析为一个MapperStatement对象。 举例:mapper.StudentDao.findStudentById,可以唯一找到namespace为mapper.StudentDao下面id为findStudentById的MapperStatement
- Mapper接口里的方法,是不能被重载的,因为使用了全限名+方法名的保存和寻找策略。Mapper接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Mapper接口生成代理对象proxy,代理对象会拦截接口方法,转而执行MapperStatement所代表的sql,然后将sql执行结果返回
- Mybatis动态代理,约定大于配置
- 第一个约定,接口包名+接口名和xml中namespace一样的
- 第二个约定,接口方法名和xml中sql语句id是一样的
- 第三个约定,方法输入类型和返回类型和xml中的输入返回类型也是一致的
评论区