mybatis 执行过程分析

什么是 mybatis

关于 mybatis 定义及使用,官方文档已经说的很清楚了,早年间,Java 是用 JDBC 来访问数据库的 ,但是它有很多的问题,比如不能用数据库连接池,比如每次都要 set/get来读获取数据,本质上,mybatis 也是充当了中间人的角色,用于实现面向对象编程语言里不同类型系统的数据之间的转换。

Mybatis 示例

@Test
public void test() throws IOException {
    
    
    String resource = "mybatis-config.xml";
    InputStream is = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    Account account = sqlSession.selectOne("select * from account");
}

mybatis-config.xml

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC" />
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://192.168.239.128:3306/ssm?characterEncoding=utf-8}"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>
    <!-- 将包内的映射器接口实现全部注册为映射器 -->
    <mappers>
        <package name="com.lhg.smb.mapper"/>
    </mappers>
</configuration>

在刚学习 mybatis 的时候通常会用上面的方式来入门,总结一下 mybatis 运行大概流程:

首先通过 Resource 加载全局配置文件,接着通过实例化 SqlSessionFactoryBuilder构建器帮助创建SqlSessionFactory 接口实现类 DefaultSqlSessionFactory,在此过程中需要先创建 XmlConfigBuilder 解析全局配置文件流,并把解析结果存放到 Configuration, 接着把Configuration作为参数传递给 DefaultSqlSessionFactory,然后用 SqlSessionFactory 工厂创建 SqlSession, 最后调用 SqlSession 的API完成具体的事务操作

源码分析

mybatis 这个框架,从高处上看,主要有三个主线,获取数据源、获取SQL 语句、执行操作。

获取数据源

说白了,获取数据源就是获取数据库的连接信息,什么用户名呀密码呀数据库地址啦等基本信息,而这些信息我们是在 mybatis-config.xml 文件中的 configuration>environments>environment>dataSource 标签下进行了配置,所以获取数据源就是解析 xml ,获取 dataSource 属性的过程

public class SqlSessionFactoryBuilder {
    
        
   public SqlSessionFactory build(InputStream inputStream) {
    
    
       return this.build((InputStream)inputStream, (String)null, (Properties)null);
   }
   
   public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    
    
       SqlSessionFactory var5;
       try {
    
    
           //inputStream 就是读取 mybiats-config.xml的输入流
           XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
           //重点在 parser.parse() 方法,解析
           var5 = this.build(parser.parse());
       } catch (Exception var14) {
    
    
           throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
       } finally {
    
    
           ...
       }
       return var5;
   }
}
public class XMLConfigBuilder extends BaseBuilder {
    
      
	public Configuration parse() {
    
    
        if (this.parsed) {
    
    
            throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        } else {
    
    
            this.parsed = true;
            this.parseConfiguration(this.parser.evalNode("/configuration"));
            return this.configuration;
        }
    }
    /**
       看到了,这个方法就是解析配置文件中各个标签, 什么mappers settings typeAliases environments
    **/
    private void parseConfiguration(XNode root) {
    
    
        try {
    
    
            this.propertiesElement(root.evalNode("properties"));
            Properties settings = this.settingsAsProperties(root.evalNode("settings"));
            this.loadCustomVfs(settings);
            this.typeAliasesElement(root.evalNode("typeAliases"));
            this.pluginElement(root.evalNode("plugins"));
            this.objectFactoryElement(root.evalNode("objectFactory"));
            this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
            this.reflectorFactoryElement(root.evalNode("reflectorFactory"));
            this.settingsElement(settings);
            //和 environments 标签相关
            this.environmentsElement(root.evalNode("environments"));
            this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
            this.typeHandlerElement(root.evalNode("typeHandlers"));
            //和 mapper 解析相关
            this.mapperElement(root.evalNode("mappers"));
        } catch (Exception var3) {
    
    
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
        }
    }
}

