在本系列第一篇文章已经讲述过在Mybatis-Spring项目中,是通过 MapperFactoryBean 的 getObject()方法来获取到Mapper的代理类并注入到Spring容器中的。在学习本章之前我们先提出以下几点问题:
本章内容就是围绕着上面三个问题进行解析,那么带着问题去看源码吧!
针对Mybatis项目,Mapper的配置加载是从XmlConfigBuilder.mapperElement()方法中触发的。我们来看下源码:
private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { if ("package".equals(child.getName())) { // 通过 package 形式加载 ,内部其实也是获取到 package 路径下的所有class再通过 class 形式加载 String mapperPackage = child.getStringAttribute("name"); configuration.addMappers(mapperPackage); } else { String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); if (resource != null && url == null && mapperClass == null) { // 加载 Mapper.xml 的 ErrorContext.instance().resource(resource); InputStream inputStream = Resources.getResourceAsStream(resource); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); mapperParser.parse(); } else if (resource == null && url != null && mapperClass == null) { // 加载 Mapper.xml 的 ErrorContext.instance().resource(url); InputStream inputStream = Resources.getUrlAsStream(url); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); mapperParser.parse(); } else if (resource == null && url == null && mapperClass != null) { // 通过 class 形式加载 Class<?> mapperInterface = Resources.classForName(mapperClass); configuration.addMapper(mapperInterface); } else { throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); } } } } }
从上面源码看,加载Mapper接口有2种形式: 一种是根据设置的 package 找到路径下面所有的class并通过configuration.addMapper() 加载。 另一种是根据指定设置的Mapper接口路径直接通过 configuration.addMapper()加载class。所以加载Mapper接口最终都是 configuration.addMapper() 来加载的。
而针对Mybatis-Spring项目则是 获取 MapperScannerConfigurer 的 basePackage 参数,并通过 ClassPathMapperScanner 扫描到 设置的 basePackage 路径下的所有class ,并得到 BeanDefinition ,后面的情况在第一篇文章已经讲过,最终是得到了 MapperFactoryBean ,并且还到了 MapperFactoryBean 内部的 checkDaoConfig() 方法加载Mapper接口的内容,那么我们再次回顾下这个方法的源码:
@Override protected void checkDaoConfig() { super.checkDaoConfig(); notNull(this.mapperInterface, "Property 'mapperInterface' is required"); Configuration configuration = getSqlSession().getConfiguration(); if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) { try { // 最终都是通过 addMapper() 方法加载的。 configuration.addMapper(this.mapperInterface); } catch (Throwable t) { logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", t); throw new IllegalArgumentException(t); } finally { ErrorContext.instance().reset(); } } }
从源码中我们发现 内部其实还是通过 configuration.addMapper() 加载的。可能有些同学会问,checkDaoConfig()什么时候被调用的,这个可以追溯到 MapperFactoryBean 的继承关系图,可以发现实现了 InitializingBean接口, 而 checkDaoConfig() 就是 通过afterPropertiesSet() 调用的。所以在MapperFactoryBean 初始化创建的时候就会调用checkDaoConfig(),即 加载Mapper接口。
根据上面的分析,我们可以发现 configuration.addMapper() 是实现加载Mapper接口的最核心的方法,那么我们就来好好分析下这个方法内部实现源码:
public <T> void addMapper(Class<T> type) { mapperRegistry.addMapper(type); }
其内部是通过 委托 mapperRegistry 来就行加载的,那继续往下看:
public <T> void addMapper(Class<T> type) { // 1、 判断是否为接口 if (type.isInterface()) { // 2、 判断是否已加载 if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { // 3、 将 Mapper的class作为 MapperProxyFactory(生成Mapper代理对象的工厂类) 的构参,并保存到 knownMappers 中。 knownMappers.put(type, new MapperProxyFactory<T>(type)); // 4、 解析 Class对象中,包含的所有mybatis框架中定义的注解,并生成Cache、ResultMap、MappedStatement。 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } }
根据源码可以把整个加载流程分4个步骤:
其中第三步是加载Mapper的核心,也就是同创建了一个生成Mapper代理对象的工厂对象,并将其放到map,等需要创建Mapper代理对象的是再通过获取map中的工厂对象即可。 关于第四步,就是最近几年比较流行的通过注解编写SQl形式的解析方法。我们知道mybatis支持xml和注解形式的Sql编写。所以 MapperAnnotationBuilder 就是解析注解形式,根解析xml一样, 最终也会生成 ResultMap、MappedStatement对象封装到 configuration 中。关于它是如何解析的,有兴趣的同学可以看啊可能源码,这里不在描述。
通过之前的文章,我们知道 MapperFactoryBean 实现了 FactoryBean,也就是说在Spring 根据BeanDefinition 加载Bean的时候会调用 MapperFactoryBean.getObject() 获取真实的Bean并注入到容器中。不用想,getObject()获取到的一定是Mapper接口的代理实现类 MapperProxy ,那么我们来一步步分析是如何创建 MapperProxy :
public T getObject() throws Exception { return getSqlSession().getMapper(this.mapperInterface); }
getObject()方法是通过 getSqlSession().getMapper() 获取到 MapperProxy 的,相信大家对这个不陌生吧。至于这里的SqlSession 其实是 SqlSessionTemplate ,这个之前也讲过,所以继续查看:
public <T> T getMapper(Class<T> type) { return getConfiguration().getMapper(type, this); }
最终还是通过 configuration().getMapper() 获取到 MapperProxy。继续查看:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); }
毫不意外的肯定是委托 mapperRegistry.getMapper() 来获取,继续查看:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) { // 1、 从 knownMappers 中获取到 MapperProxyFactory final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) throw new BindingException("Type " + type + " is not known to the MapperRegistry."); try { // 2、 通过 mapperProxyFactory.newInstance() 创建 MapperProxy return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } }
也毫不意外的是 从 knownMappers 中获取到 MapperProxyFactory ,再 通过 mapperProxyFactory.newInstance() 创建 MapperProxy。 继续查看 mapperProxyFactory.newInstance() 内部实现:
@SuppressWarnings("unchecked") protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); }
从上面源码可以清晰的看到 Proxy.newProxyInstance() 指定了被代理的类是 mapperInterface,其代理类是 mapperProxy,所以最终动态创建出 mapperInterface 的动态代理类 MapperProxy@xxxx (动态代理类名)
通过上面的解析,我们明确了 MapperProxy 代理是通过JDK动态生成,但接口方法是如何实现的呢? 这里就得看 MapperProxy 源码:
public class MapperProxy<T> implements InvocationHandler, Serializable { private static final long serialVersionUID = -6424540398559729838L; // 注意: 这个 SqlSession 实际上是 SqlSessionTemplate 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 { // 如果调用的方法是 Object 种定义的方法,直接执行 if (Object.class.equals(method.getDeclaringClass())) { try { return method.invoke(this, args); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } // 接口方法的调用都是通过 MapperMethod 来执行的 final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); } // Mapper接口的每个方法 都会生成一个 MapperMethod 对象,并通过 methodCache 来维护它们之间的关系 private MapperMethod cachedMapperMethod(Method method) { MapperMethod mapperMethod = methodCache.get(method); if (mapperMethod == null) { // 注意这里 传入了 要执行的 方法信息 mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()); methodCache.put(method, mapperMethod); } return mapperMethod; } }
从上面源码中,我们可以发现,接口方法的实现其实是通过 MapperMethod 来实现的,且 Mapper接口的每个方法 都会生成一个 MapperMethod 对象,并通过 methodCache 来维护它们之间的关系,而 methodCache 是通过 MapperProxyFactory 传递下来的。
MapperMethod实现接口方法的入口是 execute()方法,我们来看下其内部源码:
// 实际上都是调用 SqlSession的方法实现 public Object execute(SqlSession sqlSession, Object[] args) { Object result; // 判断 索要执行的方法类型 if (SqlCommandType.INSERT == command.getType()) { // 参数转换 Object param = method.convertArgsToSqlCommandParam(args); // 执行insert result = rowCountResult(sqlSession.insert(command.getName(), param)); } else if (SqlCommandType.UPDATE == command.getType()) { Object param = method.convertArgsToSqlCommandParam(args); // 执行 update result = rowCountResult(sqlSession.update(command.getName(), param)); } else if (SqlCommandType.DELETE == command.getType()) { Object param = method.convertArgsToSqlCommandParam(args); // 执行 delete result = rowCountResult(sqlSession.delete(command.getName(), param)); } else if (SqlCommandType.SELECT == command.getType()) { // 执行 select if (method.returnsVoid() && method.hasResultHandler()) { // 没有返回 executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { // 返回 List result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { // 返回 Map result = executeForMap(sqlSession, args); } else { // 返回 一个对象 Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); } } else { 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 的方法实现的,但它是如何区别什么时候调用哪个 SqlSession 的方法呢?这个就不得不说 MapperMethod 内部维护的 SqlCommand 对象,我们查看 SqlCommand 的构造方法:
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) throws BindingException { // 补全 方法的全名称路径 即 com.xxx.selectByName String statementName = mapperInterface.getName() + "." + method.getName(); MappedStatement ms = null; // 从 configuration 中获取到 MappedStatement 对象 if (configuration.hasStatement(statementName)) { ms = configuration.getMappedStatement(statementName); } else if (!mapperInterface.equals(method.getDeclaringClass().getName())) { // issue #35 String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName(); if (configuration.hasStatement(parentStatementName)) { ms = configuration.getMappedStatement(parentStatementName); } } if (ms == null) { throw new BindingException("Invalid bound statement (not found): " + statementName); } // 从 MappedStatement 中获取到方法名 (注意: 节点中的id属性包含命名空间) name = ms.getId(); // 从 MappedStatement 中获取到 方法的节点标签,即 select|insert|update|delete type = ms.getSqlCommandType(); if (type == SqlCommandType.UNKNOWN) { throw new BindingException("Unknown execution method for: " + name); } }
我们通过分析知道 ,SqlCommand 其实是通过从 MappedStatement 中获取到 方法名,以及所要执行的SQl命令类型(select|insert|update|delete)。 这里我们可以明确的发现从configuration 中 获取 MappedStatement 是通过 全称路径的方法去获取的,即 com.xxx.selectByName 这种,调用SqlSession的方法虽然是从 MappedStatement 中获取 id (注意: 节点中的id属性包含命名空间),但实质上都是 com.xxx.selectByName。但 我们可以通过这里看出mybatis的Mapper接口方法是不可以重载的。
本文由博客一文多发平台 OpenWrite 发布!