Mybatis源碼分析

来源:https://www.cnblogs.com/yuanbeier/archive/2022/06/12/16368245.html
-Advertisement-
Play Games

一、Mybatis的使用 創建maven工程。 添加maven依賴 <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.7</version> </dependency> ...


一、Mybatis的使用

  1. 創建maven工程。

  2. 添加maven依賴

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.7</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.20</version>
</dependency>
  1. 添加配置文件mybatis.xml,內容如下:
<!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.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/aoptest"/>
                <property name="username" value="xxx"/>
                <property name="password" value="xxx"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <!-- 把上面的Mapper.xml 註冊進來,路徑寫在resources目錄下的路徑-->
        <mapper resource="com/ybe/mapper/BookMapper.xml"/>
    </mappers>
</configuration>

  1. 添加實體類,代碼如下:
package com.ybe.entity;
public class Book {
    int id;
    double price;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }
}
  1. 添加BookMapper介面,代碼如下:
package com.ybe.mapper;

import com.ybe.entity.Book;

public interface BookMapper {
     Book getBook();
}
  1. 添加BookMapper.xml配置文件,內容如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.ybe.mapper.BookMapper">
    <select id="getBook" resultType="com.ybe.entity.Book">
        select * from book where id = 1
    </select>
</mapper>
  1. 替換pom文件的 build節點,把resources路徑下的xml文件包括在打包目錄中,內容如下:
 <build>
    <resources>
      <resource>
        <directory>src/main/resources</directory>
        <includes>
          <include>**/*.properties</include>
          <include>**/*.xml</include>
        </includes>
        <filtering>true</filtering>
      </resource>
    </resources>
 </build>
  1. App主類添加代碼,使用mybaits:
//載入mybatis的配置文件
InputStream input = Book.class.getClassLoader().getResourceAsStream("mybatis.xml");
// 用建造者模式,創造 生產SqlSession的工廠(這個工廠的類型由配置文件決定)
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(input);
// 工廠生產Sqlsession
SqlSession sqlSession = factory.openSession();
Book book = sqlSession.selectOne("getBook");
System.out.println(book);
//關閉IO資源(工廠對象會自動回收)
input.close();
sqlSession.close();

二、Mybatis的初始化

  1. Mybatis的初始化就是創建一個SqlSessionFactory實例對象。

​ 步驟一、先根據配置文件創建資源流,

​ 步驟二、根據文件流解析生成SqlSessionFactory對象

  1. 時序圖如下:

  2. 初始化代碼如下,

InputStream input = Book.class.getClassLoader().getResourceAsStream("mybatis.xml");
// 用建造者模式,創造 生產SqlSession的工廠(這個工廠的類型由配置文件決定)
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(input);
  1. SqlSessionFactoryBuilder().build(input)方法的代碼如下,
// 創建XMLConfigBuilder對象,該對象解析配置文件,並給configuration對象賦值。
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// parser.parse()進行具體的解析,返回configuration實例
// build構建 SqlSessionFactory對象
return build(parser.parse());
} catch (Exception e) {
    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
    ErrorContext.instance().reset();
    try {
        inputStream.close();
    } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
    }
}
  1. 其中邏輯主要是創建了一個XMLConfigBuilder實例,併進行了parse()調用,該方法返回的是Configuration實例,Configuration保存了主配置文件的所有信息,比如,資料庫事務工廠、數據源對象、類型別名註冊器、類型處理註冊器等。Configuration實例用於對象查詢中整個過程,非常重要。

  2. parser.parse()方法進行具體解析,parseConfiguration()核心代碼如下:

