平时在日常开发的时候,我们通常定义一个Mapper接口实现数据库操作,而不是一个包含逻辑的实现类。一般而言,一个接口是没有办法执行的,那MyBatis是怎么做到的呢?
答案就是动态代理,关于动态代理的内容,博主在之前的博文中已经详细介绍过了,此处不再赘述。
本文主要探析一下MyBatis在执行数据库操作时的运行原理。
假如我们有如下的Mapper接口,主要实现对一个用户信息的增删查改操作,
package com.maowei.learning.orm.dao;
import com.maowei.learning.orm.User;
public interface UserDao {
User findUserById(String id);
Integer updateUserById(User user);
Integer deleteUserById(String id);
Integer addUser(User user);
}
同时还有相对应的Mapper.xml文件,内容如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">
<mapper namespace="com.maowei.learning.orm.dao.UserDao">
<select id="findUserById" resultType="User" parameterType="string">
select * from user where id = #{id}
</select>
<insert id="addUser" parameterType="User" >
insert into user(id,name,age) values(#{id},#{name},#{age})
</insert>
<delete id="deleteUserById" parameterType="string">
delete from user where id = #{id}
</delete>
<update id="updateUserById" parameterType="User" >
update user set name = #{name}, age = #{age} where id = #{id}
</update>
</mapper>
为了能在初始化的时候被加载,还需要如下配置:
<mappers>
<mapper resource="spring/mapper/UserMapper.xml"/>
</mappers>
以上三步算是使用MyBatis操作数据库必须的步骤,那我们如何调用呢?
//通过配置文件信息,创建SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("spring/SqlMapConfig.xml"));
//通过SqlSessionFactory对象创建数据库会话
SqlSession session1 = sqlSessionFactory.openSession(true);
UserDao dao1 = session1.getMapper(UserDao.class);
logger.debug("开始查询用户信息");
logger.info(dao1.findUserById("A001").toString());
重点来看一下第三步中调用的SqlSession#getMaopper方法,一般默认情况下,MyBatis创建的SqlSession实例类对象为DefaultSqlSession类实例对象,我们进入DefaultSqlSession的getMapper方法,
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
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);
}
}
可以看到通过传入的Class类型的参数type获取对应的MapperProxyFactory对象,再调用newInstance方法生成具体的实例对象,进入MapperProxyFactory#newInstance方法,
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
看到这里,基本可以推断出大概的运行逻辑了。先通过sqlSession, mapperInterface, methodCache三个参数构造MapperProxy对象,再通过反射创建双数Mapper接口的代理对象。
那我们来看一下MapperProxy的内部构造,代码如下,
public class MapperProxy<T> implements InvocationHandler, Serializable {
……
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;
//构造方法略
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
……
}
MapperProxy类实现了InvocationHandler接口,因此重写了invoke方法,以此拦截了外部对原先Mapper接口的调用操作。接下来的重点就是MapperMethod#execute方法,
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;
}
经过上述处理,MyBatis把外部对Mapper接口的调用方法,都委托给了SqlSession对象来处理。
MapperMethod内部有个名为SqlCommand的内部类,代码如下,
public static class SqlCommand {
private final String name;
private final SqlCommandType type;
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
//通过Mapper接口名称和方法名称查找Statement对象
String statementName = mapperInterface.getName() + "." + method.getName();
MappedStatement ms = null;
if (configuration.hasStatement(statementName)) {
ms = configuration.getMappedStatement(statementName);
} else if (!mapperInterface.equals(method.getDeclaringClass())) { // issue #35
String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName();
if (configuration.hasStatement(parentStatementName)) {
ms = configuration.getMappedStatement(parentStatementName);
}
}
……
}
……
}
构造方法中通过Mapper接口名称和方法名称查找Statement对象,这就是为什么要求在使用MyBatis的时候需要将sql语句的namespace名称和Mapper接口的包路径保持一致,sql语句的id需要和mapper接口的方法名称保持一致。也正是因为这个原因,MyBatis的Mapper接口方法不支持重载。