概述
-
在应用代码中,如果不结合spring来使用mybatis,则需要通过SqlSession获取mapper接口对应的代理对象MapperProxy,然后通过该代理对象来调用并执行mapper接口的方法。使用示例如下:
String resource = "org/mybatis/example/mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); // 通过SqlSessionFactory获取sqlSession SqlSession session = sqlSessionFactory.openSession(); try { // 通过SqlSession对象获取BlogMapper接口的代理对象, // 然后赋值给BlogMapper接口 BlogMapper mapper = session.getMapper(BlogMapper.class); // 调用BlogMapper接口的方法来执行指定的数据库操作 Blog blog = mapper.selectBlog(101); } finally { session.close(); }
-
在mybatis内部是通过该MapperProxy代理对象来获取并执行该mapper接口方法所对应的SQL的。
Mapper接口代理对象MapperProxy的获取
-
由上面的例子可知,在应用代码中通过SqlSession来获取mapper接口BlogMapper的代理对象,并将该代理对象赋值到类型为BlogMapper的一个引用,通过该引用来调用BlogMapper接口的对应方法。
-
SqlSession获取mapper接口的代理对象的getMapper方法实现如下:调用configuration的getMapper方法,从configuration内部获取指定mapper接口类型的MapperProxy代理对象,其中mapper接口类型使用泛型T来表示。
public <T> T getMapper(Class<T> type) { return configuration.<T>getMapper(type, this); }
-
Configuration的getMapper方法实现如下:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); } // MapperRegistry类定义 public class MapperRegistry { // 配置Configuration引用 private final Configuration config; // 应用代码中的mapper接口和代理对象MapperProxy的工厂类MapperProxyFactory的映射, private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>(); public MapperRegistry(Configuration config) { this.config = config; } @SuppressWarnings("unchecked") public <T> T getMapper(Class<T> type, SqlSession sqlSession) { // 将mapper接口的类对象作为key final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } try { // 创建MapperProxy对象实例 return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } } ... }
-
从内部维护的mapperRegistry中获取该mapper接口对应的mapperProxy代理对象,由之前的文章分析可知,每个mapper接口对应的mapperProxy代理对象是在应用程序中,使用SqlSessionFactoryBuilder创建SqlSessionFactory对象实例时,解析mapper.xml配置文件,然后保存到SqlSessionFactory对象关联的配置Configuration的mapperRegistry中的。
SQL的执行
-
由以上分析可知,应用代码通过SqlSession对象获取了mapper接口对应的代理对象MapperProxy,并在应用代码中赋值给了mapper接口的一个引用,然后通过该引用来调用mapper接口的方法,从而触发该代理对象MapperProxy来获取并执行该方法对应的SQL。MapperProxy的定义如下:
// 实现了InvocationHandler接口实现动态代理,目标类T每个方法的执行都会通过invoke来拦截 // 每个mapper对应一个MapperProxy,在spring中,针对每个mapper接口使用对应的MapperProxy对象来作为bean public class MapperProxy<T> implements InvocationHandler, Serializable { private static final long serialVersionUID = -6424540398559729838L; // sqlSession引用,由该mapper的所有方法共享,在结合mybatis-spring中,是所有mapper的所有方法共享同一个sqlSessionTemplate引用 private final SqlSession sqlSession; private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache; ... }
-
MapperProxy实现了JDK提供的InvocationHandler接口,InvocationHandler是JDK提供的用来实现动态代理的,即被代理的类的所有方法的执行都通过InvocationHandler接口定义的invoke方法来拦截。
-
MapperProxy的invoke的方法实现如下:
// args为mapper接口的代理对象的method方法被调用时,应用代码提供的参数值 @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 if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } // 根据目标类,即应用代码中的mapper接口的方法method,找到对应的mapperMethod // MapperProxy的MapperMethod是懒加载的,即使用的时候才创建MapperMethod,然后存放到该MapperProxy的methodCache中 final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); }
-
核心实现为通过cachedMapperMethod方法来创建该被调用的mapper接口的方法对应的MapperMethod,然后调用MapperMethod的execute方法来执行实际的SQL调用。
-
其中MapperMethod对象是在第一次调用该方法时创建,然后保存在MapperProxy,具体为MapperProxyFactory的一个类型为ConcurrentHashMap的methodCache缓存中,之后直接取出调用即可。
-
在mybatis内部设计当中mapper接口对应MapperProxy,mapper接口的方法对应MapperMethod。
mapper接口的方法映射MapperMethod的创建
-
MapperProxy的cachedMapperMethod的实现如下:
private MapperMethod cachedMapperMethod(Method method) { // methodCache具体在MapperProxyFactory定义,用于缓存该mapper接口的方法对应的MapperMethod对象 // 这样由该MapperProxyFactory创建的MapperProxy对象共享这一个缓存,每个MapperProxy内部维护一个引用, // 这个实现基础是每个mapper接口都对应一个MapperProxyFactory。 // 由于methodCache是ConcurrentHashMap,故是线程安全的。 MapperMethod mapperMethod = methodCache.get(method); if (mapperMethod == null) { mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()); methodCache.put(method, mapperMethod); } return mapperMethod; }
先从方法缓存methodCache中获取,如果存在该方法对应的MapperMethod则直接返回,否则创建一个MapperMethod对象。
-
MapperMethod类定义与对象实例的创建如下:在执行时,即调用execute方法时,通过SqlCommand对象来获取mapper接口的该方法对应的MappedStatement对象。
public class MapperMethod { private final SqlCommand command; private final MethodSignature method; public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) { // 在SqlCommand构造方法中,从configuration的mappedStatements集合中, // 获取key为该mapper接口的方法method,value为对应的MappedStatement,即SQL语句包装器, // 这样在下面执行execute方法时,可以直接通过command的name作为key, // 从configuration的mappedStatements集合取出对应的mappedStatement对象即可 this.command = new SqlCommand(config, mapperInterface, method); // 该mapper接口方法的签名 this.method = new MethodSignature(config, mapperInterface, method); } ... }
-
SqlCommand类定义:其中name为该mapper接口的方法的全限定名。通过SqlCommand的name属性作为key,可以从Configuration的mappedStatements字典中,取出该方法对应的MappedStatement对象。由之前的文章分析可知,Configuration的mappedStatements集合用于存放mapper.xml的增删查改对应的SQL语句。
-
SqlCommandType为SQL语句类型枚举:UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
// 获取mapper接口的某个方法对应的mapper.xml中的id为该方法名的SQL操作节点对应的mappedStatement // 使用name来关联这个mappedStatement对象在configuration的mappedStatements集合的key public static class SqlCommand { // mapper接口的方法全限定名,便于从Configuration的mappedStatements集合获取对应的SQL包装对象MappedStatement对象 private final String name; // SQL语句的类型枚举 private final SqlCommandType type; public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) { final String methodName = method.getName(); // mapper接口方法名 final Class<?> declaringClass = method.getDeclaringClass(); // 获取该方法对应的MapperStatement对象 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为configuration中的mappedStatements集合的某个元素的key name = ms.getId(); type = ms.getSqlCommandType(); if (type == SqlCommandType.UNKNOWN) { throw new BindingException("Unknown execution method for: " + name); } } } ... } // MapperMethod的resolveMappedStatement方法 private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName, Class<?> declaringClass, Configuration configuration) { // statementId为方法全限定名,即类全限定名+方法名 String statementId = mapperInterface.getName() + "." + methodName; // 创建SqlSessionFactory时,已经将mapper接口的方法对应的SQL操作, // 生成了对应的MappedStatement对象存放到了configuration的mappedStatement集合中了 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; } }
SQL的执行:调用MapperMethod的execute方法
-
SQL的执行主要是通过MapperMethod的execute来实现的。由以上分析可知,MapperMethod通过SqlCommand来获取mapper接口的该方法对应的mapper.xml的增删查改节点的SQL,具体为SQL对应的MappedStatement对象。具体实现如下:
public Object execute(SqlSession sqlSession, Object[] args) { Object result; // 使用sqlSession来执行对应的SQL语句 switch (command.getType()) { case INSERT: { // param为一个HashMap,SQL参数占位符名和mapper接口方法的参数值的映射 Object param = method.convertArgsToSqlCommandParam(args); // command.getName为SQL语句的id,param为SQL的参数和参数值对 // 交给sqlSession执行,其中sqlSession绑定一个数据库连接 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; }
通过SqlSession内部的executor来执行MappedStatement封装的SQL语句
-
以insert插入数据为例:sqlSession.insert(command.getName(), param),以下为SqlSession的update方法定义,insert底层也是调用update方法来执行的。
-
具体为通过executor包的Executor来实现:使用SQL包装器MappedStatement和SQL参数parameter获取实际的执行SQL和完成动态SQL参数绑定,最后调用JDBC相关API来完成SQL的执行。
public int update(String statement, Object parameter) { try { dirty = true; // statement为mapper接口的方法全限定名, // 从configuration的mappedStatements中获取在创建sqlSessionFactory时填充进去的该statement对应的 // SQL语句包装类MappedStatement MappedStatement ms = configuration.getMappedStatement(statement); return executor.update(ms, wrapCollection(parameter)); } catch (Exception e) { throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
-
每个SqlSession都包含一个独立的Executor,避免不同SqlSession的相互影响,通过该executor来完成SQL的执行。
-
Executor在内部解析MappedStatement获取SQL语句并完成动态SQL参数值的绑定,以下为Executor接口的其中一个实现类SimpleExecutor的update方法实现,具体实现为:通过StatementHandler对象来完成MappedStatement内部的SQL解析提取,SQL参数值绑定,生成JDBC的预处理语句,以及最后通过JDBC的API执行该SQL语句。
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); // SQL语句处理器,将SQL语句对象ms和参数键值对映射parameter,赋值为其内部参数,以便之后的SQL语句参数赋值 StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null); // 生成预处理语句,并且将参数值替换SQL的问号?参数,形成可执行的SQL // 内部调用handler.prepare方法 stmt = prepareStatement(handler, ms.getStatementLog()); // 使用SQL语句处理器执行SQL完成更新,返回更新的数据库行数 return handler.update(stmt); } finally { closeStatement(stmt); } }
-
以下为SQL语句处理器StatementHandler接口的其中一个实现类PreparedStatementHandler的update方法实现:主要是JDBC的PrepareStatement的执行。
@Override public int update(Statement statement) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; // SQL语句执行 ps.execute(); // 获取执行结果,即更新的数据库行数 int rows = ps.getUpdateCount(); Object parameterObject = boundSql.getParameterObject(); // 生成key信息,如插入数据产生行的id KeyGenerator keyGenerator = mappedStatement.getKeyGenerator(); // 将key信息返回给应用代码 keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject); // 放回更新的行数 return rows; }
总结
通过以上分析可知,在应用代码中调用Mapper接口的某个方法来执行对应的数据库操作,在mybatis内部过程为:
- 首先通过mapper接口的代理对象MapperProxy的invoke方法拦截该mapper接口方法的执行,在invoke方法中获取该mapper接口方法对应的MapperMethod对象;
- 调用MapperMethod对象的execute方法,通过当前的SqlSession对象执行mapper接口的该方法对应的SQL语句;
- SqlSession对象通过关联的Configuration引用获取该方法对应的MappedStatement对象;然后通过SqlSession绑定的SQL执行器Executor来解析MappedStatement对象获取实际的SQL,并完成该SQL的动态参数绑定;
- Executor通过SQL语句处理器StatementHandler来从MappedStatement获取SQL语句,然后生成JDBC相关的statement语句并进行参数绑定,最后执行该statement语句,从而将该SQL语句发送到数据库执行并获取执行结果。