// issue #117 read properties first
// 解析 properties 內容
propertiesElement(root.evalNode("properties"));
// 解析 settings 內容
Properties settings = settingsAsProperties(root.evalNode("settings"));
//添加vfs的自定義實現,這個功能不怎麼用
loadCustomVfs(settings);
loadCustomLogImpl(settings);
//配置類的別名,配置後就可以用別名來替代全限定名
//mybatis預設設置了很多別名,參考附錄部分
typeAliasesElement(root.evalNode("typeAliases"));
//解析攔截器和攔截器的屬性,set到 Configration的interceptorChain中
//MyBatis 允許你在已映射語句執行過程中的某一點進行攔截調用。預設情況下,MyBatis 允許使用插件來攔截的方法調用
//包括:
//Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
//ParameterHandler (getParameterObject, setParameters)
//ResultSetHandler (handleResultSets, handleOutputParameters)
//StatementHandler (prepare, parameterize, batch, update, query)
pluginElement(root.evalNode("plugins"));
//Mybatis創建對象是會使用objectFactory來創建對象,一般情況下不會自己配置這個objectFactory,
// 使用系統預設的objectFactory就好了
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
//設置在setting標簽中配置的配置
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
//解析環境信息,包括事物管理器和數據源,SqlSessionFactoryBuilder在解析時需要指定環境id
// ,如果不指定的話,會選擇預設的環境;
//最後將這些信息set到 Configration的 Environment屬性裡面
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//無論是 MyBatis 在預處理語句(PreparedStatement)中設置一個參數時,還是從結果集中取出一個值時,
// 都會用類型處理器將獲取的值以合適的方式轉換成 Java 類型。解析typeHandler。
typeHandlerElement(root.evalNode("typeHandlers"));
//解析mapper文件
mapperElement(root.evalNode("mappers"));
  1. build(Configuration config)返回DefaultSqlSessionFactory對象,代碼如下,
public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}
  1. Configuration類主要屬性說明:

​ variables:用來存放 properties 節點中解析出來的 Properties 數據。

​ typeAliasRegistry:用來存放 typeAliases 節點中解析出來的數據。

​ interceptorChain: 用來存放 plugins 節點解析出來的攔截器鏈。

​ environment: 用來存放 environments 節點解析出來的數據,比如資料庫事務管理器和數據源。

​ typeHandlerRegistry:用來存放 typeHandlers 節點解析出來的數據。

​ mapperRegistry:用來註冊Mapper介面 。

​ mappedStatements:用來存儲 MappedStatement 對象,MappedStatement用來表示XXXMapper.XML文件中具體的 select|insert|update|delete節點數據 。

三、配置文件解析

​ Mybaits配置文件解析讀取的過程是通過創建不同的XML構建器來完成的,把解析出來的數據賦值給Configuration實例的屬性。

3.1 XML構造解析類

​ Mybatis主要構造解析類有XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、MapperBuilderAssistant。他們有一個共同的基類BaseBuilder。

​ **BaseBuilder **類中有3個欄位,用來存儲別名註冊器、類型處理器註冊器、配置類。其中類型別名註冊器和類型處理註冊器是從configuration對象中獲取的。BaseBuilder提供了根據別名獲取具體的對象實例的方法以及根據java類型獲取類型處理器對象的方法等。

XMLConfigBuilder 主要用來構建解析主配置文件,構造方法中會創建XPathParser類,通過XPathParser 來解析和讀取XML文件,XMLConfigBuilder的構造方法如下:

private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    super(new Configuration());
    ErrorContext.instance().resource("SQL Mapper Configuration");
    this.configuration.setVariables(props);
    this.parsed = false;
    this.environment = environment;
    this.parser = parser;
}

在構造方法中會創建了一個 Configuration的實例。在Configuration的構造方法中會進行一些別名的註冊和屬性的初始化,部分代碼如下:

protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
protected final InterceptorChain interceptorChain = new InterceptorChain();
protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(this);
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();

protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
    .conflictMessageProducer((savedValue, targetValue) ->
                             ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");

protected final Set<String> loadedResources = new HashSet<>();
protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");

public Configuration() {
    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
    typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
    typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
    typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
    typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
    typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
    typeAliasRegistry.registerAlias("LRU", LruCache.class);
    typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
    typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
    typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
    typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
    languageRegistry.register(RawLanguageDriver.class);
  }

其中的 TypeAliasRegistry 類的構造方法中也進行了一些數據類型別名的註冊,部分代碼如下:

registerAlias("string", String.class);
registerAlias("byte", Byte.class);
registerAlias("long", Long.class);
registerAlias("short", Short.class);
registerAlias("int", Integer.class);
registerAlias("integer", Integer.class);
registerAlias("double", Double.class);
registerAlias("float", Float.class);
registerAlias("boolean", Boolean.class);
registerAlias("byte[]", Byte[].class);
registerAlias("long[]", Long[].class);
registerAlias("short[]", Short[].class);
registerAlias("int[]", Integer[].class);
registerAlias("integer[]", Integer[].class);
registerAlias("double[]", Double[].class);
registerAlias("float[]", Float[].class);
registerAlias("boolean[]", Boolean[].class);

XMLMapperBuilder: 主要用來解析 Mapper.XML文件的。

XMLStatementBuilder :主要用來解析 Mapper.xml 文件中 select|insert|update|delete 等語句的。

MapperBuilderAssistant :Mapper解析過程中的助手類,可以用來創建Mapper的二級緩存,添加MappedStatement等。

MappedStatement:用來存放解析 Mapper.xml 文件中的 select|insert|update|delete 節點數據。

3.2 解析過程

3.2.1environments節點解析

過程比較簡單,根據environments的預設值創建environments的子節點,其中主要是創建資料庫事務工廠和數據源對象,並構建Environment類,賦值給configuration實例,代碼如下,

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
        if (environment == null) {
            // 解析 default 屬性值
            environment = context.getStringAttribute("default");
        }
        // 獲取子節點
        for (XNode child : context.getChildren()) {
            // 獲取 id 屬性值
            String id = child.getStringAttribute("id");
            // 判斷 子節點的id 是否 等於 default 值
            if (isSpecifiedEnvironment(id)) {
                // 獲取事務工廠
                TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
                // 獲取數據源工廠
                DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
                // 獲取數據源
                DataSource dataSource = dsFactory.getDataSource();
                // 構建 Environment 實例,賦值給configuration
                Environment.Builder environmentBuilder = new Environment.Builder(id)
                    .transactionFactory(txFactory)
                    .dataSource(dataSource);
                configuration.setEnvironment(environmentBuilder.build());
                break;
            }
        }
    }
}
3.2.2mappers節點解析

整個解析過程中比較複雜,主要邏輯是要解析具體的mapper文件或者mapper介面。關鍵業務實現在XMLConfigBuilder.mapperElement()方法中。根據mappers的子節點的name值和屬性來執行不同的方法。

一、如果為mappers子節點是以 package 開頭則調用

configuration.addMappers(mapperPackage);

public void addMappers(String packageName) {
    mapperRegistry.addMappers(packageName);
}

MapperRegistry類中方法如下:

public void addMappers(String packageName) {
    addMappers(packageName, Object.class);
}

public void addMappers(String packageName, Class<?> superType) {
    //創建解析類
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    // 找到 package 路徑下所有繼承 superType的類, 並且放入 matches 屬性當中
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    // 獲取所有 matches 的值
    Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
    for (Class<?> mapperClass : mapperSet) {
        // 添加 具體的 映射類
        addMapper(mapperClass);
    }
}

public <T> void addMapper(Class<T> type) {
    // 必須是介面類型才能添加成功
    if (type.isInterface()) {
        // 如果該類型以及添加,則拋異常
        if (hasMapper(type)) {
            throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
        }
        boolean loadCompleted = false;
        try {
            // 添加type 類型的代理工程對象 到  knownMappers 對象中。
            knownMappers.put(type, new MapperProxyFactory<>(type));
            // It's important that the type is added before the parser is run
            // otherwise the binding may automatically be attempted by the
            // mapper parser. If the type is already known, it won't try.
            //創建 mapper 註解解析類
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
            parser.parse();
            loadCompleted = true;
        } finally {
            if (!loadCompleted) {
                knownMappers.remove(type);
            }
        }
    }
}

二、如果為mappers子節點是以 mapper 開頭並且屬性為 class 則調用

configuration.addMapper(mapperInterface);

​ 1. 這上面兩種方式,都會調用類MapperAnnotationBuilder的parse()方法進行Mapper文件或者Mapper介面的解析,代碼如下,

