刚开始使用Mybaits的同学有没有这样的疑惑,为什么我们没有编写Mapper的实现类,却能调用Mapper的方法呢?本篇文章我带大家一起来解决这个疑问
上一篇文章我们获取到了DefaultSqlSession,接着我们来看第一篇文章测试用例后面的代码
EmployeeMapper employeeMapper = sqlSession.getMapper(Employee.class); ListallEmployees = employeeMapper.getAll();
为 Mapper 接口创建代理对象
我们先从 DefaultSqlSession 的 getMapper 方法开始看起,如下:
1 publicT getMapper(Class type) { 2 return configuration. getMapper(type, this); 3 } 4 5 // Configuration 6 public T getMapper(Class type, SqlSession sqlSession) { 7 return mapperRegistry.getMapper(type, sqlSession); 8 } 9 10 // MapperRegistry 11 public T getMapper(Class type, SqlSession sqlSession) { 12 // 从 knownMappers 中获取与 type 对应的 MapperProxyFactory 13 final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory 14 if (mapperProxyFactory == null) { 15 throw new BindingException("Type " + type + " is not known to the MapperRegistry."); 16 } 17 try { 18 // 创建代理对象 19 return mapperProxyFactory.newInstance(sqlSession); 20 } catch (Exception e) { 21 throw new BindingException("Error getting mapper instance. Cause: " + e, e); 22 } 23 }) knownMappers.get(type);
这里最重要就是两行代码,第13行和第19行,我们接下来就分析这两行代码
获取MapperProxyFactory
根据名称看,可以理解为Mapper代理的创建工厂,是不是Mapper的代理对象由它创建呢?我们先来回顾一下knownMappers 集合中的元素是何时存入的。这要在我前面的文章中找答案,MyBatis 在解析配置文件的
private void bindMapperForNamespace() { // 获取映射文件的命名空间 String namespace = builderAssistant.getCurrentNamespace(); if (namespace != null) { Class> boundType = null; try { // 根据命名空间解析 mapper 类型 boundType = Resources.classForName(namespace); } catch (ClassNotFoundException e) { } if (boundType != null) { // 检测当前 mapper 类是否被绑定过 if (!configuration.hasMapper(boundType)) { configuration.addLoadedResource("namespace:" + namespace); // 绑定 mapper 类 configuration.addMapper(boundType); } } } } // Configuration publicvoid addMapper(Class type) { // 通过 MapperRegistry 绑定 mapper 类 mapperRegistry.addMapper(type); } // MapperRegistry public void addMapper(Class type) { if (type.isInterface()) { if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { /* * 将 type 和 MapperProxyFactory 进行绑定,MapperProxyFactory 可为 mapper 接口生成代理类 */ knownMappers.put(type, new MapperProxyFactory (type)); MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); // 解析注解中的信息 parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } }
在解析Mapper.xml的最后阶段,获取到Mapper.xml的namespace,然后利用反射,获取到namespace的Class,并创建一个MapperProxyFactory的实例,namespace的Class作为参数,最后将namespace的Class为key,MapperProxyFactory的实例为value存入knownMappers。
注意,我们这里是通过映射文件的命名空间的Class当做knownMappers的Key。然后我们看看getMapper方法的13行,是通过参数Employee.class也就是Mapper接口的Class来获取MapperProxyFactory,所以我们明白了为什么要求xml配置中的namespace要和和对应的Mapper接口的全限定名了
生成代理对象
我们看第19行代码 return mapperProxyFactory.newInstance(sqlSession);,很明显是调用了MapperProxyFactory的一个工厂方法,我们跟进去看看
public class MapperProxyFactory{ //存放Mapper接口Class private final Class mapperInterface; private final Map methodCache = new ConcurrentHashMap(); public MapperProxyFactory(Class mapperInterface) { this.mapperInterface = mapperInterface; } public Class getMapperInterface() { return this.mapperInterface; } public Map getMethodCache() { return this.methodCache; } protected T newInstance(MapperProxy mapperProxy) { //生成mapperInterface的代理类 return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy); } public T newInstance(SqlSession sqlSession) { /* * 创建 MapperProxy 对象,MapperProxy 实现了 InvocationHandler 接口,代理逻辑封装在此类中 * 将sqlSession传入MapperProxy对象中,第二个参数是Mapper的接口,并不是其实现类 */ MapperProxy mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache); return this.newInstance(mapperProxy); } }
上面的代码首先创建了一个 MapperProxy 对象,该对象实现了 InvocationHandler 接口。然后将对象作为参数传给重载方法,并在重载方法中调用 JDK 动态代理接口为 Mapper接口 生成代理对象。
这里要注意一点,MapperProxy这个InvocationHandler 创建的时候,传入的参数并不是Mapper接口的实现类,我们以前是怎么创建JDK动态代理的?先创建一个接口,然后再创建一个接口的实现类,最后创建一个InvocationHandler并将实现类传入其中作为目标类,创建接口的代理类,然后调用代理类方法时会回调InvocationHandler的invoke方法,最后在invoke方法中调用目标类的方法,但是我们这里调用Mapper接口代理类的方法时,需要调用其实现类的方法吗?不需要,我们需要调用对应的配置文件的SQL,所以这里并不需要传入Mapper的实现类到MapperProxy中,那Mapper接口的代理对象是如何调用对应配置文件的SQL呢?下面我们来看看。
Mapper代理类如何执行SQL?
上面一节中我们已经获取到了EmployeeMapper的代理类,并且其InvocationHandler为MapperProxy,那我们接着看Mapper接口方法的调用
ListallEmployees = employeeMapper.getAll();
知道JDK动态代理的同学都知道,调用代理类的方法,最后都会回调到InvocationHandler的Invoke方法,那我们来看看这个InvocationHandler(MapperProxy)
public class MapperProxyimplements InvocationHandler, Serializable { private final SqlSession sqlSession; private final Class mapperInterface; private final Map methodCache; public MapperProxy(SqlSession sqlSession, Class mapperInterface, Map methodCache) { this.sqlSession = sqlSession; this.mapperInterface = mapperInterface; this.methodCache = methodCache; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 如果方法是定义在 Object 类中的,则直接调用 if (Object.class.equals(method.getDeclaringClass())) { try { return method.invoke(this, args); } catch (Throwable var5) { throw ExceptionUtil.unwrapThrowable(var5); } } else { // 从缓存中获取 MapperMethod 对象,若缓存未命中,则创建 MapperMethod 对象 MapperMethod mapperMethod = this.cachedMapperMethod(method); // 调用 execute 方法执行 SQL return mapperMethod.execute(this.sqlSession, args); } } private MapperMethod cachedMapperMethod(Method method) { MapperMethod mapperMethod = (MapperMethod)this.methodCache.get(method); if (mapperMethod == null) { //创建一个MapperMethod,参数为mapperInterface和method还有Configuration mapperMethod = new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration()); this.methodCache.put(method, mapperMethod); } return mapperMethod; } }
如上,回调函数invoke逻辑会首先检测被拦截的方法是不是定义在 Object 中的,比如 equals、hashCode 方法等。对于这类方法,直接执行即可。紧接着从缓存中获取或者创建 MapperMethod 对象,然后通过该对象中的 execute 方法执行 SQL。我们先来看看如何创建MapperMethod
创建 MapperMethod 对象
public class MapperMethod { //包含SQL相关信息,比喻MappedStatement的id属性,(mapper.EmployeeMapper.getAll) private final SqlCommand command; //包含了关于执行的Mapper方法的参数类型和返回类型。 private final MethodSignature method; public MapperMethod(Class> mapperInterface, Method method, Configuration config) { // 创建 SqlCommand 对象,该对象包含一些和 SQL 相关的信息 this.command = new SqlCommand(config, mapperInterface, method); // 创建 MethodSignature 对象,从类名中可知,该对象包含了被拦截方法的一些信息 this.method = new MethodSignature(config, mapperInterface, method); } }
MapperMethod包含SqlCommand 和MethodSignature 对象,我们来看看其创建过程
① 创建 SqlCommand 对象
public static class SqlCommand { //name为MappedStatement的id,也就是namespace.methodName(mapper.EmployeeMapper.getAll) private final String name; //SQL的类型,如insert,delete,update private final SqlCommandType type; public SqlCommand(Configuration configuration, Class> mapperInterface, Method method) { //拼接Mapper接口名和方法名,(mapper.EmployeeMapper.getAll) String statementName = mapperInterface.getName() + "." + method.getName(); MappedStatement ms = null; //检测configuration是否有key为mapper.EmployeeMapper.getAll的MappedStatement if (configuration.hasStatement(statementName)) { //获取MappedStatement ms = configuration.getMappedStatement(statementName); } else if (!mapperInterface.equals(method.getDeclaringClass())) { String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName(); if (configuration.hasStatement(parentStatementName)) { ms = configuration.getMappedStatement(parentStatementName); } } // 检测当前方法是否有对应的 MappedStatement 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 和 type 变量 name = ms.getId(); type = ms.getSqlCommandType(); if (type == SqlCommandType.UNKNOWN) { throw new BindingException("Unknown execution method for: " + name); } } } } public boolean hasStatement(String statementName, boolean validateIncompleteStatements) { //检测configuration是否有key为statementName的MappedStatement return this.mappedStatements.containsKey(statementName); }
通过拼接接口名和方法名,在configuration获取对应的MappedStatement,并设置设置 name 和 type 变量,代码很简单
② 创建 MethodSignature 对象
MethodSignature 包含了被拦截方法的一些信息,如目标方法的返回类型,目标方法的参数列表信息等。下面,我们来看一下 MethodSignature 的构造方法。
public static class MethodSignature { private final boolean returnsMany; private final boolean returnsMap; private final boolean returnsVoid; private final boolean returnsCursor; private final Class> returnType; private final String mapKey; private final Integer resultHandlerIndex; private final Integer rowBoundsIndex; private final ParamNameResolver paramNameResolver; public MethodSignature(Configuration configuration, Class> mapperInterface, Method method) { // 通过反射解析方法返回类型 Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface); if (resolvedReturnType instanceof Class>) { this.returnType = (Class>) resolvedReturnType; } else if (resolvedReturnType instanceof ParameterizedType) { this.returnType = (Class>) ((ParameterizedType) resolvedReturnType).getRawType(); } else { this.returnType = method.getReturnType(); } // 检测返回值类型是否是 void、集合或数组、Cursor、Map 等 this.returnsVoid = void.class.equals(this.returnType); this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray(); this.returnsCursor = Cursor.class.equals(this.returnType); // 解析 @MapKey 注解,获取注解内容 this.mapKey = getMapKey(method); this.returnsMap = this.mapKey != null; /* * 获取 RowBounds 参数在参数列表中的位置,如果参数列表中 * 包含多个 RowBounds 参数,此方法会抛出异常 */ this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class); // 获取 ResultHandler 参数在参数列表中的位置 this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class); // 解析参数列表 this.paramNameResolver = new ParamNameResolver(configuration, method); } }
执行 execute 方法
前面已经分析了 MapperMethod 的初始化过程,现在 MapperMethod 创建好了。那么,接下来要做的事情是调用 MapperMethod 的 execute 方法,执行 SQL。传递参数sqlSession和method的运行参数args
return mapperMethod.execute(this.sqlSession, args);
我们去MapperMethod 的execute方法中看看
MapperMethod
public Object execute(SqlSession sqlSession, Object[] args) { Object result; // 根据 SQL 类型执行相应的数据库操作 switch (command.getType()) { case INSERT: { // 对用户传入的参数进行转换,下同 Object param = method.convertArgsToSqlCommandParam(args); // 执行插入操作,rowCountResult 方法用于处理返回值 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()) { // 执行查询操作,并将结果封装在 Map 中返回 result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { // 执行查询操作,并返回一个 Cursor 对象 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()); } return result; }
如上,execute 方法主要由一个 switch 语句组成,用于根据 SQL 类型执行相应的数据库操作。我们先来看看是参数的处理方法convertArgsToSqlCommandParam是如何将方法参数数组转化成Map的
public Object convertArgsToSqlCommandParam(Object[] args) { return paramNameResolver.getNamedParams(args); } public Object getNamedParams(Object[] args) { final int paramCount = names.size(); if (args == null || paramCount == 0) { return null; } else if (!hasParamAnnotation && paramCount == 1) { return args[names.firstKey()]; } else { //创建一个Map,key为method的参数名,值为method的运行时参数值 final Mapparam = new ParamMap
我们看到,将Object[] args转化成了一个Map<参数名, 参数值> ,接着我们就可以看查询过程分析了,如下
// 执行查询操作,并返回一个结果 result = sqlSession.selectOne(command.getName(), param);
我们看到是通过sqlSession来执行查询的,并且传入的参数为command.getName()和param,也就是namespace.methodName(mapper.EmployeeMapper.getAll)和方法的运行参数。
查询操作我们下一篇文章单独来讲