因为数据库源的配置在 environments 标签下, 所以重点看 this.environmentsElement(); 方法, 在这个过程就把数据源获取到了,并且设置到了 configuration 中.

public class XMLConfigBuilder extends BaseBuilder {
    
     
    
     protected final Configuration configuration;
    
    //打印 context 就会发现,其实就是 datasource 标签
	private void environmentsElement(XNode context) throws Exception {
    
    
        if (context != null) {
    
    
            if (this.environment == null) {
    
    
                this.environment = context.getStringAttribute("default");
            }
            Iterator var2 = context.getChildren().iterator();
            while(var2.hasNext()) {
    
    
                XNode child = (XNode)var2.next();
                String id = child.getStringAttribute("id");
                if (this.isSpecifiedEnvironment(id)) {
    
    
                    TransactionFactory txFactory = this.transactionManagerElement(child.evalNode("transactionManager"));
                    DataSourceFactory dsFactory = this.dataSourceElement(child.evalNode("dataSource"));
                    DataSource dataSource = dsFactory.getDataSource();
                    Builder environmentBuilder = (new Builder(id)).transactionFactory(txFactory).dataSource(dataSource);
                    //把解析完的 xml 给到 java 对象 configuration
                    this.configuration.setEnvironment(environmentBuilder.build());
                }
            }
        }
    }
    private DataSourceFactory dataSourceElement(XNode context) throws Exception {
    
    
        if (context != null) {
    
    
            String type = context.getStringAttribute("type");
            Properties props = context.getChildrenAsProperties();
            DataSourceFactory factory = (DataSourceFactory)this.resolveClass(type).newInstance();
            factory.setProperties(props);
            return factory;
        } else {
    
    
            throw new BuilderException("Environment declaration requires a DataSourceFactory.");
        }
    }
}

Configuration 对象, 和 mytatis-config.xml 声明的 dtd 标签对应

<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
public class Configuration {
    
    
    protected Environment environment;
   
    protected boolean useGeneratedKeys;
    protected boolean useColumnLabel;
    protected boolean cacheEnabled;
 
    protected Integer defaultFetchSize;
    protected ExecutorType defaultExecutorType;
    ....
}

获取执行 SQL 语句

从配置文件中可以看到, 通过 mappers 映射器告诉我们写的 sql 语句所在的文件, 顺便提一下, mybatis中解析 mappers 映射文件有几种方式? 它们的优先级别?

第一问在 mybatis 文档中有说明: 共有4种

<!-- 使用相对于类路径的资源引用 -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
</mappers>
<!-- 使用完全限定资源定位符(URL) -->
<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
</mappers>
<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
</mappers>
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

第二问就要看源码了, 还是刚才那个方法

扫描二维码关注公众号,回复: 16241145 查看本文章
public class XMLConfigBuilder extends BaseBuilder {
    
      
    /**
       看到了,这个方法就是解析配置文件中各个标签, 什么mappers settings typeAliases environments
    **/
    private void parseConfiguration(XNode root) {
    
    
        try {
    
    
            ...
            this.environmentsElement(root.evalNode("environments"));
            ...
            this.mapperElement(root.evalNode("mappers"));
        } catch (Exception var3) {
    
    
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
        }
    }
    private void mapperElement(XNode parent) throws Exception {
    
    
        if (parent != null) {
    
    
            Iterator var2 = parent.getChildren().iterator();
            while(true) {
    
    
                while(var2.hasNext()) {
    
    
                    XNode child = (XNode)var2.next();
                    String resource;
                    //第一个判断的是 package , 所以它的优先级最高
                    if ("package".equals(child.getName())) {
    
    
                        resource = child.getStringAttribute("name");
                        this.configuration.addMappers(resource);
                    } else {
    
    
                        resource = child.getStringAttribute("resource");
                        String url = child.getStringAttribute("url");
                        String mapperClass = child.getStringAttribute("class");
                        XMLMapperBuilder mapperParser;
                        InputStream inputStream;
                        if (resource != null && url == null && mapperClass == null) {
    
    
                            ErrorContext.instance().resource(resource);
                            inputStream = Resources.getResourceAsStream(resource);
                            mapperParser = new XMLMapperBuilder(inputStream, this.configuration, resource, this.configuration.getSqlFragments());
                            //解析 mapper
                            mapperParser.parse();
                        } else if (resource == null && url != null && mapperClass == null) {
    
    
                            ErrorContext.instance().resource(url);
                            inputStream = Resources.getUrlAsStream(url);
                            mapperParser = new XMLMapperBuilder(inputStream, this.configuration, url, this.configuration.getSqlFragments());
                            mapperParser.parse();
                        } else {
    
    
                            if (resource != null || url != null || mapperClass == null) {
    
    
                                throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                            }
                            Class<?> mapperInterface = Resources.classForName(mapperClass);
                            this.configuration.addMapper(mapperInterface);
                        }
                    }
                }
                return;
            }
        }
    }
}