public void parse() {
    String resource = type.toString();
    // 判斷資源是否已經添加過
    if (!configuration.isResourceLoaded(resource)) {
      // 載入 mapperxml 文件
      loadXmlResource();
      //添加 已經載入的資源
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      // 解析二級緩存
      parseCache();
      // 解析緩存引用
      parseCacheRef();
      // 遍歷mapper 介面的 方法
      for (Method method : type.getMethods()) {
        if (!canHaveStatement(method)) {
          continue;
        }
        // 解析 ResultMap
        if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
            && method.getAnnotation(ResultMap.class) == null) {
          parseResultMap(method);
        }
        try {
          // 解析具體的sql語句
          parseStatement(method);
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    parsePendingMethods();
}
  1. loadXmlResource()方法是進行Mapper.Xml文件解析,parseStatement()方法則是進行Mapper介面的解析。這裡主要講解mapper.xml文件解析,loadXmlResource中主要邏輯為:找到資源文件流,創建XMLMapperBuilder實例,調用其parse()方法進行MapperXML文件的解析,主要代碼如下,
// 通過文件流創建 XMLMapper 解析對象,
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
// 進行具體解析
xmlParser.parse();
  1. XMLMapperBuilder.parse()解析Mapper文件的mapper節點,代碼如下:
// 判斷 資源 是否載入過
if (!configuration.isResourceLoaded(resource)) {
    // 解析 具體的 mapper.xml 文件
    configurationElement(parser.evalNode("/mapper"));
    // 設置為已經載入過的 資源
    configuration.addLoadedResource(resource);
    // 綁定該資源的 Mapper介面到 configuration 中
    bindMapperForNamespace();
}

parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
  1. configurationElement()為具體解析 mapper節點的方法,其中會解析 mapper中的namespace、cache、parameterMap、resultMap、sql、select|insert|update|delete,代碼如下:
private void configurationElement(XNode context) {
    try {
        // 獲取節點的 namespace 值
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.isEmpty()) {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
        // 設置助手類的 CurrentNamespace,即 mapper.xml 文件中的 namespace 值
        builderAssistant.setCurrentNamespace(namespace);
        cacheRefElement(context.evalNode("cache-ref"));
        cacheElement(context.evalNode("cache"));
        // 解析 mapper 的 parameterMap
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        // 解析 mapper 的 resultMap
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        // 解析 sql 片段
        sqlElement(context.evalNodes("/mapper/sql"));
        // 解析 select|insert|update|delete
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
}
  1. buildStatementFromContext()方法用來解析mapper文件中 select|insert|update|delete 節點,代碼如下,
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
        // 創建 XMLStatementBuilder 類
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
        try {
            // 解析 具體的 select|insert|update|delete 節點
            statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
            configuration.addIncompleteStatement(statementParser);
        }
    }
}
  1. XMLStatementBuilder.parseStatementNode()方法為實際解析select|insert|update|delete 節點的方法,主要邏輯為從節點中獲取相關參數構建MapperStatement對象,調用builderAssistant.addMappedStatement方法把MapperStatement添加到configuration.mappedStatements集合中去。部分代碼如下,
// 獲取節點名稱
String nodeName = context.getNode().getNodeName();
// 節點名稱就是數據命令名稱
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
// 判斷是否是 SELECT 命令
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// 設置是否刷新緩存
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
// 設置是否用緩存
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
// 獲取自定義sql腳本語言驅動 預設 為 XMLLanguageDriver
LanguageDriver langDriver = getLanguageDriver(lang);
// 通過 XMLLanguageDriver 來解析我們的sql 腳本對象,解析 SqlNode ,
// 註意,只是解析成一個個的SqlNode,並不會完全解析sql,因為這個
// 時候參數都沒確定,動態sql無法解析
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
// 獲取 StatementType 類型
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
// 構建 MappedStatement 對象,添加到configuration.mappedStatements集合中去
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  1. langDriver.createSqlSource(configuration, context, parameterTypeClass),具體的Sql語句被解析構造為了實現了SqlSource介面的類。這些實現類通過SqlNode節點來記錄具體的Sql語句、參數類型、configuration對象。此過程只是根據配置的Sql語句生成具體的SqlNode對象,以便後面在執行sql語句的時候進行解析。
  2. builderAssistant.addMappedStatement();構建MappedStatement類裡面主要存放了 statementLog日誌對象、,添加到configuration實例的mappedStatements集合中去。集合key值為 mapper文件的 namaspace值 + id 值。

三、如果為mappers子節點是以 mapper 開頭並且屬性為 resouce 或者 url 則調用

XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
#具體解析過程請看上面講解
mapperParser.parse();

四、Mybatis的使用

​ 使用分為兩步,第一步獲取SqlSession對象,第二步調用SqlSession對象具體方法。

4.1 獲取SqlSession對象

​ 通過factory.openSession()獲取DefaultSqlSession實例。主要邏輯為,先從configuration對象中獲取 environment環境變數、Executor執行器對象,然後從環境變數中創建事務對象,最後構建DefaultSqlSession對象實例。代碼如下,

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
        // 獲取 環境對象
        final Environment environment = configuration.getEnvironment();
        // 獲取事務工廠
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        // 創建事務
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        // 獲取 executor
        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();
    }
}

