共计 10607 个字符,预计需要花费 27 分钟才能阅读完成。
Mybatis 源码解析(三) —— Mapper 代理类的生成
在本系列第一篇文章已经讲述过在 Mybatis-Spring 项目中,是通过 MapperFactoryBean 的 getObject()方法来获取到 Mapper 的代理类并注入到 Spring 容器中的。在学习本章之前我们先提出以下几点问题:
- 1、Mapper 接口是如何被加载 到 Configuration 中的?
- 2、Mapper 代理类是如何生成的?
- 3、Mapper 代理类是如何实现接口方法的?
本章内容就是围绕着上面三个问题进行解析,那么带着问题去看源码吧!
一、加载 Mapper 接口
针对 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 个步骤:
- 1、判断是否为接口
- 2、判断是否已加载
- 3、将 Mapper 的 class 作为 MapperProxyFactory(生成 Mapper 代理对象的工厂类) 的构参,并保存到 knownMappers 中。
- 4、解析 Class 对象中,包含的所有 mybatis 框架中定义的注解,并生成 Cache、ResultMap、MappedStatement。
其中第三步是加载 Mapper 的核心,也就是同创建了一个生成 Mapper 代理对象的工厂对象,并将其放到 map,等需要创建 Mapper 代理对象的是再通过获取 map 中的工厂对象即可。关于第四步,就是最近几年比较流行的通过注解编写 SQl 形式的解析方法。我们知道 mybatis 支持 xml 和注解形式的 Sql 编写。所以 MapperAnnotationBuilder 就是解析注解形式,根解析 xml 一样,最终也会生成 ResultMap、MappedStatement 对象封装到 configuration 中。关于它是如何解析的,有兴趣的同学可以看啊可能源码,这里不在描述。
二、Mapper 动态代理对象(MapperProxy)的创建
通过之前的文章,我们知道 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 接口方法的实现
通过上面的解析,我们明确了 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
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 接口方法是不可以重载的。
四、个人总结
- 1、Configuration 维护了一个 MapperRegistry 对象,该对象主要作用就是加载 Mapper 接口和获取 MapperProxy。
- 2、MapperRegistry 维护了一个 key 为 mapper 接口 class 对象,value 为 MapperProxyFactory 的 map
- 3、MapperProxy 是 通过 MapperProxyFactory 创建的。
- 4、MapperProxy 实现 Mapper 接口方法是委托 MapperMethod 执行的。
- 5、MapperMethod 执行接口方法时是通过 SqlCommand 来判断要执行的具体 SQL 节点,并且最终委托 SqlSession执行。
- 6、SqlCommand 内部的 信息是通过从 MappedStatement 中获取的。
如果您对这些感兴趣,欢迎 star、follow、收藏、转发给予支持!
本文由博客一文多发平台 OpenWrite 发布!