1 引言和主要类
上一节讲解了sqlSession读写数据库的整个流程和四大组件的执行过程,相信大家对mybatis操作数据库有了一定的了解。上一节还提到过,其实我们还可以通过mapper方式读写数据库,并且mybatis建议使用mapper方式,而不是直接通过sqlSession的selectList update等方法。使用mapper方式的例子如下
// 读取XML配置文件
String resource = "main/resources/SqlMapConfig.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 创建sqlSessionFactory单例,初始化mybatis容器
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 创建sqlSession实例,用它来进行数据库操作,mybatis运行时的门面
SqlSession session = sessionFactory.openSession();
// 获取mapper接口动态代理对象
UserMapper mapper = session.getMapper(UserMapper.class);
// 利用动态代理调用mapper的相关方法
User user = mapper.findUserById(1);
mapper使用起来十分方便,它的优点如下
- 只用定义接口,无需实现类。也不需要继承任何类,十分灵活
- 通过接口的方法进行调用,可以有效避免String拼写错误
- 返回值不用局限于sqlSession有限的那几个方法,实现了完全的自定义。
下面我们来分析mapper方式是如何读写数据库的。
2 流程
mapper操作数据库分为两个步骤,先getMapper()获取动态代理对象,然后利用mapper对象操作数据库。我们一步步分析。
2.1 getMapper()获取动态代理对象流程
先从DefaultSqlSession的getMapper()分析。
// mapper动态代理方式执行SQL命令的入口
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
这儿是总入口,没啥好说的,再看Configuration类的getMapper()
// mapper方式执行SQL命令,先获取mapper对象
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
这也没啥好说的,看MapperRegistry类
// 动态代理方式创建mapper对象
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 先从mybatis初始化,也就是创建SqlSessionFactory时,创建的MapperProxyFactory的map中取出当前mapper接口对应的MapperProxyFactory
// MapperProxyFactory为mapper动态代理的工厂类
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 利用工厂类创建mapper动态代理对象
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
先从mybatis初始化时创建的MapperProxyFactory的Map中,取出当前mapper接口类的MapperProxyFactory。利用这个工厂类构造MapperProxy。下面详细看这个过程
// 创建mapper动态代理对象
public T newInstance(SqlSession sqlSession) {
// 构造MapperProxy对象,然后创建我们定义的mapper接口对应的对象
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
// 动态代理的方式,反射生成mapper接口对象
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
先创建MapperProxy类,然后创建Java动态代理对象,将mapperProxy作为它的InvocationHandler。执行动态代理方法时,会回调InvocationHandler的invoke()方法。对Java动态代理不清楚的同学,建议复习一下这个内容。
Java的动态代理类Proxy的newProxyInstance()生成mapper接口的动态代理对象后,getMapper()方法就执行完毕了。接下来就是动态代理执行方法调用的过程了。下面详细分析。
2.2 mapper 接口方法调用流程
mapper接口方法调用,是通过动态代理的方式执行的。Proxy动态代理执行方法调用时,调用到invocationHandler的invoke方法。我们传入的invocationHandler是创建的MapperProxy对象,故方法调用的入口为MapperProxy的invoke()方法。下面详细分析。
// 调用mapper接口中方法时的入口,Proxy会回调InvocationHandler的invoke方法
// @Param proxy: Java反射的动态代理对象,getMapper()中通过Java反射 Proxy.newProxyInstance()生成的动态代理
// @Param method: 要调用的接口方法
// @Param args: 方法入参
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// mapper接口类中的抽象方法不会执行这儿的if和else
if (Object.class.equals(method.getDeclaringClass())) {
// mapper是一个接口,而非实现类,不会走到这儿
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
// mapper中方法为抽象方法,也不会走到这儿
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 生成mapperMethod,先从cache中取,没有则创建一个MapperMethod。
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 执行execute,通过mapperMethod的method方法名,从XML中找到匹配的SQL语句,最终利用sqlSession执行数据库操作
return mapperMethod.execute(sqlSession, args);
}
先创建MapperMethod对象,然后利用它的execute来执行。我们分两小节分别分析。
2.2.1 MapperMethod的创建
先看MapperMethod的创建过程cachedMapperMethod方法。
// 生成mapperMethod,先从cache中取,没有则创建一个MapperMethod
private MapperMethod cachedMapperMethod(Method method) {
// 先从cache中取
MapperMethod mapperMethod = methodCache.get(method);
// 未命中,则创建一个MapperMethod
if (mapperMethod == null) {
// 创建MapperMethod用来执行mapper接口的方法。
// mapperInterface: 用户定义的mapper接口类
// method:要执行的mapper接口中的方法
// Configuration: XML配置信息
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
MapperMethod对象是mapper方法调用的核心对象,它先尝试从cache中取,没有则创建。下面看MapperMethod的构造方法。
// MapperMethod的构造方法,创建SqlCommand和MethodSignature两个主要成员变量。二者也是动态代理的关键
// @Param mapperInterface: 用户定义的mapper接口类
// @Param method:要执行的mapper接口中的方法
// @Param Configuration: XML配置信息
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
// 创建SqlCommand,包含SQL语句的id和type两个关键字段,type为insert update delete等类型。
this.command = new SqlCommand(config, mapperInterface, method);
// 创建MethodSignature,包含方法的返回值returnType等关键字段
this.method = new MethodSignature(config, mapperInterface, method);
}
MapperMethod包含两个主要成员变量,SqlCommand和MethodSignature。SqlCommand是mapper.xml中SQL语句的描述,包含了SQL语句的id和type。MethodSignature是mapper接口中方法的描述,包含了方法返回值等关键信息。有了它们,mapper.xml配置文件和mapper接口方法基本就描述清楚了。后面会使用到这两个变量来执行方法调用。下面我们分别看这两个关键对象是如何创建的。
先看SqlCommand的构造过程。
public static class SqlCommand {
private final String name; // sql语句的id,mapper接口类名.方法名,拼接而成
private final SqlCommandType type; // sql语句的标签,insert select update delete等
// SqlCommand构造方法,描述了mapper.xml配置信息中一条SQL语句
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
// 先获取mapper接口方法的方法名,和方法声明所在的类,也就是mapper接口类
final String methodName = method.getName();
final Class<?> declaringClass = method.getDeclaringClass();
// 拼接mapperInterface和methodName,构成id,从mybatis初始化阶段生成的mappedStatements这个map中获取当前方法对应的mappedStatement
// mybatis初始化一节讲过mappedStatement,它是通过解析mapper.xml生成的,描述了其中的一条CRUD语句。
// mappedStatement是mapper.xml中SQL操作语句的核心,包含sqlSource和BoundSql等重要对象
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
configuration);
if (ms == null) {
// 找不到此sql语句时
if (method.getAnnotation(Flush.class) != null) {
name = null;
type = SqlCommandType.FLUSH;
} else {
throw new BindingException("Invalid bound statement (not found): "
+ mapperInterface.getName() + "." + methodName);
}
} else {
// 找到此SQL语句对应的MappedStatement时。
// name为SQL语句的id,mapper接口类名.方法名,拼接而成。也就是上面分析的statementId。
name = ms.getId();
// sqlCommandType为mapper.xml中SQL语句的标签,如select insert delete update等
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
// 从mybatis初始化时创建的mappedStatements中找到本SQL语句对应的mappedStatement
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
// 以mapper接口类名.方法名,作为id,这样可以唯一对应一条SQL操作。
// mapper.xml中namespace对应接口类名,SQL语句id对应mapper接口中方法名。
String statementId = mapperInterface.getName() + "." + methodName;
// 本SQL语句在mybatis初始化阶段已经添加了时
if (configuration.hasStatement(statementId)) {
// 直接从初始化mybatis阶段,生成的mappedStatements总map表中,获取当前SQL语句的mappedStatement
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;
}
}
MethodSignature构造方法我们就不详细分析了,有兴趣的同学可以自行分析。
2.2.2 MapperMethod的执行
下面来看MapperMethod的方法执行,也就是execute方法。
// 执行mapper接口中的方法
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
// 按照SQL语句的不同type,如insert update等,调用sqlSession的insert update等对应方法,执行数据库操作
// 下面重点分析Select
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:
// 根据select返回值的不同,调用对应的不同方法来查询,其实本质上大同小异,最后都是调用了sqlSession的select方法
if (method.returnsVoid() && method.hasResultHandler()) {
// 无需返回值时
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
// 返回值为Collection集合类型或者数组类型时,下面重点分析
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
// 返回值为Map类型时
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
// 返回值为Cursor类型时
result = executeForCursor(sqlSession, args);
} else {
// 返回值为其他类型时,认为不是集合类型,故调用selectOne返回一个对象。
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;
}
execute按照SQL语句的不同type,如insert update等,调用sqlSession的insert update等对应方法。由此可见,最终mapper方式还是通过sqlSession来完成数据库insert select等操作的。我们以select这个type为例分析。不同的select返回值类型,如集合Collection,Map等对应不同的执行方法,但其实本质都是调用selectList方法。我们以最常见的返回Collection为例分析。
返回值为Collection集合类型或者数组类型时,调用executeForMany()来执行方法调用。如下。
// select返回值为集合类型时,如List
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
// 调用paramNameResolver,处理入参
Object param = method.convertArgsToSqlCommandParam(args);
// 调用sqlSession的selectList()方法,查询数据库。是否定义了逻辑分页时,会有些细微差别
// selectList方法前面一节已经讲解过了,这儿不详细讲了。
// 从这儿可见,mapper方式最终还是通过sqlSession操作数据库。
// mapper方式比直接操作方式更灵活,且不易出错。mybatis建议大家使用mapper方式。
if (method.hasRowBounds()) {
// 定义了逻辑分页rowBounds时
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
} else {
// 未定义rowBounds时
result = sqlSession.<E>selectList(command.getName(), param);
}
// 由于selectList返回的是List类型的集合,而mapper接口中的方法返回值可能为Set Queue等其他集合类型,故此处需要做转换
if (!method.getReturnType().isAssignableFrom(result.getClass())) {
if (method.getReturnType().isArray()) {
return convertToArray(result);
} else {
return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
}
}
return result;
}
可以很清晰的看到,最终我们还是通过sqlSession的selectList方法完成查询操作的。selectList()方法的流程上一节已经讲解过了,这儿就不说了。这样,mapper动态代理就完成了它的方法调用了。
3 总结
sqlSession的mapper动态代理方法调用,比直接通过sqlSession的selectList,update等方法更优。mapper方式是sqlSession selectList等直接调用方式的一层封装。正因为有了这层封装,使得mybatis在增加灵活性之余,还能提高健壮性。这也充分体现了mybatis的设计精巧,值得我们学习。
相关文章
mybatis源码分析2 - SqlSessionFactory的创建
mybatis源码分析4 - sqlSession读写数据库完全解析
mybatis源码分析5 - mapper读写数据库完全解析