​ configuration.newExecutor(tx, execType),根據事務對象和執行器類型創建執行器,執行器有三種類型SIMPLE(簡單), REUSE(可復用), BATCH(批量)。預設為SIMPLE。如果開啟cacheEnabled(二級緩存),則會創建CachingExecutor對象實例包裝SimpleExecutor實例。cacheEnabled預設是true。然後判斷是否有攔截器進行代理,如果有會創建CachingExecutor實例的代理類。代碼如下,

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;
}

4.2 根據id調用SqlSession具體的方法

執行sqlSession.selectOne("getBook")語句來獲取 Book對象實例。selectOne其實內部調用的是SelectList。主要邏輯:先通過statment的id獲取configuration中的MappedStatement實例,再調用執行器的query方法進行查詢,並返回。代碼如下,

private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
    try {
        // 根據 id 獲取 MappedStatement類
        MappedStatement ms = configuration.getMappedStatement(statement);
        // 調用執行器執行查詢語句,並返回對象實例
        return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

executor.query 是執行的 CachingExecutor的query方法。主要邏輯:先通過 調用 實例獲取ms.getBoundSql()方法獲取 BondSql實例,BondSql中有具體的sql語句、傳入的參數對象。

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 解析獲取 SqlSource 實現類,獲取BoundSql, BoundSql裡面存儲了 解析之後的sql語句 ,參數對象
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // 創建緩存key (命名空間id  + sql語句 + 參數值 + 環境變數id)
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    // 進行查詢
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

createCacheKey(ms, parameterObject, rowBounds, boundSql),創建緩存的key,key的規則為(命名空間id + sql語句 + 參數值 + 環境變數id)。query()方法主體邏輯為:先獲取MappedStatement實例中的緩存,如果緩存存則獲取key的對象,代碼如下,

@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);
        // 如果是查詢,並且 resultHandler 為null
        if (ms.isUseCache() && resultHandler == null) {
            ensureNoOutParams(ms, boundSql);
            @SuppressWarnings("unchecked")
            // 從 TransactionalCacheManager 中獲取 key的 緩存對象
            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;
        }
    }
    // 調用代理執行器(預設為 SimpleExecutor)
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql),調用代理執行器(預設為 SimpleExecutor)的query方法,具體執行的是BaseExecutor.query方法,此方法的主要邏輯為:先從本地緩存中獲取key的對象,如果緩存存在即返回該對象,如果緩存不存在則調用queryFromDatabase()方法走資料庫查詢。代碼如下,

queryStack++;
// 從一級緩存中拿數據
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
    handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
    // 如果沒有則走資料庫查詢
    list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

queryFromDatabase()的關鍵邏輯為,先執行sql語句拿到具體的對象實例,再把返回結果存入本地緩存,最終返回執行結果。代碼如下,

// 具體的查詢語句
doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
// 把數據存入一級緩存
localCache.putObject(key, list);
return list;

doQuery()方法中執行資料庫sql,並且將資料庫結果集轉成具體的對象實例。主要邏輯為:通過 MappedStatement 獲取configuration對象,然後configuration創建 StatementHandler的實例,預設值為PreparedStatementHandler類型的實例。再通過prepareStatement方法對Statement對象進行初始化 。最後通過調用StatementHandler的query方法,返回對象實例。代碼如下,

// 獲取 configuration 實例
Configuration configuration = ms.getConfiguration();
// 創建 StatementHandler 實例
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 初始化 Statement 對象
stmt = prepareStatement(handler, ms.getStatementLog());
// 執行 Statement,並且處理結果集
return handler.query(stmt, resultHandler);

handler.query(stmt, resultHandler),會調用PreparedStatementHandler的query方法, 主體邏輯執行statement.execute拿到結果集,再通過結果處理器將資料庫結果集轉成對象實例,最終返回。

// 轉成 PreparedStatement
PreparedStatement ps = (PreparedStatement) statement;
// 執行sql,拿到結果
ps.execute();
// 結果處理器處理資料庫結果集
// 最終會返回實體對象
return resultSetHandler.handleResultSets(ps);

resultSetHandler.handleResultSets(ps)方法主要是轉換查詢出的資料庫結果集為配置的對象實例,最終返回對象實例。代碼如下,

