MyBatis运行
生成Mapper实例
前面分析MyBatis解析mapper.xml以及在执行过程中根据参数动态生成预编译sql的流程,以及相关实现细节。可以说配置文件层面的流程和细节有了一个大致的了解,我们知道在使用MyBatis的第一步不是编写mapper.xml,而是定义Mapper接口,以Mapper接口的全路径作为mapper.xml的namespace,从而才能通过Mapper的接口方法找到要执行的具体sql。
MyBatis的ORM映射方式没有具体的实现类,都是通过JDK动态代理方式+mapper.xml实现,可以说是非常方便了,也很好的实现了Java代码和sql语句的一个解耦分离。
接下来我们分析一下是如何实现Mapper接口动态代理的,并且又是如何执行的。
首先明确一点,MyBatis的Mapper动态代理使用的是JDK代理。 然后JDK代理的先决条件就是必须要声明接口。
先来看一个MyBatis的基本使用:
private static SqlSessionFactory sqlSessionFactory;
//使用mybatis第一步,获取sqlSessionFactory对象
static {
try {
String resource = "mybatis-test/mybatis-config-test.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
sqlSessionFactory.getConfiguration();
} catch (IOException e) {
e.printStackTrace();
}
}
public static SqlSession getSqlSession(){
return sqlSessionFactory.openSession();
}
public static void main(String[] args) {
// 获取Mapper实例
SysConfigMapper mapper = getSqlSession().getMapper(SysConfigMapper.class);
List<SysConfig> sysConfigs1 = mapper.selectConfigList(cond);
}
从上面看到,我们获取定义Mapper接口的实例是通过SqlSession获取的那么,基本上可以分析出来创建Mapper代理对象的逻辑就在里面。
因此我们来查看SqlSession的源码,SqlSession默认实现是DefaultSqlSession。
DefaultSqlSession#getMapper
@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
可以看到又用到了上面反复强调的Configuration类,DefaultSqlSession的getMapper实际是调用了Configuration#getMapper。
Configuration#getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
没什么好说的继续看:
MapperRegistry#getMapper 实现生成Mapper实例:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
可以看到最关键的是通过:mapperProxyFactory.newInstance(sqlSession);创建了一个Mapper实例。
接下来直接看
MapperProxyFactory#newInstance(org.apache.ibatis.session.SqlSession)
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
在这里我们看到了一个关键类MapperProxy,这个类就是Mapper接口实例的基类,也就是调用接口中被代理方法的具体实现。
如果不太理解,我们回忆一下JDK动态代理的写法:
//第一个参数被代理类的类加载器
//第二个参数mapperInterface的实现接口
//第三参数是InvocationHandler
(T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
可以看到JDK代理第三个参数,需要传递一个InvocationHandler的实例,该实例中有个方法是invoke,该方法就是要代理类的具体逻辑(接口只是声明,没有逻辑)因此具体的逻辑还是需要自己实现,而实现的核心就是这个MapperProxy
走到这一步,就完成了Mapper接口实例的创建。
简单总结一下,创建Mapper接口实例过程:
- 入口在SqlSession的getMapper方法创建Mapper实例,调用 SqlSession#getMapper Configuration#getMapper MapperRegistry#getMapper MapperProxyFactory#newInstance(org.apache.ibatis.session.SqlSession)
- 在MapperProxyFactory的newInstance中,创建了一个MapperProxy类实例,该实例实现了InvocationHandler接口,是Mapper接口调用的基类
- 调用JDK提供的代理生成工具Proxy.newProxyInstance生成代理类
Mapper实例调用
当我们用java代码对Mapper接口代理的实例进行调用的时候,实际逻辑是由MapperProxy类中的invoke实现的。
直接快进到MapperProxy#invoke方法:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 如果调用的方法是Object里面的方法,直接反射调用
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
// 通过缓存获取到具体的调用者(Invoker)进行反射调用接口方法
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
MapperProxy#cachedInvoker通过缓存获取到具体的调用者
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
// methodCache用来缓存方法调用者的,方便以后不用每次调用Mapper实例方法都进行一次判断
return MapUtil.computeIfAbsent(methodCache, method, m -> {
// 判断是否默认方法,因为JDK8开始,可以在接口中定义default方法,default方法还分为java8和9
if (m.isDefault()) {
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
} else {
// 最常用的方法调用者,用得最多的也是它
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
PlainMethodInvoker,在创建PlainMethodInvoker的时候,必须要传一个MapperMethod对象,该对象内部有个SqlCommand会描述本次执行方法的类型,如是Insert、Update...以及该方法对应的完整namespace。
private static class PlainMethodInvoker implements MapperMethodInvoker {
private final MapperMethod mapperMethod;
public PlainMethodInvoker(MapperMethod mapperMethod) {
super();
this.mapperMethod = mapperMethod;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
}
直接通过MapperMethod对象,调用其execute执行代理方法逻辑:
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
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:
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);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
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的方法,执行sql语句,还记得前面分析SqlSession提供了一系列的方法insert、select、update、select....吗,核心就是调用那些这些方法。最后Executor执行sql语句,从而完成一次Mapper接口方法调用。
小结一下:
- 每次调用Mapper接口方法的时候,实际都是调用的Mapper代理对象所谓委托的执行者MapperProxy
- 在MapperProxy中会通过方法方法类型,寻找到对应的调用者MapperMethodInvoker,主要是因为Interface中可以定义default方法
- 在MapperMethodInvoker中,会根据方法执行类型,来调用SqlSession提供的操作数据库方法,最后封装返回结果,进行返回。
方法执行类型,主要也是通过前面说到的解析解析mapper.xml的时候生成的MappedStatement来判断的
扩展
MyBatis每次执行数据库操作,会生成新的对象吗,也就是说性能开销大吗?
根据我们的分析分析,MyBatis在执行过程中,除了第一次执行会生成一些对象,如对应的Invoker以及MapperMethod,在执行数据库操作之前,最大的开销就是解析sql语句,但是此处的sql语句解析脱离了mapper.xml,因为都转换成了SqlNode,因此都是在内存中进行解析拼接的,所以开销并不大。
并且在MyBatis中运用了大量的缓存手段,增强了对象的复用,减少了GC负担。
Spring是何时将mapper代理对象放入容器中的?是单例的吗?
首先Spring中对象的默认作用域都是单例的,因此Mapper的实例默认也是单例的,当然可以对其使用注解
@Scope("prototype")声明为原型的,也就是多例的。Spring创建Mapper实例时机在启动解析依赖注入的时候,需要使用到该Mapper@Resource、@Autowire这种属性注入的时候。简单来说,就是将Mapper实例加入到Spring容器中,启动自动创建。
这时候就引出了一个新问题,上面分析了创建Mapper的入口是mybatis提供的DefaultSqlSession.getMapper方法进行创建,我们都支持Spring启动的时候对实例的创建都是通过反射创建的,那么Spring是如何做到创建Mapper代理对象的?
这时候就不得不提一个Spring的扩展,也就是FactoryBean,它可以指定在Spring启动,调用回调方法创建对象,而不是使用Spring的默认创建对象方式进行创建。
Spring就为MyBatis提供了一个MapperFactoryBean对象,将生成代理逻辑放入了MapperFactoryBean的getObject方法中。类似的做法还有AOP的ProxyFactoryBean,动态生成代理对象也是这个原理。
另外,Spring对MyBatis还重写了SqlSession,不再使用MyBatis提供的DefaultSqlSession,而是使用自定义的SqlSessionTemplate。
要不要顺便猜猜为甚@Mapper就可以将Mapper实例加入到Spring容器中,这个也是Spring的知识,涉及到Spring的BeanScanner知识了,如果用@MapperScan默认声明就为,当然也可以自己使用MapperScan指定生成代理对象。
public @interface MapperScan {
....
Class<? extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class;
....
}
在MapperScannerRegistrar#registerBeanDefinitions(...)会将扫描到的所有Mapper接口,通过BeanDefinitionBuilder生成BeanDefinition,从而指定实例class为MapperFactoryBean
Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean"); if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) { builder.addPropertyValue("mapperFactoryBeanClass", mapperFactoryBeanClass); }