注意这个类已经变成了 XMLMapperBuilder , 之前是 XMLConfigBuilder

public class XMLMapperBuilder extends BaseBuilder {
    
     
	public void parse() {
    
    
        if (!this.configuration.isResourceLoaded(this.resource)) {
    
    
            //解析 mapper 标签
            this.configurationElement(this.parser.evalNode("/mapper"));
            this.configuration.addLoadedResource(this.resource);
            this.bindMapperForNamespace();
        }

        this.parsePendingResultMaps();
        this.parsePendingCacheRefs();
        this.parsePendingStatements();
    }
    //打印 context 就会发现, 就是我们在 mapper.xml 文件中写的 sql语句
    private void configurationElement(XNode context) {
    
    
        try {
    
    
            //下面这些标签namespace parameterMap resultMap 是不是很熟悉?
            String namespace = context.getStringAttribute("namespace");
            if (namespace != null && !namespace.equals("")) {
    
    
               ....
                this.parameterMapElement(context.evalNodes("/mapper/parameterMap"));
                this.resultMapElements(context.evalNodes("/mapper/resultMap"));
                this.sqlElement(context.evalNodes("/mapper/sql"));
                //看看是什么 sql, 增删查改?
                this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
            } else {
    
    
                throw new BuilderException("Mapper's namespace cannot be empty");
            }
        } 
    }
    private void buildStatementFromContext(List<XNode> list) {
    
    
        if (this.configuration.getDatabaseId() != null) {
    
    
            this.buildStatementFromContext(list, this.configuration.getDatabaseId());
        }
        this.buildStatementFromContext(list, (String)null);
    }

    private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    
    
        Iterator var3 = list.iterator();

        while(var3.hasNext()) {
    
    
            XNode context = (XNode)var3.next();
            XMLStatementBuilder statementParser = new XMLStatementBuilder(this.configuration, this.builderAssistant, context, requiredDatabaseId);

            try {
    
    
                //解析 sql 语句的内容
                statementParser.parseStatementNode();
            } catch (IncompleteElementException var7) {
    
    
                this.configuration.addIncompleteStatement(statementParser);
            }
        }

    }
}

这个类叫 XMLStatementBuilder , 负责 解析 我们在 mapper.xml 里面写的 sql