@Override
  public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

    final List<Object> multipleResults = new ArrayList<>();

    int resultSetCount = 0;
    // 獲取 資料庫結果集的包裝類 ResultSetWrapper
    ResultSetWrapper rsw = getFirstResultSet(stmt);
    // 獲取返回結果Map對象
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    // 判斷 rsw 結果集不為空 ,並且 mappedStatement的 resultMapCount 數量 小於 1
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) {
      // 獲取結果 Map 對象
      ResultMap resultMap = resultMaps.get(resultSetCount);
      // 處理結果集,集體返回結果存在 multipleResults 中
      handleResultSet(rsw, resultMap, multipleResults, null);
      // 獲取下一個結果集
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }
    //獲取返回結果Set對象
    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
      while (rsw != null && resultSetCount < resultSets.length) {
        ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
        if (parentMapping != null) {
          String nestedResultMapId = parentMapping.getNestedResultMapId();
          ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
          handleResultSet(rsw, resultMap, null, parentMapping);
        }
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }
    }
    // 返回結果
    return collapseSingleResultList(multipleResults);
  }

handleResultSet(rsw, resultMap, multipleResults, null)方法,封裝了處理資料庫結果集的具體邏輯,源碼裡面邏輯比較複雜,大概邏輯:利用反射創建需要返回的對象實例,再根據資料庫結果集以及相關配置,把資料庫結果集的數據賦值給反射創建對象的屬性。並且把結果添加在multipleResults實例中。整個過程至此完結。

說明:通過SqlSession獲取Mapper介面,再調用Mapper介面的方法執行SQL。其實是先通過JKD生成代理類,底層也是用的根據id調用SqlSession方法的邏輯,和上面講解的一樣。這裡不做講解。

五、緩存

1.一級緩存

結論:一級緩存可以理解為同一個SqlSession的緩存。一級緩存預設開啟。開啟後,在同一個SqlSession中用相同參數值多次調用同一方法,只會查詢一次資料庫,返回對象實例的記憶體地址相同,對象實例屬性也相同。

源碼分析:在BaseExecutor類中localCache屬性表示一級緩存,它類型為PerpetualCache,底層是一個HashMap對象,用來緩存查詢結果對象。緩存的 key 是在createCacheKey()方法中創建,key的規則為(命名空間id + sql語句 + 參數值 + 環境變數id),在BaseExecutor類中query方法裡面有localCache.getObject(key),表示從緩存中獲取對象。queryFromDatabase方法中的localCache.putObject(key, list),表示把查出來的對象實例放進key的緩存中。

2.二級緩存

結論:二級緩存可以理解為Mapper文件的緩存,多個SqlSession之間的緩存。二級緩存預設不開啟。開啟後,在不同的SqlSession中用相同參數值按照同步順序多次調用同一個方法,(每次用完SqlSession需要調用SqlSession的close()關閉SqlSession),只會查詢一次資料庫,返回對象實例的記憶體地址不相同,對象實例屬性相同。開啟二級緩存需要對象支持序列化。

源碼分析:

2.1二級緩存的創建

org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement中調用cacheElement()方法,源碼如下

private void cacheElement(XNode context) {
    // 如果節點不為null,則創建二級緩存
    if (context != null) {
      String type = context.getStringAttribute("type", "PERPETUAL");
      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
      String eviction = context.getStringAttribute("eviction", "LRU");
      Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
      Long flushInterval = context.getLongAttribute("flushInterval");
      Integer size = context.getIntAttribute("size");
      boolean readWrite = !context.getBooleanAttribute("readOnly", false);
      boolean blocking = context.getBooleanAttribute("blocking", false);
      Properties props = context.getChildrenAsProperties();
      // 創建新緩存
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
}

useNewCache()方法是創建緩存的具體方法,其中創建了一個緩存類,並且給currentCache賦值。代碼如下

  public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
    // 
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    //給配置文件添加二級緩存類
    configuration.addCache(cache);
    //給 currentCache 賦值
    currentCache = cache;
    return cache;
  }

在 org.apache.ibatis.builder.MapperBuilderAssistant#addMappedStatement()方法中構建MapperStatement的時候,會把二級緩存對象傳進去,代碼如下:

MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
    .resource(resource)
    .fetchSize(fetchSize)
    .timeout(timeout)
    .statementType(statementType)
    .keyGenerator(keyGenerator)
    .keyProperty(keyProperty)
    .keyColumn(keyColumn)
    .databaseId(databaseId)
    .lang(lang)
    .resultOrdered(resultOrdered)
    .resultSets(resultSets)
    .resultMaps(getStatementResultMaps(resultMap, resultType, id))
    .resultSetType(resultSetType)
    .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
    .useCache(valueOrDefault(useCache, isSelect))
    //賦值二級緩存
    .cache(currentCache);

