前言
在日常开发中,我们经常会通过Mybatis的Mapper接口方式来实现操作数据库的功能,因为使用此方式最为方便快捷,我们只需要定义接口并编写对应的映射文件即可完成对数据库的操作。但此方式的底层实现原理是怎样的呢,本文将结合Mybatis源码来进行详细阐述。
一、Mapper接口的使用方式
首先介绍我们在日常开发中,如何通过Mapper接口操作数据库:
(1)定义Mapper接口:
public interface UserMapper {
void insert(User user);
}
(2)编写对应的映射文件:
<mapper namespace="spring.dao.mapper.UserMapper">
<insert id="insert" parameterType="spring.domain.User">
insert into t_user (user_id, user_name) values
<![CDATA[
(#{userId}, #{userName})
]]>
</insert>
</mapper>
(3)在业务中实际调用:
@Repository
public class UserDao {
@Autowired
private UserMapper userMapper;
public void create(User user) {
userMapper.insert(user);
}
}
从上述代码可以看出,利用Mapper接口的方式操作数据库非常便捷(当然除上述的操作,还需要进行配置数据源、加载映射文件等必要操作),接下来我们将对实现原理进行详细阐述。
二、Mapper原理详细解读
上述代码阐述了如何使用Mapper接口便捷的操作数据库,下面的章节将结合Mybatis源码详细说明Mapper的实现原理。
这张代码流程图,完美的诠释了Mapper的实现原理: Mapper代码实现流程图(图片较大,请通过下载方式查看)
除了上述的代码流程图,此时序图也较好的说明了Mapper的实现原理:
下面将结合时序图进行mysql的源码解析
(1)getMapper方法
MapperRegistry类的getMapper方法,主要的作用是根据sqlSession创建一个Mapper的代理类:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 获取Mapper接口(type)对应的MapperProxyFactory
MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
} else {
try {
// MapperProxyFactory利用sqlSession创建代理
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception var5) {
throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
}
}
}
public T newInstance(SqlSession sqlSession) {
// 利用sqlSession、mapper接口等信息创建mapperProxy
// mapperProxy实现了InvocationHandler接
// 此处指明了代理定义的增强方法就在mapperProxy的invoke方法中执行
MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
return this.newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
// 最终创建并返回一个真实的代理对象
return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}
如上述代码所示,MapperProxyFactory利用sqlSession、mapper接口信息创建mapper对应的代理实现类,之后业务在调用mapper接口中定义的操作数据库的方法时,就会直接进入代理设置的回调函数invoke方法中。
结合开篇所述的Mapper接口的日常使用方式(第一章节)进行说明:
public class UserDao {
@Autowired // 在初始化bean的时候,即创建了userMapper对应的代理实现类
private UserMapper userMapper;
public void create(User user) {
// 当通过userMapper调用方法时,就会进入此Mapper的代理实现类定义的增强中
// 即上述的mapperProxy的invoke方法
userMapper.insert(user);
}
}
(2)代理增强的回调方法(invoke)详解
下面对上述提到的代理增强方法做进一步解读:
public class MapperProxy<T> implements InvocationHandler, Serializable {
private static final long serialVersionUID = -6424540398559729838L;
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 先判断执行的方法是不是Object类的方法,比如tostring,hashcode等方法
// 如果是上述方法则直接反射执行
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
// 判断执行的方法是否为默认方法,如果是则直接执行反射执行
if (method.isDefault()) {
return this.invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
// 利用接口、方法等信息,构造mapperMethod并进行缓存
// MapperMethod代理Mapper中的方法(下面有对MapperMethod的详细介绍)
MapperMethod mapperMethod = this.cachedMapperMethod(method);
// 最终通过MapperMethod代理执行具体的数据库操作
return mapperMethod.execute(this.sqlSession, args);
}
}
此处简单来讲,MapperProxy就对应是一个Mapper接口的代理,一个MapperMethod就是Mapper定义的一个方法的封装类。
结合开篇所述的Mapper接口的日常使用例子,UserMapper的代理类就是一个MapperProxy实例,mapper的insert方法的封装类就是一个mapperMethod实例。
注:插入一个文章内容之外的知识点,大家知道,代理中的invoke方法的第一个参数有什么意义吗?
回答:invoke第二个参数是method,是通过Proxy动态实例 (即第一个参数proxy)生成后(class文件),通过反向编译得到的,也就是说只有proxy实例在InvocationHandler实现类里加载才能产生第二个参数method,所以Proxy 实例要把自己传给InvocationHandler的invoke 方法。真正在invoke方法执行过程中,一般用不到proxy参数。
(3)MapperMethod详解
MapperMethod是对Mapper接口方法的实际映射(封装类),每个Mapper接口方法在调用的时候,都会先生成对应的MapperMethod,来最终实例化执行数据库操作。
MapperMethod的主要类结构信息:
SqlCommand:内部类,封装了SQL标签的类型:insert update delete select
MethodSignature:内部类,封装了方法的参数信息、返回类型信息等
private final MapperMethod.SqlCommand command;
private final MapperMethod.MethodSignature method;
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new MapperMethod.SqlCommand(config, mapperInterface, method);
this.method = new MapperMethod.MethodSignature(config, mapperInterface, method);
}
MapperMethod的execute方法:
execute方法是对SqlSession的包装,对应insert、delete、update、select四种操作。
public Object execute(SqlSession sqlSession, Object[] args) {
Object result; // 返回结果
// INSERT操作
if (SqlCommandType.INSERT == command.getType()) {
// 处理参数
Object param = method.convertArgsToSqlCommandParam(args);
// 调用sqlSession的insert方法
result = rowCountResult(sqlSession.insert(command.getName(), param));
} else if (SqlCommandType.UPDATE == command.getType()) {
// UPDATE操作 同上
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
} else if (SqlCommandType.DELETE == command.getType()) {
// DELETE操作 同上
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
} else if (SqlCommandType.SELECT == command.getType()) {
// 如果返回void 并且参数有resultHandler
// 则调用void select(String statement, Object parameter, ResultHandler handler)方法
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
// 如果返回多行结果
// executeForMany这个方法调用 <E> List<E> selectList(String statement, Object parameter);
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
// 如果返回类型是MAP 则调用executeForMap方法
result = executeForMap(sqlSession, args);
} else {
//否则就是查询单个对象
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
} else {
// 接口方法没有和sql命令绑定
throw new BindingException("Unknown execution method for: " + command.getName());
}
// 如果返回值为空 并且方法返回值类型是基础类型 并且不是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;
}
总结
本文结合Mybatis源码,详细的阐述了业务编码中通过Mapper接口方式来操作数据库的底层实现原理。
实现关键在于每次通过调用接口方法操作数据库的时候,Mybatis都会利用MapperProxyFactory创建当前Mapper接口对应的MapperProxy代理实现类,在此代理类定义的增强中,会利用sqlSession、接口、方法等信息构造MapperMethod。MapperMethod是Mybatis对Mapper的接口方法生成的对应封装类,此封装类定义了真正的操作数据库的代码实现,最终对数据库的操作就是依赖他实现的。
综上所述,开发人员之所以可以非常便捷的使用Mapper接口方式来实现操作数据库,是因为Mybatis利用了代理机制,封装了中间的代码实现,极大的降低了开发复杂度。