public class XMLStatementBuilder extends BaseBuilder {
    
    
	public void parseStatementNode() {
    
    
        String id = this.context.getStringAttribute("id");
        String databaseId = this.context.getStringAttribute("databaseId");
        //这些属性parameterMap parameterType resultMap 熟悉吧..
        if (this.databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
    
    
          String parameterMap = this.context.getStringAttribute("parameterMap");
            String parameterType = this.context.getStringAttribute("parameterType");
            Class<?> parameterTypeClass = this.resolveClass(parameterType);
            String resultMap = this.context.getStringAttribute("resultMap");
            String resultType = this.context.getStringAttribute("resultType");
            ....
           Object keyGenerator;
            if (this.configuration.hasKeyGenerator(keyStatementId)) {
    
    
                keyGenerator = this.configuration.getKeyGenerator(keyStatementId);
            } else {
    
    
                keyGenerator = this.context.getBooleanAttribute("useGeneratedKeys", this.configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
            }
			
            this.builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, (KeyGenerator)keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
        }
    }
}
public class MapperBuilderAssistant extends BaseBuilder {
    
    
	public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType, SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<?> parameterType, String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache, boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId, LanguageDriver lang, String resultSets) {
    
    
        if (this.unresolvedCacheRef) {
    
    
            throw new IncompleteElementException("Cache-ref not yet resolved");
        } else {
    
    
            id = this.applyCurrentNamespace(id, false);
            boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
            org.apache.ibatis.mapping.MappedStatement.Builder statementBuilder = (new org.apache.ibatis.mapping.MappedStatement.Builder(this.configuration, id, sqlSource, sqlCommandType)).resource(this.resource).fetchSize(fetchSize).timeout(timeout).statementType(statementType).keyGenerator(keyGenerator).keyProperty(keyProperty).keyColumn(keyColumn).databaseId(databaseId).lang(lang).resultOrdered(resultOrdered).resultSets(resultSets).resultMaps(this.getStatementResultMaps(resultMap, resultType, id)).resultSetType(resultSetType).flushCacheRequired((Boolean)this.valueOrDefault(flushCache, !isSelect)).useCache((Boolean)this.valueOrDefault(useCache, isSelect)).cache(this.currentCache);
            ParameterMap statementParameterMap = this.getStatementParameterMap(parameterMap, parameterType, id);
            if (statementParameterMap != null) {
    
    
                statementBuilder.parameterMap(statementParameterMap);
            }

            MappedStatement statement = statementBuilder.build();
            //把解析好的mapper加入到 configuration 对象中..
            this.configuration.addMappedStatement(statement);
            return statement;
        }
    }
}

执行操作

经过上面两步, 我们数据源拿到了, sql 也拿到了, 接下来就要准备执行了, 我们知道 Mysql 是通过 执行引擎来操作数据库, 早期在通过 JDBC 连接数据库时, 我们的几个固定步骤是加载驱动>获取连接>创建 Statement>返回结果

mybatis 底层也是有 Connection / Statement/ResultSet , 来看看源码

首先查看 SqlSession sqlSession = sqlSessionFactory.openSession();源码,

public class DefaultSqlSessionFactory implements SqlSessionFactory {
    
    

    public SqlSession openSession() {
    
    
        return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);
    }
    private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    
    
        Transaction tx = null;

        DefaultSqlSession var8;
        try {
    
    
            Environment environment = this.configuration.getEnvironment();
            TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
            tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
           //获取执行器
            Executor executor = this.configuration.newExecutor(tx, execType);
            var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
        } catch (Exception var12) {
    
    
            this.closeTransaction(tx);
            throw ExceptionFactory.wrapException("Error opening session.  Cause: " + var12, var12);
        } finally {
    
    
            ErrorContext.instance().reset();
        }
        return var8;
    }
}

这其中有步很重要的操作就是获取执行器, 通过源码发现, mybatis 提供了 三种不同的执行器BatchExecutor、ReuseExecutor、SimpleExecutor, 它们的共同接口时 Executor, 通过 debug 发现, 最终走的是 SimpleExecutor 这个默认执行器,

public class Configuration {
    
    
	public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    
    
        executorType = executorType == null ? this.defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Object 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);
        }

        //查看 cacheEnabled 定义, 发现默认为 true, 所以if 条件执行, 也就是说mybaits 一级缓存默认开启
        if (this.cacheEnabled) {
    
    
            //CachingExecutor是 Mybatis的一级缓存的执行器实现
            executor = new CachingExecutor((Executor)executor);
        }
        Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
        return executor;
    }
}

最后来看看sqlSession.selectOne("select * from account");的底层源码, 层层点击查看, 最终调用如下:

public class DefaultSqlSession implements SqlSession {
    
     
	public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    
    
        List var5;
        try {
    
    
            //这里, 还记得吗,之前是通过this.configuration.addMappedStatement(statement)放入到 configuration 中, 现在是取出来
            MappedStatement ms = this.configuration.getMappedStatement(statement);
            var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
        } ...
        return var5;
    }
}
public class CachingExecutor implements Executor {
    
    
	public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    
    
       //这个 boundSql 就是我们的执行sql 语句 以及 参数
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
        return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
}

所以你还可以看看 缓存 Key 的组成

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    
    
        if (this.closed) {
    
    
            throw new ExecutorException("Executor was closed.");
        } else {
    
    
            CacheKey cacheKey = new CacheKey();
            cacheKey.update(ms.getId());
            cacheKey.update(rowBounds.getOffset());
            cacheKey.update(rowBounds.getLimit());
            cacheKey.update(boundSql.getSql());
            ....
            return cacheKey;
        }
    }

query 的最终调用

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    
    
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
       ....
            List list;
            try {
    
    
                ++this.queryStack;
                list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
                //缓存 key 有值就取 缓存,否则从数据库查询
                if (list != null) {
    
    
                    this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
                } else {
    
    
                    list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
                }
            } 
          ....
            return list;
        }
    }

我们再看看怎么执行数据库查询的, 查看 queryFromDatabase() 方法实现

public abstract class BaseExecutor implements Executor {
    
       
	private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    
    
       ...
        try {
    
    
            list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } 
        ...
        return list;
    }
}
public class SimpleExecutor extends BaseExecutor {
    
        
	public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    
    
        Statement stmt = null;

        List var9;
        try {
    
    
            Configuration configuration = ms.getConfiguration();
            StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
            //查看这个方法实现
            stmt = this.prepareStatement(handler, ms.getStatementLog());
            var9 = handler.query(stmt, resultHandler);
        } ...
        return var9;
    }
    
    private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    
    
       //获取连接
        Connection connection = this.getConnection(statementLog);
        //查看 prepare 方法实现发现创建的是 PrepareStatement
        Statement stmt = handler.prepare(connection, this.transaction.getTimeout());
        handler.parameterize(stmt);
        return stmt;
    }
}

最后通过 resultSetHandler 返回我们要查询的结果

public class PreparedStatementHandler extends BaseStatementHandler {
    
     
	public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    
    
        PreparedStatement ps = (PreparedStatement)statement;
        ps.execute();
        return this.resultSetHandler.handleResultSets(ps);
    }
}

当然闲的蛋疼可以再看看this.resultSetHandler.handleResultSets(ps); 方法实现

public class DefaultResultSetHandler implements ResultSetHandler {
    
    
	public List<Object> handleResultSets(Statement stmt) throws SQLException {
    
    
        ErrorContext.instance().activity("handling results").object(this.mappedStatement.getId());
        List<Object> multipleResults = new ArrayList();
        int resultSetCount = 0;
        //在 getFirstResultSet方法中定义了 ResultSet
        ResultSetWrapper rsw = this.getFirstResultSet(stmt);
      	...
        return this.collapseSingleResultList(multipleResults);
    }
    private ResultSetWrapper getFirstResultSet(Statement stmt) throws SQLException {
    
    
        ResultSet rs = stmt.getResultSet();

        while(rs == null) {
    
    
            if (stmt.getMoreResults()) {
    
    
                rs = stmt.getResultSet();
            } else if (stmt.getUpdateCount() == -1) {
    
    
                break;
            }
        }

        return rs != null ? new ResultSetWrapper(rs, this.configuration) : null;
    }
}

总结

最后一张图总结 mybatis 的执行过程, 很明显, 前半段到 SqlSession 是解析 xml 文件到 Java 的过程; 后半段是 mybatis 真正操作数据库的过程
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/lhg_55/article/details/105436999
今日推荐