至此,二級緩存對象被初始化在了MappedStatement 對象中。

2.2二級緩存使用

在 org.apache.ibatis.executor.CachingExecutor#query()方法中會先查詢緩存,如果緩存對象不為空,則判斷是否使用緩存,再從TransactionalCacheManager對象中獲取緩存數據。代碼如下,

// 獲取二級緩存
Cache cache = ms.getCache();
// 如果有二級緩存
if (cache != null) {
    flushCacheIfRequired(ms);
    // 如果是查詢,並且 resultHandler 為null
    if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        // 從 TransactionalCacheManager 中獲取 key的 緩存對象
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
            // 繼續查詢緩存對象
            list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
            // 放入緩存管理對象中,這裡只是放入tcm的 臨時集合對象中,二級緩存具體的更新是在session關閉之後才會提交更新
            tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
    }
}

tcm.putObject(cache, key, list);放入緩存管理對象中,這裡只是放入tcm的 臨時集合對象中,二級緩存具體的更新是在session關閉之後才會提交更新,putObject的代碼如下,

@Override
public void putObject(Object key, Object object) {
    //放入臨時集合中,保存緩存的數據
    entriesToAddOnCommit.put(key, object);
}

session.close()方法代碼會調用executor.close方法進行執行器的關閉,executor.close代碼如下

@Override
public void close(boolean forceRollback) {
    try {
        // issues #499, #524 and #573
        if (forceRollback) {
            tcm.rollback();
        } else {
            tcm.commit();
        }
    } finally {
        delegate.close(forceRollback);
    }
}

tcm.commit()方法中,會調用tcm緩存管理器中所有緩存對象的commit的方法,代碼如下

public void commit() {
    // 遍歷 transactionalCaches 對象的 values 進行提交
    for (TransactionalCache txCache : transactionalCaches.values()) {
        txCache.commit();
    }
}

transactionalCaches的commit的方法代碼如下,

public void commit() {
    if (clearOnCommit) {
        delegate.clear();
    }
    // 刷新 緩存中的待刷新的緩存數據
    flushPendingEntries();
    reset();
}

private void flushPendingEntries() 
    // 提交entriesToAddOnCommit集合的數據到二級緩存代對象
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
        delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
        if (!entriesToAddOnCommit.containsKey(entry)) {
            delegate.putObject(entry, null);
        }
    }
}

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • theme: mk-cute highlight: arduino-light 一、開發背景 產品出設計稿要求做一個仿原生app簡訊驗證碼組件,花了兩小時搞出來一個還可以的組件,支持屏幕自適應,可以用於彈出框,或自己封裝的vue組件里,希望可以幫助那些被產品壓榨的同學,哈哈。😄 其核心思想就是利用 ...
  • ​網址:https://parcel.passerma.com/ GitHub:GitHub - passerma/parcel-doc: 🌎 Parcel 中文文檔 本文檔持續翻譯中,有想幫忙(希望有人)翻譯的小伙伴也可參與哦 使用 Parcel 構建 Web 應用程式 安裝 在開始之前,您需要 ...
  • 本章是系列文章的第六章,介紹了迴圈的分析方法。迴圈優化的邏輯相對簡單,但對性能提升的效果卻非常明顯。迴圈優化的分析還產生了一個圖靈獎。 本文中的所有內容來自學習DCC888的學習筆記或者自己理解的整理,如需轉載請註明出處。周榮華@燧原科技 6.1 迴圈的重要性 90/10定律,90%的算力消耗在10 ...
  • title: 二叉樹的基本知識 date: 2022-06-12 15:37:23 tags: 二叉樹 演算法 待補充 二叉樹的四種遍歷方式 不要較真,其實也可以分為兩種:廣度優先(層級)和深度優先(前序、中序、後序) 基本概念不再贅述。**複雜度:**設二叉樹中元素數目為n。這四種遍歷演算法的空間複雜 ...
  • 目錄 一.簡介 二.效果演示 三.源碼下載 四.猜你喜歡 零基礎 OpenGL (ES) 學習路線推薦 : OpenGL (ES) 學習目錄 >> OpenGL ES 基礎 零基礎 OpenGL (ES) 學習路線推薦 : OpenGL (ES) 學習目錄 >> OpenGL ES 轉場 零基礎 O ...
  • synchronized,synchronized下的 i+=2 和 i++ i++執行結果居然不一樣,位元組碼分析 ...
  • 原型 gtkmm void set_size_request(int width = -1, int height = -1); gtk void gtk_widget_set_size_request ( GtkWidget* widget, int width, int height ) 描述 ...
  • Java-SpringBoot-使用多態給項目解耦 提及 今天在打算維護一下智慧社區這個項目的時候,想到項目是使用Satoken這個開箱即用的授權和認證的組件,因為在項目開啟的時候對SpringSecurity並不熟悉,而Satoken類似傻瓜式的,導入依賴進去,配置一下獲取許可權和角色的方法即可使用 ...
