【源码阅读】Mybatis底层源码分析(详细Debug查看附代码)

一、搭建测试代码框架

(代码已提交到github->测试代码,建议结合代码根据本文debug一遍更有利于理解,帮忙点个Star 哈,本人在这里谢谢了)

二、猜想Mybatis是如何设计的

从上面的案例中,可以大致可以猜测到Mybatis一共做了哪些步骤。

1.定位到mybatis-config.xml并读取装载。获取输入流InputStream。

2.解析输入流InputStream,把mybatis-config.xml配置文件中相关配置项解析,校验,保存起来。

3.创建sqlSessionFactory对象,在我们的印象里,session就是一次会话,所以可以理解sqlSessionFactory就是个工厂类,就专门创建sqlSession对象,并且这个sqlSessionFactory工厂类是唯一不变的(单例)。

4.创建sqlSession,SqlSession中肯定保存了配置文件内容信息和执行数据库相关的操作。

5.获取userMapper对象,但是UserMapper是接口,并且没有实现类。怎么就可以调用其方法呢?这里猜想可能用到了动态代理。

6.seleteById()方法和SQL关联(主要是使用的时候,都是方法名必须和SQL中statementId一致,由此猜想的)userMapper接口中的方法是如何关联到SQL的,这个猜想可能是有个专门映射的类,另外,肯定使用到了接口全路径名+方法名称,这个才能确保。

7.最后底层使用JDBC去操作数据库。

JDBC是如何操作的不清楚的可以启动测试方法中的(Main),可通过下面日志进行查看可以了解到它的一个执行流程。

8.作为一个持久化框架,很有可能会使用到缓存,用来存储每次查询数据。

仅仅是个人假设不清楚源码的情况,仅仅从这个简单的案例出发的,案例中没有差距、缓存,但是下面源码分析中是有的。

三、 开始debug代码(深层查看源码)

1、读取mybatis-config.xml,得到是InputStream输入流

2、解析mybatis-config.xml配置文件,创建sqlSessionFactory
// 解析mybatis-config.xml配置文件,创建sqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

SqlSessionFactory没有构造方法,那么这里使用的就是默认无参构造方法,直接进去build方法。

该类中的build重载的方法如下:

SqlSessionFactory中提供了三种读取配置信息的方法: 字节流、字符流和Configuration配置类。

接着查看源码:

        new XMLConfigBuilder() 对象,可以看到XMLConfigBuilder这个类是BaseBuilder的子类

BaseBuilder类图:

看到这些子类基本上都是以Builder结尾,所以这里使用的是 建造者设计模式

这个类名可以猜出该类就是解析xml配置文件的。继续进入XMLConfigBuilder

这里会涉及到 parsing 中相关类的操作:

其中Mybatis对应解析包org.apache.ibatis.parsing:

XPathParser基于 Java XPath 解析器,用于解析 MyBatis中

  • mybatis-config.xml
  • mapper.xml

等 XML 配置文件 。

XPathParser主要内容:

继续上面的源码分析:

然后我们再回到SqlSessionFactoryBuilder中的build方法里:

先看parser.parse()方法:

进入parseConfiguration()方法:

结合 mybatis-config.xml配置文件和解析方法,可以得出如何关联:

mybatis-config.xml中如果把标签当做一级标签,那么有多少个二级标签可与定义呢?

在org.apache.ibatis.builder.xml下的mybatis-3-config.dtd中已经定义了

 <!ELEMENT configuration (properties?, settings?, typeAliases?, typeHandlers?, objectFactory?, objectWrapperFactory?, reflectorFactory?, plugins?, environments?, databaseIdProvider?, mappers?)>

与之对应的mybatis-config.xsd中

 <xs:element name="configuration"><xs:complexType><xs:sequence><xs:element minOccurs="0" ref="properties"/><xs:element minOccurs="0" ref="settings"/><xs:element minOccurs="0" ref="typeAliases"/><xs:element minOccurs="0" ref="typeHandlers"/><xs:element minOccurs="0" ref="objectFactory"/><xs:element minOccurs="0" ref="objectWrapperFactory"/><xs:element minOccurs="0" ref="reflectorFactory"/><xs:element minOccurs="0" ref="plugins"/><xs:element minOccurs="0" ref="environments"/><xs:element minOccurs="0" ref="databaseIdProvider"/><xs:element minOccurs="0" ref="mappers"/></xs:sequence></xs:complexType></xs:element>

我们最关心的Mapper.xml中能定义哪些标签,也在mybatis-3-mapper.dtd中也定义了,另外也有与之对应的 mybatis-mapper.xsd 文件中也能找到:

下面看看这些标签内容是如何存入configuration对象中?(这里例举部分,挑几个相对重要的)。

propertiesElement()源码分析:

  private void propertiesElement(XNode context) throws Exception {if (context != null) {// 如果在这些地方,属性多于一个的话,MyBatis 按照如下的顺序加载它们:// 1.在 properties 元素体内指定的属性首先被读取。// 2.从类路径下资源或 properties 元素的 url 属性中加载的属性第二被读取,它会覆盖已经存在的完全一样的属性。// 3.作为方法参数传递的属性最后被读取, 它也会覆盖任一已经存在的完全一样的属性,这些属性可能是从 properties 元素体内和资源/url 属性中加载的。// 传入方式是调用构造函数时传入,public XMLConfigBuilder(Reader reader, String environment, Properties props)// 1.XNode.getChildrenAsProperties函数方便得到孩子所有PropertiesProperties defaults = context.getChildrenAsProperties();// 2.然后查找resource或者url,加入前面的PropertiesString resource = context.getStringAttribute("resource");String url = context.getStringAttribute("url");if (resource != null && url != null) {throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");}if (resource != null) {defaults.putAll(Resources.getResourceAsProperties(resource));} else if (url != null) {defaults.putAll(Resources.getUrlAsProperties(url));}// 3.Variables也全部加入PropertiesProperties vars = configuration.getVariables();if (vars != null) {defaults.putAll(vars);}parser.setVariables(defaults);configuration.setVariables(defaults);}}

typeAliasesElement()源码分析

private void typeAliasesElement(XNode parent) {if (parent != null) {for (XNode child : parent.getChildren()) {if ("package".equals(child.getName())) {//如果是packageString typeAliasPackage = child.getStringAttribute("name");//(一)调用TypeAliasRegistry.registerAliases,去包下找所有类,然后注册别名(有@Alias注解则用,没有则取类的simpleName)configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);} else {//如果是typeAliasString alias = child.getStringAttribute("alias");String type = child.getStringAttribute("type");try {Class<?> clazz = Resources.classForName(type);//根据Class名字来注册类型别名//(二)调用TypeAliasRegistry.registerAliasif (alias == null) {//alias可以省略typeAliasRegistry.registerAlias(clazz);} else {typeAliasRegistry.registerAlias(alias, clazz);}} catch (ClassNotFoundException e) {throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);}}}}}

插件plugins解析

pluginElement()方法:(可定义多个)

Configuration中interceptorChain用来存储所有定义的插件,InterceptorChain插件链(连接链),这里使用了责任链模式。

继续看Mapper.xml是如何解析的。

mapper.xml解析

Mapper.xml在mybatis-config.xml中的配置是这样的:

使用方式有以下四种:

<--! 1使用类路径 -->
<mappers><mapper resource="org/mybatis/builder/AuthorMapper.xml"/><mapper resource="org/mybatis/builder/BlogMapper.xml"/><mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<--! 2使用绝对url路径 -->
<mappers><mapper url="file:///var/mappers/AuthorMapper.xml"/><mapper url="file:///var/mappers/BlogMapper.xml"/><mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<--! 3使用java类名 -->
<mappers><mapper class="org.mybatis.builder.AuthorMapper"/><mapper class="org.mybatis.builder.BlogMapper"/><mapper class="org.mybatis.builder.PostMapper"/>
</mappers><--! 4自动扫描包下所有映射器 -->
<mappers><package name="org.mybatis.builder"/>
</mappers>

源码分析:

private void mapperElement(XNode parent) throws Exception {if (parent != null) {for (XNode child : parent.getChildren()) {if ("package".equals(child.getName())) {// 10.4自动扫描包下所有映射器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) {// 10.1使用类路径ErrorContext.instance().resource(resource);InputStream inputStream = Resources.getResourceAsStream(resource);// 映射器比较复杂,调用XMLMapperBuilder// 注意在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());mapperParser.parse();} else if (resource == null && url != null && mapperClass == null) {// 10.2使用绝对url路径ErrorContext.instance().resource(url);InputStream inputStream = Resources.getUrlAsStream(url);// 映射器比较复杂,调用XMLMapperBuilderXMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());mapperParser.parse();} else if (resource == null && url == null && mapperClass != null) {// 10.3使用java类名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.");}}}}}

把这些UserMapper类似接口保存到configuration对象中。

configuration.addMapper(mapperInterface);

到这里,配置文件mybatis-config.xml和我们定义映射文件XxxMapper.xml就全部解析完成。

关于其他配置项,解析方式类似,最终都保存到了一个Configuration大对象中。

Configuration对象类似于单例模式,就是整个Mybatis中只有一个Configuration对象。

回到SqlSessionFactoryBuilder类

前面讲到了XMLConfigBuilder中的parse方法,并返回了一个Configuration对象。

build(parser.parse());

这个build方法就是传入一个Configuration对象,然后构建一个DefaultSqlSession对象。

public SqlSessionFactory build(Configuration config) {return new DefaultSqlSessionFactory(config);
}

继续回到demo代码中这一行代码里。

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

这一行代码就相当于:

SqlSessionFactory sqlSessionFactory = new new DefaultSqlSessionFactory();

到此。配置文件解析完毕。

配置文件解析流程:

既然已经获取到了SqlSessionFactory,那么我们就可以构建SqlSession了。下面我们回到  (main)测试方法继续 来看看构建SqlSession的整个过程。

3、构建SqlSession

前面已经做了配置文件的解析,那么现在我们来构建SqlSession。

// 创建sqlSession
sqlSession = sqlSessionFactory.openSession();

前面已经分析了,这里的sqlSessionFactory是DefaultSqlSessionFactory。那么此时调用的openSession()方法为DefaultSqlSessionFactory中的方法。

源码分析:

public class DefaultSqlSessionFactory implements SqlSessionFactory {//配置文件所有内容private final Configuration configuration;//创建session@Overridepublic SqlSession openSession() {// 调用的是另外一个openSessionFromDataSource方法return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);}// 其实是调用这个方法//protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {Transaction tx = null;try {// 对应xml标签<environments> ,这个在配置文件解析的时候就已经存放到configuration中了。final Environment environment = configuration.getEnvironment();// 构建事务工厂final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);// 构建一个事务对象  tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);// 创建一个executor来执行SQL  final Executor executor = configuration.newExecutor(tx, execType);// 创建一个DefaultSqlSession对象并返回return new DefaultSqlSession(configuration, executor, autoCommit);} catch (Exception e) {closeTransaction(tx); // may have fetched a connection so lets call close()throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);} finally {ErrorContext.instance().reset();}}private TransactionFactory getTransactionFactoryFromEnvironment(Environment environment) {if (environment == null || environment.getTransactionFactory() == null) {return new ManagedTransactionFactory();}return environment.getTransactionFactory();}
3.1 创建事务Transaction

Transaction类图:

事务工厂类型可以配置为JDBC类型或者MANAGED类型。

  • JdbcTransactionFactory生产JdbcTransaction。
  • ManagedTransactionFactory生产ManagedTransaction。

如果配置的JDBC,则会使用Connection对象的commit()、rollback()、close()方法来管理事务。

如果配置的是MANAGED,会把事务交给容器来管理,比如JBOSS,Weblogic。

因为是本地跑的程序,如果配置成MANAGED就会不有任何事务。

但是,如果是Spring+Mybatis,则没有必要配置,因为我们会直接在applicationContext.xml里配置数据源和事务管理器,从而覆盖Mybatis的配置。

把事务传给newExecutor()方法创建执行器Executor对象。

configuration.newExecutor(tx, execType)
3.2 创建Executor

调用configuration的newExecutor方法创建Executor。

final Executor executor = configuration.newExecutor(tx, execType);
//Configuration中
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {executorType = executorType == null ? defaultExecutorType : executorType;executorType = executorType == null ? ExecutorType.SIMPLE : executorType;Executor executor;//第一步if (ExecutorType.BATCH == executorType) {executor = new BatchExecutor(this, transaction);} else if (ExecutorType.REUSE == executorType) {executor = new ReuseExecutor(this, transaction);} else {executor = new SimpleExecutor(this, transaction);}//第二步if (cacheEnabled) {executor = new CachingExecutor(executor);}//第三步executor = (Executor) interceptorChain.pluginAll(executor);return executor;}

此方法分三个步骤。

第一步:创建执行器

Executor的基本类型有三种:SIMPLE为默认类型。

public enum ExecutorType {SIMPLE, REUSE, BATCH
}

Executor类图:

为什么要让抽象类BaseExecutor实现Executor接口,然后让具体实现类继承抽象类呢?

这就是模板方法模式的实现。

模板方法模式就是定义一个算法骨架,并允许子类为一个或者多个步骤提供实现。模板方法是得子类可以再不改变算法结构的情况下,重新定义算法的某些步骤。

抽象方法是在子类汇总实现的,每种执行器自己实现自己的逻辑,BaseExecutor最终会调用到具体的子类。

抽象方法

第二步:缓存装饰

在上面代码中的第二步

if (cacheEnabled) {executor = new CachingExecutor(executor);
}

如果cacheEnabled=true,会用装饰器设计模式对Executor进行装饰。

第三步:插件代理

缓存装饰完后,就会执行

executor = (Executor) interceptorChain.pluginAll(executor);

这里会对Executor植入插件逻辑。

比如:分页插件中就需要把插件植入的Executor

好了,到此,执行器创建的就搞定了

Executor创建完毕后,就该创建DefaultSqlSession了,代码:

// 创建一个DefaultSqlSession对象并返回
return new DefaultSqlSession(configuration, executor, autoCommit);

进入DefaultSqlSession的构造方法中:

DefaultSqlSession中包含两个重要属性:

自此,SqlSession对象构建完毕。

sqlSession = sqlSessionFactory.openSession();

这里的sqlSession就是:

sqlSession = new DefaultSqlSession();

整个构建过程:

4、获取UserMapper接口的代理对象

前面已经知道Mybatis配置文件解析到获取SqlSession,下面分析从SqlSession到userMapper:

// 创建userMapper对象(UserMapper并没有实现类)
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

已经知道了这里的sqlSession使用的是默认实现类DefaultSqlSession。

直接进入DefaultSqlSession的getMapper方法。

这里有三个疑问:

问题1:getMapper返回的是个什么对象?

可以看出,getMapper方法调用的是Configuration中的getMapper方法。然后进入Configuration中

// 在Configuration类中  
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
// type=UserMapper.class
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {return mapperRegistry.getMapper(type, sqlSession);
}

这里也没做什么,继续调用MapperRegistry中的getMapper:

源码分析: 

// MapperRegistry中
public class MapperRegistry {// 主要是存放配置信息private final Configuration config;// MapperProxyFactory 的映射private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();// 获得 Mapper Proxy 对象//type=UserMapper.class,session为当前会话public <T> T getMapper(Class<T> type, SqlSession sqlSession) {// 这里是get,那就有add或者putfinal 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);}}// 解析配置文件的时候就会调用这个方法,// type=UserMapper.classpublic <T> void addMapper(Class<T> type) {// 判断 type 必须是接口,也就是说 Mapper 接口。if (type.isInterface()) {// 已经添加过,则抛出 BindingException 异常if (hasMapper(type)) {throw new BindingException("Type " + type + " is already known to the MapperRegistry.");}boolean loadCompleted = false;try {// 添加到 knownMappers 中knownMappers.put(type, new MapperProxyFactory<>(type));// 创建 MapperAnnotationBuilder 对象,解析 Mapper 的注解配置MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);parser.parse();// 标记加载完成loadCompleted = true;} finally {// 若加载未完成,从 knownMappers 中移除if (!loadCompleted) {knownMappers.remove(type);}}}
}
}

MapperProxyFactory对象里保存了mapper接口的class对象,就是一个普通的类,没有什么逻辑。

在这个类里可以理解使用了单例模式methodCache(注册式单例模式), 和工厂模式getMapper()。

public class MapperProxyFactory<T> {private final Class<T> mapperInterface;private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();public MapperProxyFactory(Class<T> mapperInterface) {this.mapperInterface = mapperInterface;}public Class<T> getMapperInterface() {return mapperInterface;}public Map<Method, MapperMethod> getMethodCache() {return methodCache;}@SuppressWarnings("unchecked")protected T newInstance(MapperProxy<T> mapperProxy) {// 最终以JDK动态代理创建对象并返回return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);}public T newInstance(SqlSession sqlSession) {// 创建 MapperProxy 对象final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);return newInstance(mapperProxy);}}

返回到 MapperRegistry类中 getMapper 方法,继续看这行重点代码:

    return mapperProxyFactory.newInstance(sqlSession);

newInstance()方法:

依然是基于 JDK Proxy 实现,而 InvocationHandler 参数是 MapperProxy 对象。

// loader 是 UserMapper 的类加载器
// interfaces 是 UserMapper
// h 是 mapperProxy 对象
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h){.....
}
问题2:为什么就可以调用他的方法?

上面调用newInstance方法时候创建了MapperProxy对象,并且是当做newProxyInstance的第三个参数,所以MapperProxy类肯定实现了InvocationHandler。

进入MapperProxy类中:

// 果然实现了InvocationHandler接口
public class MapperProxy<T> implements InvocationHandler, Serializable {private static final long serialVersionUID = -6424540398559729838L;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;}// 调用 userMapper.selectById() 实质上是调用这个invoke方法@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {// 代理以后,所有Mapper的方法调用时,都会调用这个invoke方法// 并不是任何一个方法都需要执行调用代理对象进行执行,如果这个方法是Object中通用的方法(toString、hashCode等)无需执行if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, args);} else if (method.isDefault()) {// JDK8以后的接口默认实现方法  return invokeDefaultMethod(proxy, method, args);}} catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);}// 这里优化了,去缓存中找MapperMethod// 创建MapperMethod对象final MapperMethod mapperMethod = cachedMapperMethod(method);// 执行(会去根据insert|update|delete|select 4种情况,分别调用SqlSession的4大类方法)return mapperMethod.execute(sqlSession, args);}
}

其中  mapperMethod.execute(sqlSession, args) :

public Object execute(SqlSession sqlSession, Object[] args) {Object result;// 可以看到执行时就是4种情况,insert|update|delete|select,分别调用SqlSession的4大类方法if (SqlCommandType.INSERT == command.getType()) {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.insert(command.getName(), param));} else if (SqlCommandType.UPDATE == command.getType()) {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.update(command.getName(), param));} else if (SqlCommandType.DELETE == command.getType()) {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.delete(command.getName(), param));} else if (SqlCommandType.SELECT == command.getType()) {if (method.returnsVoid() && method.hasResultHandler()) {// 如果有结果处理器executeWithResultHandler(sqlSession, args);result = null;} else if (method.returnsMany()) {// 如果结果有多条记录result = executeForMany(sqlSession, args);} else if (method.returnsMap()) {// 如果结果是mapresult = 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;}

也就是说,getMapper方法返回的是一个JDK动态代理对象(类型是$Proxy+数字)。这个代理对象会继承Proxy类,实现被代理的接口UserMpper,里面持有了一个MapperProxy类型的触发管理类。

当调用UserMpper的方法时候,实质上调用的是MapperProxy的invoke方法。userMapper=$Proxy6@3252。

现在回答前面的问题:

为什么要在MapperRegistry中保存一个工厂类,原来他是用来创建并返回代理类的。这里是代理模式的一个非常经典的应用。

MapperProxy如何实现对接口的代理?

我们知道,JDK动态代理有三个核心角色:

  • 被代理类(即就是实现类)
  • 接口
  • 实现了InvocationHanndler的触发管理类,用来生成代理对象。

被代理类必须实现接口,因为要通过接口获取方法,而且代理类也要实现这个接口。

而Mybatis中并没有Mapper接口的实现类,怎么被代理呢?它忽略了实现类,直接对Mapper接口进行代理。

MyBatis动态代理:

在Mybatis中,JDK动态代理为什么不需要实现类呢?

这里我们的目的其实就是根据一个可以执行的方法,直接找到Mapper.xml中statement ID ,方便调用。

最后返回的userMapper是MapperProxyFactory创建的代理对象,这个对象中包含MapperProxy对象。

问题3:到底是怎么根据 Mapper.java 找到 Mapper.xml 的?

最后调用userMapper.selectUserById(),本质上调用的是MapperProxy的invoke()方法。

如果根据(接口+方法名找到Statement ID ),这个逻辑在InvocationHandler子类(MapperProxy类)中就可以完成了,其实也就没有必要在用实现类了。

整个流程

自此,我们已经拿到了UserMapper接口的代理对象。接下来我们就去调用这个代理对象的方法。

5、UserMapp中的方法和SQL如何关联
User user = userMapper.selectById(1));

通过前面的分析,我们已经知道了userMapper是通过动态代理生成的代理对象,

所以调用这个代理对象的任意方法都是执行触发管理类MapperProxy的invoke()方法。

分为两部分:

1、MapperProxy.invoke() 到 Executor.query(方法和SQL关联)。

2、Executor.query 到 JDBC 中的SQL执行。

第一部分流程图:

MapperProxy.invoke()

先来看看这个invoke()方法里有些什么逻辑。

//MapperProxy类
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {//首先判断是否为Object本身的方法,是则不需要去执行SQL,//比如:toString()、hashCode()等方法。if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, args);} else if (method.isDefault()) {//判断是否JDK8以后的接口默认实现方法。return invokeDefaultMethod(proxy, method, args);}} catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);}//<3>  final MapperMethod mapperMethod = cachedMapperMethod(method);//<4>return mapperMethod.execute(sqlSession, args);
}

从缓存获取MapperMethod,这里加入了缓存主要是为了提升MapperMethod的获取速度。这个设计非常的有意思,缓存的使用在Mybatis中也是非常之多。

private final Map<Method, MapperMethod> methodCache;
private MapperMethod cachedMapperMethod(Method method) {return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));}

Map的computeIfAbsent方法:根据key获取值,如果值为null,则把后面的Object的值付给key。

继续看MapperMethod这个类,定义了两个属性command和method,以及两个静态内部类。

public class MapperMethod {private final SqlCommand command;private final MethodSignature method;public static class SqlCommand {private final String name;private final SqlCommandType type;public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {final String methodName = method.getName();final Class<?> declaringClass = method.getDeclaringClass();//获得 MappedStatement 对象MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,configuration);// <2> 找不到 MappedStatementif (ms == null) {// 如果有 @Flush 注解,则标记为 FLUSH 类型if (method.getAnnotation(Flush.class) != null) {name = null;type = SqlCommandType.FLUSH;} else { // 抛出 BindingException 异常,如果找不到 MappedStatement//(开发中容易见到的错误)说明该方法上,没有对应的 SQL 声明。throw new BindingException("Invalid bound statement (not found): "+ mapperInterface.getName() + "." + methodName);}//找到 MappedStatement} else {// 获得 name//id=com.tian.mybatis.mapper.UserMapper.selectByIdname = ms.getId();// 获得 type=SELECTtype = ms.getSqlCommandType();//如果type=UNKNOWNif (type == SqlCommandType.UNKNOWN) { // 抛出 BindingException 异常,如果是 UNKNOWN 类型throw new BindingException("Unknown execution method for: " + name);}}}   private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,Class<?> declaringClass, Configuration configuration) {// 获得编号//com.tian.mybatis.mapper.UserMapper.selectByIdString statementId = mapperInterface.getName() + "." + methodName;//如果有,获得 MappedStatement 对象,并返回if (configuration.hasStatement(statementId)) {//mappedStatements.get(statementId);  //解析配置文件时候创建并保存Map<String, MappedStatement> mappedStatements中return configuration.getMappedStatement(statementId);// 如果没有,并且当前方法就是 declaringClass 声明的,则说明真的找不到} else if (mapperInterface.equals(declaringClass)) {return null;}// 遍历父接口,继续获得 MappedStatement 对象for (Class<?> superInterface : mapperInterface.getInterfaces()) {if (declaringClass.isAssignableFrom(superInterface)) {MappedStatement ms = resolveMappedStatement(superInterface, methodName,declaringClass, configuration);if (ms != null) {return ms;}}}// 真的找不到,返回 nullreturn null;} //....}
public static class MethodSignature {private final boolean returnsMap;private final Class<?> returnType;private final Integer rowBoundsIndex;//....
}

SqlCommand封装了statement ID,比如说:

com.tian.mybatis.mapper.UserMapper.selectById

和SQL类型。

public enum SqlCommandType {
UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
}

另外还有个属性MethodSignature,主要是封装的是返回值的类型和方法入参。这里我们debug看看这个MapperMethod对象返回的内容和我们案例中代码的关联。

接着看MapperMethod中execute方法。

MapperMethod.execute()

先来看看这个方法的整体逻辑:

 public Object execute(SqlSession sqlSession, Object[] args) {Object result;switch (command.getType()) {case SELECT://部分代码省略Object param = method.convertArgsToSqlCommandParam(args);//本次是QUERY类型,所以这里是重点  result = sqlSession.selectOne(command.getName(), param);if (method.returnsOptional()&& (result == null || !method.getReturnType().equals(result.getClass()))) {result = Optional.ofNullable(result);}break;default:throw new BindingException("Unknown execution method for: " + command.getName());} return result;}

这个方法中,根据我们上面获得的不同的type(INSERT、UPDATE、DELETE、SELECT)和返回类型:

1.调用convertArgsToSqlCommandParam()将方法参数转换为SQL的参数。

2.调用sqlSession的insert()、update()、dalete()、selectOne()方法。

我们这个案例是查询,这里回到了DefaultSqlSession中selectOne方法中。

SqlSession.selectOne()
//DefaultSqlSession中  
@Override
public <T> T selectOne(String statement, Object parameter) {//这是一种好的设计方法//不管是执行多条查询还是单条查询,都走selectList方法(重点)List<T> list = this.selectList(statement, parameter);if (list.size() == 1) {//如果只有一条就返回第一条return list.get(0);} else if (list.size() > 1) {//(开发中常见错误)方法定义的是返回一条数据,结果查出了多条数据,就会报这个异常throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());} else {//数据库中没有数据就返回nullreturn null;}}

这里调用的是selectList方法。

@Override
public <E> List<E> selectList(String statement, Object parameter) {return this.selectList(statement, parameter, RowBounds.DEFAULT);}@Overridepublic <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {try {//从configuration获取MappedStatement//此时的statement=com.tian.mybatis.mapper.UserMapper.selectByIdMappedStatement ms = configuration.getMappedStatement(statement);//调用执行器中的query方法return executor.query(...);} catch (Exception e) {//.....} finally {ErrorContext.instance().reset();}}

在这个方法里是根据statement从configuration对象中获取MappedStatement。

MappedStatement ms = configuration.getMappedStatement(statement);

在configuration中getMappedStatement方法:

//存放在一个map中的
//key是statement=com.tian.mybatis.mapper.UserMapper.selectById,value是MappedStatement
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>();  public MappedStatement getMappedStatement(String id) {return this.getMappedStatement(id, true);}
public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) { return mappedStatements.get(id);
}

而MappedStatement里面有xml中增删改查标签配置的所有属性,包括id、statementType、sqlSource、入参、返回值等。

到此,我们UserMapper类中的方法已经和UserMapper.xml中的sql给彻底关联起来了。继续

executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

这里执行的是执行器Executor中的query()方法。

Executor.query()方法

这里的Executor对象是在调用openSession方法的时候创建的。

下面来看看调用执行器的query()放的整个流程(第二部分流程):

下面看看具体源码是如何实现的。

CachingExecutor.query()

在CachingExecutor中

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameterObject);CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

BoundSql中主要是SQL和参数:

既然是缓存,可以想到key-value数据结构。

下面来看看这个key生成规则:

这个二级缓存是怎么构成的呢?并且还要保证在查询的时候必须是唯一。

也就说,构成key主要有:

方法相同、翻页偏移量相同、SQL相同、参数相同、数据源环境相同才会被认为是同一个查询。

这里大家知道这个层面就已经阔以了。如果向更深入的搞,就得把hashCode这些扯进来了,请看上面这个张图里前面的几个属性。

处理二级缓存

首先是从ms中取出cache对象,判断cache对象是否为null,如果为null,则没有查询二级缓存和写入二级缓存的流程。

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {Cache cache = ms.getCache();//判断是否有二级缓存if (cache != null) {flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) {ensureNoOutParams(ms, boundSql);@SuppressWarnings("unchecked")List<E> list = (List<E>) tcm.getObject(cache, key);if (list == null) {list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);tcm.putObject(cache, key, list); // issue #578 and #116}return list;}}return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

那么这个Cache对象是什么创建的呢?

二级缓存如何开启?

配置项:

<configuration><settings><setting name="cacheEnabled" value="true|false" /></settings>
</configuration>

cacheEnabled=true表示二级缓存可用,但是要开启话,需要在Mapper.xml内配置。

<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
<!--或者 简单方式-->
<cache/>

对配置项属性说明:

  • flushInterval="60000",间隔60秒清空缓存,这个间隔60秒,是被动触发的,而不是定时器轮询的。
  • size=512,表示队列最大512个长度,大于则移除队列最前面的元素,这里的长度指的是CacheKey的个数,默认为1024。
  • readOnly="true",表示任何获取对象的操作,都将返回同一实例对象。如果readOnly="false",则每次返回该对象的拷贝对象,简单说就是序列化复制一份返回。
  • eviction:缓存会使用默认的Least Recently Used(LRU,最近最少使用的)算法来收回。FIFO:First In First Out先进先出队列。

在解析Mapper.xml的XMLMapperBuilder类中的cacheElement()方法里。

解析二级缓存中的标签:

创建Cache对象:

二级缓存处理完了,就来到BaseExecutor的query方法中。

BaseExecutor.query()

第一步,清空缓存

if (queryStack == 0 && ms.isFlushCacheRequired()) {clearLocalCache();
}

queryStack用于记录查询栈,防止地柜查询重复处理缓存。

flushCache=true的时候,会先清理本地缓存(一级缓存)。

如果没有缓存会从数据库中查询

 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

在看看这个方法的逻辑:

private <E> List<E> queryFromDatabase(...) throws SQLException {List<E> list;//使用占位符的方式,先抢占一级缓存。localCache.putObject(key, EXECUTION_PLACEHOLDER);try {list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);} finally {//删除上面抢占的占位符  localCache.removeObject(key);}//放入一级缓存中localCache.putObject(key, list);return list;
}

先在缓存使用占位符占位,然后查询,移除占位符,将数据放入一级缓存中。

执行Executor的doQuery()方法,默认使用SimpleExecutor。

list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);

下面就来到了SimpleExecutor中的doQuery方法。

SimpleExecutor.doQuery
@Override
public <E> List<E> doQuery(....) throws SQLException {Statement stmt = null;try {//获取配置文件信息  Configuration configuration = ms.getConfiguration();//获取handlerStatementHandler handler = configuration.newStatementHandler(....);//获取Statementstmt = prepareStatement(handler, ms.getStatementLog());//执行RoutingStatementHandler的query方法  return handler.query(stmt, resultHandler);} finally {closeStatement(stmt);}
}
创建StatementHandler

在configuration中newStatementHandler()里,创建了一个StatementHandler,先得到RoutingStatementHandler(路由)。

public StatementHandler newStatementHandler() {StatementHandler statementHandler = new RoutingStatementHandler//执行StatementHandler类型的插件    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);return statementHandler;
}

RoutingStatementHandler创建的时候是用来创建基本的StatementHandler的。这里会根据MapperStament里面的statementType决定StatementHandler类型。

默认是PREPARED。

StatementHandler里面包含了处理参数的ParameterHandler和处理结果集的ResultHandler。

上面说的这几个对象正式被插件拦截的四大对象,所以在创建的时都要用拦截器进行包装的方法。

对于插件相关的,请看前面发的插件的文章。

创建Statement

创建对象后就会执行RoutingStatementHandler的query方法。

//RoutingStatementHandler中 
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {//委派delegate=PreparedStatementHandlerreturn delegate.query(statement, resultHandler);
}

这里设计很有意思,所有的处理都要使用RoutingStatementHandler来路由,全部通过委托的方式进行调用。

然后执行到PreparedStatementHandler中的query方法。

@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {PreparedStatement ps = (PreparedStatement) statement;//JDBC的流程了  ps.execute();//处理结果集,如果有插件代理ResultHandler,会先走到被拦截的业务逻辑中return resultSetHandler.handleResultSets(ps);
}

看到了ps.execute();表示已经到JDBC层面了,这时候SQL就已经执行了。后面就是调用DefaultResultSetHandler类进行处理。

到这里,SQL语句就执行完毕,并将结果集赋值并返回了。

总结

        本文从一个案例代码出发,到解析Mybatis的配置文件,到创建SqlSession对象,到获取UserMapper接口的代理对象,到调用代理对象方法,再到让方法和SQL关联起来,最后执行SQL,返回结果集。

涉及到的设计模式:单例模式、建造者设计模式、模板方法模式、代理模式、装饰器模式等

整个流程

从调用userMapper的selectById()方法开始,到JDBC中SQL执行的整个流程图。

感兴趣的建议对照着这张流程图就行一步一步的debug。

      👍如果对你有帮助,给博主一个免费的点赞以示鼓励
欢迎各位🔎点赞👍评论收藏⭐️

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/753354.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

酷开科技以酷开系统的力量让电视机“活”起来

让用户回归电视的绝不会是因为电视机本身&#xff0c;而是电视系统的内容和交互的形式。酷开科技以系统的力量让电视机“活”起来。对于许多人来说&#xff0c;观看电影是一种享受、一种放松、一种逃避现实的方式。而现在&#xff0c;酷开科技作为行业内领军企业&#xff0c;为…

YOLOV5 部署:QT的可视化界面推理(创建UI,并编译成py文件)

1、前言 之前用YOLOV5 做了一个猫和老鼠的实战检测项目,本章将根据之前训练好的权重进行部署,搭建一个基于QT的可视化推理界面,可以检测图片和视频 本章使用的数据集和权重参照:YOLOV5 初体验:简单猫和老鼠数据集模型训练-CSDN博客 可视化界面如下: 2、安装Pyside6 本…

JSONP漏洞详解

目录 同源策略 JSONP简介 JSONP劫持漏洞 漏洞原理 漏洞利用过程 利用工具 JSONP漏洞挖掘思路 JSONP防御 首先&#xff0c;要了解一下什么是同源策略&#xff1f; 同源策略 同源策略&#xff08;SOP&#xff09;是浏览器的一个安全基石&#xff0c;浏览器为了保证数据…

碳课堂|什么是碳减排?如何减少碳排放?

一、碳减排的定义及提出背景&#xff1a; 碳减排&#xff0c;即减少人类在生产、生活中二氧化碳&#xff08;CO2&#xff09;等温室气体的排放量&#xff0c;以应对全球气候变暖。 18世纪工业革命起&#xff0c;人类在生产活动中使用大量矿物燃料&#xff08;如煤、石油等&am…

从电子邮件到即时通讯:信息技术演变与现代沟通方式的变迁

上世纪90年代&#xff0c;欧美企业界迎来了信息化建设的大爆发时期。Oracle、SAP和Adobe等国际知名软件巨头纷纷致力于推广和普及电子邮件系统&#xff0c;使电子邮件迅速成为企业和个人进行信息交换的主要工具之一。电子邮件的出现&#xff0c;也提供了一种高效的信息传递手段…

zookeeper集群安装部署和集群异常处理

准备jdk和zookeeper安装包【官网即可下载】 zookeeper-3.5.1-alpha.tar.gz jdk1.7.0_8020200612.tar 准备三台linux虚拟机【具体以项目实际需要为准】&#xff0c;并安装jdk和zookeeper 虚拟机地址如下&#xff1a;194.1.1.86&#xff08;server.1&#xff09;、194.1.1.74…

阿里云-云服务器ECS新手如何建网站?

租阿里云服务器一年要多少钱&#xff1f; 不同类型的服务器有不同的价格。 以ECS计算型c5为例&#xff1a;2核4G-1年518.40元&#xff0c;4核8G-1年948.00元。 阿里云ECS云服务器租赁价格由三部分组成&#xff1a; 也就是说&#xff0c;云服务器配置成本磁盘价格网络宽带价格…

本地部署Grok需要的条件

2024年3月18日凌晨三点钟&#xff0c;马斯克开源了Grok&#xff0c;兑现了他承诺&#xff0c;Grok 的开源将为人工智能技术的发展和应用带来新的机遇&#xff0c;那我们如何运行、部署Grok呢。 本地部署Grok通常需要以下条件&#xff1a; 硬件资源&#xff1a; 大量的计算资源&…

【JAVA笔记】IDEA配置本地Maven

文章目录 1 配置本地Maven1.1 Maven下载1.2 Maven安装与配置1.2.1 安装1.2.2 配置1.2.2.1 环境配置1.2.2.2 本地仓库配置 2 IDEA设置本地Maven 1 配置本地Maven 1.1 Maven下载 官网&#xff1a;http://maven.apache.org/下载地址&#xff1a;http://maven.apache.org/downloa…

浏览器如何查看http请求的报文?

HTTP协议用于从WWW服务器传输超文本到本地浏览器的传送协议。 它可以使浏览器更加高效&#xff0c;使网络传输减少。 它不仅保证计算机正确快速地传输超文本文档&#xff0c;还确定传输文档中的哪一部分&#xff0c;以及哪部分内容首先显示 (如文本先于图形)等。所以在node.js里…

Vertex cover preprocessing for influence maximization algorithms

Abstract 影响力最大化问题是社交网络分析中的一个基本问题&#xff0c;其目的是选择一小组节点作为种子集&#xff0c;并在特定的传播模型下最大化通过种子集传播的影响力。本文研究了独立级联模型下影响力最大化算法中执行顶点覆盖作为预处理的效果。所提出的方法从主要计算过…

结构体成员访问操作符

1.结构体成员的直接访问&#xff1a; 结构体变量.成员名&#xff1a; 2.结构体成员的间接访问: 间接访问应用于指向结构体变量的指针&#xff1a;如下

代码随想录算法训练营第九天|28. 实现 strStr()、459.重复的子字符串、字符串总结、双指针回顾

题目&#xff1a;28. 实现 strStr() 文章链接&#xff1a;代码随想录 视频链接&#xff1a;LeetCode:实现strStr 题目链接&#xff1a;力扣题目链接 详解&#xff1a;KMP&#xff1a;主要应用于字符串匹配&#xff0c;当出现字符串不匹配是&#xff0c;可以知道一部分之前已…

YOLOv8独家改进:block改进 | RepViTBlock和C2f进行结合实现二次创新 | CVPR2024清华RepViT

💡💡💡本文独家改进:CVPR2024 清华提出RepViT:轻量级新主干!从ViT角度重新审视移动CNN,RepViTBlock和C2f进行结合实现二次创新 改进结构图如下: 收录 YOLOv8原创自研 https://blog.csdn.net/m0_63774211/category_12511737.html?spm=1001.2014.3001.5482 💡…

数字化时代的风向标:解密Facebook的成功秘诀

在当今数字化时代&#xff0c;社交媒体已经成为人们日常生活中不可或缺的一部分&#xff0c;而Facebook作为全球最大的社交媒体平台之一&#xff0c;其成功之处不言而喻。本文将深入探讨Facebook的成功秘诀&#xff0c;解密其在数字化时代的风向标。 用户体验至上&#xff1a; …

深入探讨ChatGPT:技术突破与应用前景

目录 一、ChatGPT究竟是什么&#xff1f; 二、ChatGPT的发展脉络 三、ChatGPT的突出优势 强大的语言生成能力 多场景适应性 多语言处理能力 广泛的应用范围 数据敏感性的重视 四、结语&#xff1a;ChatGPT的未来与挑战 Tips&#xff1a;国内的ChatGPT ⭐ 点击进入Chat…

什么是React属性钻取(Prop Drilling)

一、介绍 在React开发过程中&#xff0c;状态管理是一个绕不开的话题。无论是新手还是有经验的开发者&#xff0c;都会面临如何有效管理组件状态的挑战。React为我们提供了多种状态管理方案&#xff0c;如直接的状态传递&#xff08;俗称"属性钻取"&#xff09;、Co…

那些年我们在数模竞赛中踩过的坑(附避坑指南)

数乐君猜测关注我们的同学大多都已经参加过数模竞赛了&#xff0c;可能有的同学对于数模竞赛的经验已经很丰富&#xff0c;或者即将要参加第一次数模竞赛&#xff0c;但要说起曾经在数模竞赛中踩的那些坑&#xff0c;也是有苦难言&#xff0c;那或许大家会好奇&#xff0c;数模…

【神经网络 基础知识整理】前向传播 反向传播

神经网络 基础知识整理 前向传播前向传播pytorch中的forward函数 反向传播 顾名思义&#xff0c;前向传播是从前往后传播的&#xff0c;从input层到output层的方向&#xff1a;前面一层的输出传入后一层作为输入&#xff0c;再对数据进行相应的处理&#xff0c;继续传下去&…

力扣Lc17--- 345.反转字符串中的元音字母(java版)-2024年3月18日

1.题目 2.知识点 注1&#xff1a; indexOf() 是 Java 中 String 类的方法之一&#xff0c;它用于查找指定字符或字符串在字符串中第一次出现的位置。如果找到了该字符或字符串&#xff0c;则返回它在字符串中的索引位置&#xff08;从0开始&#xff09;&#xff0c;如果没有找…