一周排行
    -Advertisement-
    Play Games
  • 經常看到有群友調侃“為什麼搞Java的總在學習JVM調優?那是因為Java爛!我們.NET就不需要搞這些!”真的是這樣嗎?今天我就用一個案例來分析一下。 昨天,一位學生問了我一個問題:他建了一個預設的ASP.NET Core Web API的項目,也就是那個WeatherForecast的預設項目模 ...
  • 很多軟體工程師都認為MD5是一種加密演算法,然而這種觀點是不對的。作為一個 1992 年第一次被公開的演算法,到今天為止已經被髮現了一些致命的漏洞。本文討論MD5在密碼保存方面的一些問題。 ...
  • Maven可以使我們在構建項目時需要用到很多第三方類jar包,如下一些常用jar包 而maven的出現可以讓我們避免手動導入jar包出現的某些問題,它可以自動下載那須所需要的jar包 我們只需要在創建的maven項目自動生成的pom.xml中輸入如下代碼 <dependencies> <!--ser ...
  • 來源:https://developer.aliyun.com/article/694020 非同步調用幾乎是處理高併發Web應用性能問題的萬金油,那麼什麼是“非同步調用”? “非同步調用”對應的是“同步調用”,同步調用指程式按照定義順序依次執行,每一行程式都必須等待上一行程式執行完成之後才能執行;非同步調 ...
  • 1.面向對象 面向對象編程是在面向過程編程的基礎上發展來的,它比面向過程編程具有更強的靈活性和擴展性,所以可以先瞭解下什麼是面向過程編程: 面向過程編程的核心是過程,就是分析出實現需求所需要的步驟,通過函數一步一步實現這些步驟,接著依次調用即可,再簡單理解就是程式 從上到下一步步執行,從頭到尾的解決 ...
  • 10瓶毒藥其中只有一瓶有毒至少需要幾隻老鼠可以找到有毒的那瓶 身似浮雲,心如飛絮,氣若游絲。 用二分查找和二進位位運算的思想都可以把死亡的老鼠降到最低。 其中,二進位位運算就是每一隻老鼠代表一個二進位0或1,0就代表老鼠存活,1代表老鼠死亡;根據數學運算 23 = 8、24 = 16,那麼至少需要四 ...
  • 一、Kafka存在哪些方面的優勢 1. 多生產者 可以無縫地支持多個生產者,不管客戶端在使用單個主題還是多個主題。 2. 多消費者 支持多個消費者從一個單獨的消息流上讀取數據,而且消費者之間互不影響。 3. 基於磁碟的數據存儲 支持消費者非實時地讀取消息,由於消息被提交到磁碟,根據設置的規則進行保存 ...
  • 大家好,我是陶朱公Boy。 前言 上一篇文章《關於狀態機的技術選型,最後一個真心好》我跟大家聊了一下關於”狀態機“的話題。從眾多技術選型中我也推薦了一款阿裡開源的狀態機—“cola-statemachine”。 於是就有小伙伴私信我,自己項目也考慮引入這款狀態機,但網上資料實在太少,能不能系統的介紹 ...
  • 使用腳本自動跑實驗(Ubuntu),將實驗結果記錄在文件中,併在實驗結束之後將結果通過郵件發送到郵箱,最後在windows端自動解析成excel表格。 ...
  • 話說在前面,我不是小黑子~ 我是超級大黑子😏 表弟大周末的跑來我家,沒事幹天天騷擾我,搞得我都不能跟小姐姐好好聊天了,於是為了打發表弟,我決定用Python做一個小游戲來消耗一下他的精力,我思來想去,決定把他變成小黑子,於是做了一個坤坤打籃球的游戲,沒想到他還挺愛玩的~ 終於解放了,於是我把游戲寫 ...