Mybatis源碼剖析

文章目錄

  • 一、前置
      • 1.1概念
        • ORM
        • SqlSession會話
  • 二、快速入門
      • 2.1 SpringBoot整合Mybatis
      • 2.2 XML配置
          • 2.2.1 路徑位置
          • 2.2.2 名稱
          • 2.2.3 configuration標簽內容
            • 環境environments標簽
            • 映射器mappers標簽
      • 2.3 Mapper接口
          • 2.3.1 單Mybatis項目
          • 2.3.2 SpringBoot整合mybatis
          • 2.3.3 m整合mybatis
      • 2.4 XML映射文件
          • 2.4.1 單Mybatis
          • 2.4.2 SpringBoot整合mybatis
      • 2.5 執行查詢
  • 三、${}和#{}
      • 3.1 二者本質
      • 3.2 SQL注入
          • 3.2.1 原因
          • 3.2.2 SQL注入案例
      • 3.3 預防
          • 3.3.1 避免動態解析SQL
          • 3.3.2 使用#{}的占位符方式
          • 3.3.3 mybatis中#{}防注入原理
      • 3.4 特殊場景
          • order by
          • in
          • 動態表名
      • 3.5 異常
  • 四、CRUD
      • CRUD
      • 動態Sql
          • 標簽層級
          • if標簽
          • where標簽
          • trim標簽
          • foreach標簽
          • choose+when+otherwise
          • sql標簽
  • 五、源碼
    • 5.1 創建UserPOMapper
      • 5.1.1 Mybatis創建Mapper
      • 5.1.2Spring整合Mybatis創建Mapper
        • 背景
        • @MapperScan-創建bd
            • 等效@MapperScan注解
        • MapperFactoryBean-創建bean
            • 創建MapperFactoryBean
            • 創建資源配置類
            • 創建UserPOMapper
    • 5.2 MapperPrxoy
    • 5.3 方法參數解析
        • ParamNameResolver構造函數
        • convertArgsToSqlCommandParam方法參數解析
    • 5.4 sqlSession.selectOne
      • 5.4.1 SqlSessionTemplate
      • 5.4.2 sqlSessionProxy.selectOne
        • 5.4.2.1 getSqlSession創建SqlSession
          • 線程安全
            • Executor-newExecutor
            • Spring中一級緩存失效
            • Spring事務一級緩存有效
            • 不使用事務一級緩存生效
        • 5.4.2.2 method.invoke
          • RowBounds
          • MappedStatement
          • executor.query
            • 創建BoundSql
            • 創建二級緩存CacheKey
            • 執行查詢query
    • 5.5 Executor.query
      • 原生JDBC
      • 5.5.1 newStatementHandler
        • 創建PreparedStatementHandler
          • 創建parameterHandler
          • 創建resultSetHandler
      • 5.5.2 prepareStatement
        • 創建Connection
        • 創建PrepareStatement
        • parameterize動態SQL參數映射
      • 5.5.3 handler.query
        • ps.execute
        • handleResultSets
          • 獲取ResultSet
          • handleResultSet
            • 1.創建ResultHandler
            • 2.handleRowValues
    • 5.6 MetaObject
      • 5.6.1 PropertyTokenizer分詞器
    • 5.7 緩存
      • 5.6.1 一級緩存
      • 5.6.2 二級緩存
        • 1、范圍
        • 2、開啟二級緩存
        • 3、失效場景
        • 4. 解析@CacheNamespace注解
        • 5. 解析-查詢使用二級緩存
            • flushCacheIfRequired
            • **二級緩存開啟后的第一次查詢**
            • **二級緩存開啟后的第二次查詢**
    • 5.8 懶加載
    • 5.9 Configuration
    • 5.10 插件
      • 5.10.1 四大組件
        • Executor
        • StatementHandler
        • ParamterHandler
        • ResultSetHandler
      • 5.10.2 四大組件代理對象
      • 5.10.3 目標方法
      • 5.10.4 自定義插件
      • 5.10.5 插件源碼分析
      • 5.10.6 分頁插件pageHelper
      • 5.10.7 通用Mapper插件
  • 六、設計模式
    • 6.1 Proxy代理模式
    • 6.2 裝飾者模式
      • Mybatis二級緩存查詢
    • 6.3 構建者模式
    • 6.4 職責鏈模式
    • 6.5 迭代器模式
    • 6.6 簡單工廠模式

一、前置

Mybatis3.5.16中文文檔

1.1概念

ORM

Object-Relation-Map:對象關系映射。即將Java中的DO以及屬性 和 關系型數據庫MySQL中的表以及column映射

SqlSession會話

表示Java程序化和數據庫之間的會話(類似HttpSession表示Java程序和瀏覽器之間的會話)


二、快速入門

2.1 SpringBoot整合Mybatis

1、pom

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.6.RELEASE</version>
</parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>3.0.6</version></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.4</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.6</version></dependency>
</dependencies><build><plugins><plugin><groupId>org.mybatis.generator</groupId><artifactId>mybatis-generator-maven-plugin</artifactId><version>1.3.2</version><configuration><overwrite>true</overwrite><verbose>true</verbose><configurationFile>src/main/resources/mybatis-generatorConfig.xml</configurationFile></configuration></plugin> </plugins>
</build>

2、配置

前提,先看下我們UserPO、UserMapper.xml、UserMapper等路徑,如圖1所示,后續在逆向工程生成時,就按照這個路徑

在這里插入圖片描述

3、mybatis-generatorConfig.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfigurationPUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN""http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration><!-- 數據庫驅動:選擇你的本地硬盤上面的數據庫驅動包--><classPathEntry location="D:/Maven/maven_repository/mysql/mysql-connector-java/5.1.6/mysql-connector-java-5.1.6.jar"/><context id="DB2Tables" targetRuntime="MyBatis3"><commentGenerator><property name="suppressAllComments" value="true"/><property name="suppressDate" value="true"/><property name="addRemarkComments" value="true"/></commentGenerator><!--數據庫鏈接URL,用戶名、密碼 --><jdbcConnection driverClass="com.mysql.jdbc.Driver" userId="你的數據庫賬號!!!" password="你的數據庫密碼!!!"connectionURL="jdbc:mysql://127.0.0.1:3306/day21" ></jdbcConnection><!-- 類型轉換 --><javaTypeResolver ><!-- 是否使用bigDecimal,false: 把JDBC DECIMAL 和 NUMERIC 類型解析為 Integer(默認)true:  把JDBC DECIMAL 和 NUMERIC 類型解析為java.math.BigDecimal--><property name="forceBigDecimals" value="true" /></javaTypeResolver><!-- 生成模型PO的包名和位置--><javaModelGenerator targetPackage="com.mjp.mysql.entity" targetProject="src/main/java"><property name="enableSubPackages" value="true" /><property name="trimStrings" value="true" /></javaModelGenerator><!-- 生成映射文件即Mapper.xml的包名和位置--><sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"><property name="enableSubPackages" value="true"/></sqlMapGenerator><!-- 生成DAO接口即Mapper接口的包名和位置--><javaClientGenerator type="XMLMAPPER" targetPackage="com.mjp.mysql.mapper" targetProject="src/main/java"><property name="enableSubPackages" value="true"/></javaClientGenerator><!-- 要生成的表 tableName是數據庫中的表名或視圖名 domainObjectName是實體類名--><table tableName="tb_user" domainObjectName="UserPO"/></context>
</generatorConfiguration>

注意數據庫賬號和密碼的填寫

4、運行逆向工程

IDEA右側Maven 下 Plugins 下 mybatis-generator下 雙擊運行mybatis-generator: generate

5、applicaiton.properties

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/day21?useUnicode=true&characterEncoding=UTF-8&useSSL=false#day21是你的數據庫的庫名
spring.datasource.username=xxx賬號
spring.datasource.password=xxxx密碼#讀取你mapper包下面的所有XxxMapper.xml文件
mybatis.mapper-locations=classpath*:mapper/*.xml
# 打印你的sql語句執行的情況
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
spring.jackson.time-zone=Asia/Shanghai

6、啟動類

//@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}, scanBasePackages = "com.mjp")@SpringBootApplication(scanBasePackages = "com.mjp")
@MapperScan("com.mjp.mysql.mapper")
public class ApplicationLoader {public static void main(String[] args) {SpringApplication springApplication = new SpringApplication(ApplicationLoader.class);springApplication.run(args);System.out.println("=============啟動成功=============");}
}

7、表

在你的數據庫,庫day21

  • 創建表tb_user下
CREATE TABLE `tb_user` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',`name` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'NAME',`age` int(11) DEFAULT NULL COMMENT '年齡',`ctime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',`utime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',`valid` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效,0:無效,1:有效',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用戶表';
  • 插入一條數據
INSERT INTO `day21`.`tb_user`(`id`, `name`, `age`, `ctime`, `utime`) VALUES (1, 'mjp', 18, '2024-05-13 11:50:32', '2024-05-13 11:50:37')

8、Controller

@RestController
public class UserController {@Resourceprivate UserPOMapper userPOMapper;@GetMapping("/query/{id}")public List<UserPO> query(@PathVariable("id") Long id){UserPOExample example = new UserPOExample();UserPOExample.Criteria criteria = example.createCriteria();criteria.andIdEqualTo(id);List<UserPO> result = userPOMapper.selectByExample(example);return result;}
}

9、測試

  • Run啟動類
  • 瀏覽器輸入:http://localhost:8080/query/1

2.2 XML配置

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configurationPUBLIC "-//mybatis.org//DTD Config 3.0//EN""https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration><environments default="development"><environment id="development"><transactionManager type="JDBC"/><dataSource type="POOLED"><property name="driver" value="${driver}"/><property name="url" value="${url}"/><property name="username" value="${username}"/><property name="password" value="${password}"/></dataSource></environment></environments><mappers><mapper resource="mappers/UserMapper.xml"/><mapper resource="mappers/PersonMapper.xml"/></mappers>
</configuration>
2.2.1 路徑位置
  • 單個Mybatis項目,在src/main/resources文件下
  • SpringBoot整合的項目,在src/main/resources文件下
  • m項目,在和src/main/resources文件同級的profiles/prod或test文件下

2.2.2 名稱
  • 單個Mybatis項目時默認為:mybatis-config.xml
  • SpringBoot整合的項目,名稱一般為:application.properties
  • m項目,名稱一般為:zeb.properties

2.2.3 configuration標簽內容

1)內容

主要是數據庫配置相關的內容

2)說明

configuration標簽下的其它子標簽,要嚴格按照標簽之間的先后順序

環境environments標簽
  • dataSource:使用連接池、指定數據庫連接池的各種信息

    • mybatis-config.xml中
    <transactionManager type="JDBC"></transactionManager>
    <dataSource type="POOLED"><property name="driver" value="com.mysql.jdbc.Driver"/><property name="url" value="同上填寫"/><property name="username" value="同上填寫"/><property name="password" value="同上填寫"/>
    </dataSource>
    

    補充:可以將dataSource中的property屬性,抽出來放到和mybatis-config.xml同級目錄resources下名稱為:jdbc.properties

    jdbc.driver = com.mysql.jdbc.Driver
    jdbc.url = 
    jdbc.username = 
    jdbc.password = 
    

    然后在mybatis-config.xml中引用jdbc.properties內容

    <properties resource="jdbc.properties"></properties>
    <dataSource type="POOLED"><property name="driver" value="${jdbc.driver}"/><property name="url" value="${jdbc.url}"/><property name="username" value="${jdbc.username}"/><property name="password" value="${jdbc.password}"/>
    </dataSource>
    
    • application.properties中
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/day21?useUnicode=true&characterEncoding=UTF-8&useSSL=false
    #day21是你的數據庫的庫名
    spring.datasource.username=賬號
    spring.datasource.password=密碼
    

    springboot會自動加載spring.datasource.*相關配置,數據源就會自動注入到sqlSessionFactory中,sqlSessionFactory會自動注入到Mapper中

    • zeb.properties中
    mdp.zeb[0].jdbcRef=集群名稱_db名稱_test或product環境
    

映射器mappers標簽

作用:服務如何找到XxxMapper.xml文件

  • mybatis-config.xml中

    <mappers><mapper resource="mapper/UserMapper.xml"/><mapper resource="mapper/PersonMapper.xml"/><mapper resource="mapper/MyMapper.xml"/>
    </mappers>
    

    表示上述xml在resources/mapper文件下。

    后續大量的XxxMapper.xml文件,為了防止大量的mapper標簽,mappers標簽中可以通過package標簽實現統一管理。

    • 在resources目錄下創建com.mjp.mapper層級目錄

      正常情況下我們創建的包com.mjp.mapper在本地文件中是一個三級的層級目錄com/mjp/mapper。但是在resources目錄下如果使用com.mjp.mapper創建出來的結果僅是一個文件,文件的名稱就叫"com.mjp.mapper",所以,如果想在resources下創建層級目錄,就必須使用 / 斜杠的方式

    • 在mappers下指定package,這樣package下的所有XxxMapper.xml都會被找到

```
  • application.properties中
#讀取你src/main/resources/mapper包下面的所有XxxMapper.xml文件
mybatis.mapper-locations=classpath*:mapper/*.xml

補充:可以在applicaiton.properties中再引入mybatis-config.xml

mybatis:config-location: classpath:config/mybatis-config.xml
  • zeb.properties中
mdp.zeb[0].mapperLocations=classpath*:mapper/*.xml

2.3 Mapper接口

2.3.1 單Mybatis項目
public interface MyMapper{long count(String name); 
}

2.3.2 SpringBoot整合mybatis

參考:Udf-CRUD,如果不在Mapper接口上加@Mapper注解(每個Mapper接口都要加),就需要使用@MapperScan注解(指定加載所有在com.mjp.mapper文件下的Mapper接口)

@SpringBootApplication
@MapperScan("com.mjp.mapper")
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}
}

2.3.3 m整合mybatis

1)實現如下

zeb.properties文件中指定Mapper接口位置

mdp.zeb[0].basePackage=com.x.infrastructure.mysql.mapper

2)好處

  • Mapper接口上不需要加@Mapper注解
  • 啟動類上也無需要加@MapperScan(“com.xxx.mapper”)注解

2.4 XML映射文件

2.4.1 單Mybatis
<?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="MyMapper的全路徑名稱com.xx.MyMapper"><select id="count" parameterType="java.lang.String" resultType="java.lang.Long">select count(*) from table where name = #{name};</select>

2.4.2 SpringBoot整合mybatis

MyMapper.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="MyMapper的全路徑名稱com.xx.MyMapper"><resultMap id="BaseResultMap" type="MyDO的全路徑名稱com.xxMyDO"><select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
  1. mapper namespace
  • 將MyMapper.xml 和 MyMapper相關聯
  1. resultMap
  • 將MyMapper.xml 和 MyDO相關聯
  1. select
  • id:MyMapper接口中的方法名稱selectByPrimaryKey

  • parameterType:selectByPrimaryKey方法的參數類型

  • resultMap:selectByPrimaryKey返回值內容

  • resultType:方法返回類型(eg:countByExample方法resultType=“java.lang.Long”)

  • resultMap 和 resultType區別

    • select語句,二者必須有一,且只能有一
    • resultType是用來設置默認的映射關系,eg:selectByPrimaryKey根據主鍵id返回一條XxxDO結果,則resultType = ‘全類名.XxxDO’(查詢結果是List時也是這樣),其中XxxDO包含了全部column字段
    • resultMap是用來設置自定義的映射關系(SQL查詢出表結果后,會將表中column 自定義一一映射給 XxxDO類中property,可以自定義DO的property屬性名,去匹配,表column列名)
    <resultMap id="BaseResultMap" type="com.xxx.XxxDO"><id     column="id" jdbcType="BIGINT" property="id" /><result column="ctime" jdbcType="TIMESTAMP" property="ctime" /><result column="valid" jdbcType="BIT" property="valid" />
    </resultMap>
    
    • mybatisGenerator逆向工程自動生成的XxxMapper.xml中select查詢的語句中,均使用的resultMap(除了select count(*)
  1. insert、update
  • useGeneratedKeys 和 keyProperty,參考:useGeneratedKeys
  • 如果是在Mapper接口中也想使用
@Options(useGeneratedKeys = true, keyProperty = "id")
自定義insert方法

2.5 執行查詢

1、單Mybatis項目

@Test
public void test() {InputStream is = Resources.getResourceAsStream("mybatis-config.xml");SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);SqlSession sqlSession = sqlSessionFactory.openSession(true);//true表示事務自動提交MyMapper mapper = sqlSession.getMapper(MyMapper.class);MyDO myDO = mapper.selectByPrimaryKey(1L);
}
  • 讀取XML配置mybatis-config.xml(內含MyMapper.xml)
  • Mybatis幫我們創建MyMapper的實現類,執行Mapper接口的方法
  • 根據MyMapper接口名稱,通過mapper namespace一一映射,找到對應的MyMapper.xml。再根據Mapper接口的執行方法,去MyMapper.xml中找對應的id

2、SpringBoot整合mybatis

@Resource
private MyMapper mymapper;public void func(){MyDO myDO = mapper.selectByPrimaryKey(1L);
}

在MySQL中,Sql表示一條sql語句。在Java代碼中 Sql的本質:MyMappe接口全路徑名稱.selectByPrimaryKey


三、${}和#{}

3.1 二者本質

3.1.1 ${}本質是字符串拼接

// 1.注冊驅動
Class.forName("com.mysql.jdbc.Driver");
// 2.獲取數據庫連接對象
con = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "root");
// 3.拼寫sql
String name = " '攻擊' or 1==1";
String sql = "select * from table_user where name =  "+name+" ";
// 4.獲取sql語句的執行對象
stm = con.prepareStatement(sql);

3.1.2 #{}本質是占位符

String myName = "mjp or 1==1";
String sql = "select * from table_user where name =  ?";
PreparedStatement pst = con.prepareStatement(sql);
pst.setString(1, myName);// 第一個站位符,內容使用myName內容

3.2 SQL注入

3.2.1 原因

${}本質是字符串拼接,字符串拼接就會有SQL注入風險

3.2.2 SQL注入案例
// 1.假如name字段內容是用戶傳遞過來的
String name = " '攻擊' or 1==1";
String sql = "select * from table_user where name =  "+name+" ";
// 最終Sql語句內容為:select * from table_user where name =  '攻擊' or 1==1
// 因為or 1==1 恒成立,name字段的判斷匹配不生效了

3.3 預防

3.3.1 避免動態解析SQL
3.3.2 使用#{}的占位符方式

1、Mapper接口中自定義Sql

SELECT * FROM user WHERE name LIKE "%"#{name}"%"

2、原生JDBC書寫

使用PreparedStatement 結合 #{}
3.3.3 mybatis中#{}防注入原理

1、Mapper接口中自定義Sql

public interface UserMapper {@Select("SELECT * FROM user WHERE id= #{id}")User getById(int id);
}

2、使用 #{} 語法時,MyBatis 會自動生成 PreparedStatement


3.4 特殊場景

order by

1、sql

order by ${name}

2、解決方式

查詢出來的數據展示順序

  • 如果使用到了聯合索引,則按照聯合索引的順序依次展示數據;
  • 如果沒有用到索引,則按照主鍵id自增順序展示數據。

首先,正常情況下沒有使用order by ${name}這種場景,最多使用到order by id字段;

其次,就算要按照某個非主鍵id字段排序,也可以通過Java代碼在內存中進行排序,不使用Sql排序


in

1、背景

現在前端傳遞要刪除的ids,后端使用String ids = "1,2,3"來接收

2、sql

delete from table where id in (${ids});
int deleteByIds(@Param("ids") String ids)

3、解決

后端可以將String ids = "1,2,3"轉換為List ids,然后使用foreach標簽執行刪除

    @Select(("<script>" +"delete from table where id in " +"<foreach collection='skuIdList' index = 'index' item = 'id' open= '(' separator=', ' close=')'>\n" +"#{id}" +"</foreach>" +"</script>"))int deleteByIds(@Param("ids") List<Long> ids)

動態表名

只能是${tableName}


3.5 異常

1、異常描述

@Select("SELECT * FROM user WHERE id= #{id} and name = #{name}")
User getById(int id, String name);

BindingException:Parameter ‘name’ not found. Available parameters are [arg1, arg0, param1, param2]

2、解決

方式一: @Param

@Select("SELECT * FROM user WHERE id= #{id} and name = #{name}")
User getById(@Param("id") int id, @Param("name")  String name);

方式二:

sql中嘗試使用 #{param1} 或 #{param2} 或 #{arg0}  或 #{arg1}@Select("SELECT * FROM user WHERE id= #{id} and name = #{param1}")
User getById(int id, String name);

四、CRUD

CRUD

參考:CRUD


動態Sql

所有手寫的,涉及到動態SQL的,一定需要

標簽層級
<where><foreach collection="oredCriteria" item="criteria" separator="or"><if test="criteria.valid"><trim prefix="(" prefixOverrides="and" suffix=")"><foreach collection="criteria.criteria" item="criterion"><choose><when test="criterion.noValue">and ${criterion.condition}</when><when test="criterion.singleValue">and ${criterion.condition} #{criterion.value}</when><when test="criterion.betweenValue">and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}</when><when test="criterion.listValue">and ${criterion.condition}<foreach close=")" collection="criterion.value" item="listItem" open="(" separator=",">#{listItem}</foreach></when></choose></foreach></trim></if></foreach></where>

where - if -trim - foreach - choose -when|then


if標簽

1、非NULL

    @Select("<script>" +" select * from table where  "+"<if test='id != null'>" +"id = #{id}" +"</if> "+"<if test='status != null'> " +"and status = #{status}" +"</if>"+"</script>")List<FulfillAssessDO> selectByIf(@Param("id") long id, @Param("status") Integer status);

2、非Null非空

"<if test='uName != null and uName.length() &gt; 0       '> " +"and u_name = #{uName}" +
"</if>"+

3、xml中操作符語法

<= 小于等于 :<![CDATA[   <=  ]]>
>= 大于等于:<![CDATA[  >=  ]]>
>=:使用實體引用 &gt;=
<=:使用實體引用 &lt;=特殊字符   替代符號
&          &amp;
<          &lt;
>          &gt;
"          &quot;
'          &apos;

舉例

"<if test='uName != null and uName.length() &gt;= 0 '> " +"and u_name = #{uName}" +
"</if>"+

4、存在問題

"<if test='id != null'>" +"id = #{id}" +
"</if> "+"<if test='status != null'> " +"and status = #{status}" +
"</if>"+

1)問題描述

如果參數中id字段為null,則sql無法命中第一個條件。就有可能生成錯誤的sql

select * from table where and status = ?;

2)解決

使用動態where標簽


where標簽

1、作用

1)動態生成where關鍵字

  • where后有命中的條件,則會生成where關鍵字
  • where后無命中的條件,不會生成where關鍵字,直接是select * from table

2)解決上述if中前面字段為空被省略掉了,where后直接跟and的情況

發生where and status = ?時,會自動省略掉字段前多余的and,修改為where status = ?

2、使用

    @Select("<script>" +" select * from table   "+"<where>" +"<if test='id != null'>" +"id = #{id}" +"</if> "+"<if test='uName != null and uName.length() &gt;= 0 '> " +"and u_name = #{uName}" +"</if>"+"</where>" +"</script>")

3、存在問題

1)問題描述

where標簽只能自動省略掉字段前多余的and,無法省略掉字段后的and

"<if test='id != null'>" +"id = #{id} and" +
"</if> "+"<if test='status != null'> " +"status = #{status}" +
"</if>"+

即如果status字段字段為空,則sql為

select * from table where id = ? and

2)問題解決

使用trim標簽


trim標簽

1、作用

where標簽只能自動省略掉字段前多余的and,無法省略掉字段后的and。trim標簽可以

2、使用

myMapper.selectByTrim(1L,null);@Select("<script>" +" select * from table   "+"<trim prefix = 'where' suffixOverrides = 'and' >" +"<if test='id != null'>" +"id = #{id} and" +"</if> "+"<if test='uName != null and uName.length() &gt;= 0 '> " +" u_name = #{uName}" +"</if>"+"</trim>" +"</script>")
List<FulfillAssessDO> selectByTrim(@Param("id") Long id, @Param("uName") String uName);sql: select * from table where id = ?

3、標簽屬性

1)prefix屬性

向trim標簽中,前添加指定內容

2)suffix屬性

3)prefixOverrides屬性:前綴重寫

將trim標簽中,其前面指定的內容去掉。eg:prefixOverrides="and"即將trim標簽中, 前面指定的and內容去掉

  • 如果想去掉多個內容,可以使用|,比如去掉trim標簽中前面的and 和 or,則prefixOverrides=“and | or”

4)suffixOverrides屬性

4、注意

  • where:如果后續查詢條件均為空,trim標簽即使有prefix = ‘where’,生成的sql也是:select * from table

    所以這里可以得出:trim標簽可以實現where標簽的功能

  • 推薦寫法

<trim prefix = 'where' suffixOverrides = 'and' prefixOverrides = 'and' >

這樣就不用擔心前后and的問題了


foreach標簽

1、使用

@Delete("<script>" +"delete from table "+"where id in  " +"<foreach collection='ids'  item='id' open='(' close=')' separator=',' > "   +"#{id}  " +"</foreach>" +
"</script>")
int deleteByIds(@Param("ids") List<Long> ids);

2、屬性

  • collection: 要遍歷的集合名稱
  • item:集合中每個元素內容:元素是基本類型,則就是值本身,元素是封裝類型就是對象本身。這里我們集合中,每個元素都是主鍵id的值,所以這里我們就將item=‘id’
  • separator:循環體之間的分隔符。即每循環一次,就在循環內容后緊跟一個分隔符(這里是逗號,)
  • open:整個foreach標簽循環開始前,加( 左括號,一般只和in關鍵字一起使用
  • close:整個foreach標簽循環結束后,加)右括號

3、執行過程分析

1)逗號,分割符

deleteByIds(Lists.newAtrrayList(1L, 2L, 3L))
  • 整個循環開始之前的sql:delete from table where id in(
  • 第一次循環結束后的sql: delete from table where id in(1,
  • 第二次循環結束后的sql: delete from table where id in(1,2
  • 第三次循環結束后的sql: delete from table where id in(1,2,3
  • 整個循環結束后的sql:delete from table where id in (1,2,3)

2)or分隔符

  • 訴求:
想實現最終的sql為 : delete from table where id = 1 or id = 2 or id = 3接口方法 : deleteByIds(Lists.newAtrrayList(1L, 2L, 3L))
  • 對應sql
@Delete("<script>" +"delete from table "+" where " +"<foreach collection='ids'  item='id' separator='or' > "   +" id = #{id} " +"</foreach>" +"</script>"
)
int deleteByIds(@Param("ids") List<Long> ids);
  • 整個循環開始之前的sql:delete from table where
  • 第一次循環結束后的sql: delete from table where id = 1 or
  • 第二次循環結束后的sql: delete from table where id = 1 or id = 2 or
  • 第三次循環結束后的sql: delete from table where id = 1 or id = 2 or id = 3
  • 整個循環結束后的sql: delete from table where id = 1 or id = 2 or id = 3

4、批量插入

1)MyMapper接口方法

int batchInsert(@Param("list") List<MyDO> list);

1)MyMapper.xml中的sql

<insert id="batchInsert" parameterType="map">insert into table(id, status)values<foreach collection="list" item="item" separator=",">(#{item.id}, #{item.status})</foreach></insert>

2)分析

MyDO myDO1 = new MyDO(1L, 1000);
MyDO myDO2 = new MyDO(2L, 2000);
batchInsert(Lists.newArrayList(myDO1, myDO2));
  • 循環開始之前的sql:insert into table (id, status) values
  • 第一次循環結束后的sql: insert into table (id, status) values (1, 1000),
  • 第二次循環結束后的sql: insert into table (id, status) values (1, 1000), (2, 2000)
  • 整個循環結束后的sql: insert into table (id, status) values (1, 1000), (2, 2000)

choose+when+otherwise

1、作用

if(){} else if() {} else {}
  • 三者就相當于上述java代碼

  • 必須有第一個if即choose標簽出現之后,才會有后面二者。說明choose是父標簽。choose中沒有邏輯,即第一個if沒有()判斷邏輯

  • Java中else if可以有多個,所以when標簽也可以有多個。

    choose標簽必須和when標簽一起出現!

  • Java中else 只能有一個,所以otherwise標簽也只能有一個

  • 無論有多少個when標簽 和 一個otherwise標簽,只能進入其中之一的邏輯中

2、使用

    @Select("<script>" +" select * from table "+"<where>" +"<choose>"+"<when test = 'id != null'>" +"id = #{id}" +"</when>"+"<when test = 'uName != null'>" +"u_name = #{uName}" +"</when>"+"<otherwise>" +"status = #{status}" +"</otherwise>" +"</choose>"+"</where>" +"</script>")List<FulfillAssessDO> selectByChoose(@Param("id") Long id,@Param("uName") String uName,@Param("status") Integer status);
  • id !=null時,進入第一個when邏輯,sql為:select * from table where id = ?
  • Id == null && uName != null時,進入第二個when邏輯,sql為:select * from table where uName= ?
  • id ==null && uName == null,進入otherwise邏輯sql為:select * from table where status = ?

sql標簽

1、背景

正常情況下我們是select * ,但實際上我們是不允許直接寫select *,還是要指定具體要查詢的字段名稱,比如select id ,name等

如果我們每次select語句都要指定對應的column,會麻煩。所以,我們使用sql標簽,自定義一些引用

2、內容

<sql id="Base_Column_List">id, status
</sql><select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">select <include refid="Base_Column_List" />from tablewhere id = #{id}
</select>
  • 引用sql標簽時,需要使用,其中refid引用內容即sql標簽的id值

  • 這里要和resultMap標簽區別開

<resultMap id="BaseResultMap"   type="com.sankuai.groceryscm.appraisal.infrastructure.mysql.entity.FulfillAssessDO"><id column="id" jdbcType="BIGINT" property="id" /><result column="status" jdbcType="INTEGER" property="status" />
</resultMap><select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">select <include refid="Base_Column_List" />from tablewhere id = #{id}
</select>
  • sql標簽:自定義select * 中*的列,即要查詢的column
  • resultMap標簽:映射表column 和 DO屬性的,即表中column查出來值后,賦值給DO中的哪個字段

五、源碼

5.1 創建UserPOMapper

5.1.1 Mybatis創建Mapper

@Configuration  
public class MyBatisConfig {  @Resource  private DataSource dataSource;//application.properties中配置@Bean  public SqlSessionFactory sqlSessionFactory() throws Exception {  SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();  sessionFactory.setDataSource(dataSource);  sessionFactory.setMapperLocations(...)//指定mapper XML文件的位置  return sessionFactory.getObject();  }  
}
@Resource
private SqlSessionFactory sqlSessionFactory;public void func(){SqlSession sqlSession = sqlSessionFactory.openSession();UserPOMapper mapper = sqlSession.getMapper(UserPOMapper.class);UserPO userPO = mapper.selectByPrimaryKey(1L);
}

步驟一:解析application.properties中mysql配置

步驟二:根據資源配置,創建DataSource

步驟三:根據SqlSessionFactoryBean 結合 其DataSource、mapperLocations等屬性,創建SqlSessionFactory

步驟四:根據SqlSessionFactory創建SqlSession

步驟五:根據SqlSessionFactory創建Mapper接口代理對象


5.1.2Spring整合Mybatis創建Mapper

背景

1、思路

Spring整合Mybatis,就是在Spring中可以使用Mybatis的Mapper對應的代理對象。也就是將Mapper對應的代理對象注入Spring容器

2、類注入Spring容器的方式即IOC

  • @ComponentScan + @Controller|@Service|@Repository|@Component
  • @Configuration + @Bean
  • @Import

但是Mapper是接口,創建其代理對象,交由Spring。顯然上述注解都不可以

3、Spring中五種創建對象方式

方式一:resolveBeforeInstantiation

方式二:doCreateBean中反射方式

方式三:FactoryMethod

方式四:createBeanInstance-Supplier

方式五:FactoryBean:可實現為接口創建代理對象并注入Spring

3.1)暴力版

  • 定義Mapper接口
package com.mjp.mybatis;
@Service
public class MyService {@Resourceprivate MyMapper myMapper;public void func() {myMapper.selectName();}
}package com.mjp.mybatis;
public interface MyMapper {@Select("select '777'")String selectName();
}
  • 自定義FactoryBean
@Component
public class MyFactoryBean<T> implements FactoryBean<T> {@Overridepublic T getObject() {Object o = Proxy.newProxyInstance(MyFactoryBean.class.getClassLoader(), new Class[]{MyMapper.class},(proxy, method, args) -> {System.out.println(method.getName());return null;//返回代理對象。這里直接返回null為代理對象值});return (T) o;}@Overridepublic Class<?> getObjectType() {return MyMapper.class;}
}

MyFactoryBean對象放一級緩存singletonObjects中(key:“myFactoryBean”,val:MyFactoryBean@xxx)

MyMapper接口的代理對象是放factoryBeanObjectCache中(key:“myFactoryBean”,val:$Proxy@xxx),

二者均交由Spring管理

  • 配置類
@Configuration
@ComponentScan("com.mjp.mybatis")
public class MyApplication {
}
  • 測試
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyApplication.class);MyService myService = (MyService) context.getBean("myService");
myService.func();//selectNam:執Mapper代理對象的invoke方法MyFactoryBean myFactoryBean = (MyFactoryBean) context.getBean("&myFactoryBean");
System.out.println(myFactoryBean);Proxy object = myFactoryBean.getObject();// "myFactoryBean" - 代理對象
System.out.println(object); 
  • 存在問題

    MyFactoryBean中,UserPOMapper都是寫死的,有多少個Mapper就要對應寫多少個FactoryBean

  • 解決思路

    Mapper不寫死

  • 實現方式

    可以為MyfactoryBean定義一個構造方法,參數入參為XxxMapper。這樣就可以動態的指定Mapper

3.2)改進版1-構造參數

@Component
public class MyFactoryBean<T> implements FactoryBean<T> {private Class<T> mapperInterface;public MyFactoryBean(Class<T> mapperInterface) {this.mapperInterface = mapperInterface;}@Overridepublic T getObject() {Object o = Proxy.newProxyInstance(MyFactoryBean.class.getClassLoader(), new Class[]{mapperInterface},(proxy, method, args) -> {System.out.println(method.getName());return null;//返回代理對象});return (T) o;}@Overridepublic Class<?> getObjectType() {return mapperInterface;}
}
  • 優點

    這樣就可以通過構造參數的方式,動態的傳遞Mapper接口,完成相應代理對象的創建

  • 存在問題

    Spring中@Component注解,歸歸根結底只能創建一個bean對象。所以,即使我們使用了構造函數傳參的動態方式,但是最終還是只能創建一個Mapper對應的代理對象

  • 解決思路

    不使用@Component注解修飾MyFactoryBean,使用更底層的beanDefinition注冊

3.3)改進版2-不使用@Component

  • MyFactoryBean
public class MyFactoryBean<T> implements FactoryBean<T> {private Class<T> mapperInterface;public MyFactoryBean(Class<T> mapperInterface) {this.mapperInterface = mapperInterface;}@Overridepublic T getObject() {Object o = Proxy.newProxyInstance(MyFactoryBean.class.getClassLoader(), new Class[]{mapperInterface},(proxy, method, args) -> {System.out.println(method.getName());return null;//返回代理對象});return (T) o;}@Overridepublic Class<?> getObjectType() {return mapperInterface;}
}
  • 自定義BeanDefinition
public class Demo {public static void main(String[] args) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();context.register(MyApplication.class);// 1.創建一個bdAbstractBeanDefinition bd = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();// 2.設置beanClassbd.setBeanClass(MyFactoryBean.class);// 3.調用其構造函數,設置屬性bd.getConstructorArgumentValues().addGenericArgumentValue(MyMapper.class);// 4.注冊bd到容器context.registerBeanDefinition("myMapper", bd);context.refresh();MyService myService = (MyService) context.getBean("myService");myService.func();}

此時bdmap:“myMapper” - bd(beanClass = MyFactoryBean)

一級緩存: “myMapper” - bean對象MyFactoryBean@xxx(屬性mapperInterface:interface com.xxx.MyMapper)

factoryBeanObjectCache:“myMapper”-$Proxy@xxx(即MyMapepr接口的代理對象)

這樣從容器中獲取"myMapper"的bean對象時,就會拿到一個MyFactoryBean@xxx對象,然后通過此對象的getObject方法,返回一個Proxy代理對象

  • 優點

    可擴展,無論有多少個Mapper,在不改動MyFactoryBean的情況下,都可以為其創建對應的代理對象

  • 缺點

    每新增一個Mapper,都要為其code 1-4步驟。違背了可擴展,而且代碼邏輯重復

  • 解決思路

    • 將步驟1-4的邏輯抽取為公共邏輯fun
    • 然后掃描包,找到所有的Mapper接口集合
    • 遍歷集合,依次執行fun
    • 這樣就可以為所有的Mapper接口創建代理對象
    • 即使后續新增了Mapper接口,也會被掃描到

3.4)最終版:即Spring整合Mybatis實現方式

  • 掃描所有Mapper接口,執行code:1-4公共邏輯:@MapperScan(“com.mjp.mysql.mapper”)
@SpringBootApplication(scanBasePackages = "com.mjp.mybatis")
@MapperScan("com.mjp.mysql.mapper")
@EnableTransactionManagement
public class ApplicationLoader {public static void main(String[] args) {SpringApplication springApplication = new SpringApplication(ApplicationLoader.class);springApplication.run(args);System.out.println("=============啟動成功=============");}
}
  • FactoryBean
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {private Class<T> mapperInterface;// 1.構造方法public MapperFactoryBean(Class<T> mapperInterface) {this.mapperInterface = mapperInterface;}// 2.返回代理對象public T getObject() throws Exception {// 底層使用Mybatis那套,為Mapper創建代理對象MapperProxyreturn this.getSqlSession().getMapper(this.mapperInterface);}public Class<T> getObjectType() {return this.mapperInterface;}
}

當創建"myMapper"時,會去拿MapperFactoryBean@xxx(等此對象創建完成后),則可以調用getObject返回代理對象

接下來,我們就看下最終版,即Spring中整合Mybatis是怎么實現的


@MapperScan-創建bd

底層就是實現上述最終版1-4的邏輯

@SpringBootApplication(scanBasePackages = "com.mjp.mybatis")
@MapperScan("com.mjp.mysql.mapper")
public class ApplicationLoader {public static void main(String[] args) {SpringApplication springApplication = new SpringApplication(ApplicationLoader.class);springApplication.run(args);System.out.println("=============啟動成功=============");}
}
package com.mjp.mybatis;@Service
public class StudentService {@Resourceprivate UserPOMapper userPOMapper;public List<UserPO> query(Long id){UserPOExample example = new UserPOExample();UserPOExample.Criteria criteria = example.createCriteria();criteria.andIdEqualTo(1L);List<UserPO> result = userPOMapper.selectByExample(example);return result;
}

Spring-IOC,參考我的另外一篇:Spring源碼解析

1、執行priorityOrder-bdrpp:ConfigurationClassPostProcessor

@Import({MapperScannerRegistrar.class})
public @interface MapperScan {
}

1)作用:

  • 將修飾注解@MapperScan的@Import注解中的MapperScannerRegistrar加入容器
  • 使用MapperScannerRegistrar#registerBeanDefinitions,將MapperScannerConfigurer(本身是bdrpp)加入容器

2)流程

  • 解析@Import注解,將MapperScannerRegistrar加入容器

  • 解析完成后,loadBeanDefinitions

processConfigBeanDefinitions -->> this.reader.loadBeanDefinitions -->> loadBeanDefinitionsForConfigurationClass -->> (啟動類ApplicationLoader)loadBeanDefinitionsFromRegistrars -->> MapperScanRegistrar#registerBeanDefinitions

將MapperScannerConfigurer加入容器

public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean {public void afterPropertiesSet() throws Exception {//}public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {// }
}

2、NoOrder-bdrpp:MapperScannerConfigurer

1)作用

  • 將@MapperScan(“com.mjp.mysql.mapper”)指定路徑下的Mapper加入容器
  • 并設置Mapper的beanClass屬性為MapperfactoryBean類型

2)流程

Scan類的繼承關系

ClassPathMapperScanner -->>父類ClassPathBeanDefinitionScanner -->>父類ClassPathScanningCandidateComponentProvider
public int scan(String... basePackages) {// 步驟一:掃描doScan(basePackages);// 步驟二:如果不存在,則注冊internalXxxBpp 和 ConfigurationClassPostProcessorif (this.includeAnnotationConfig) {AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);}
}

步驟一:scan 掃描

–>> scan -->> ClassPathBeanDefinitionScanner#doScan -->>

public Set<BeanDefinitionHolder> doScan(String... basePackages) {// 1.1掃描Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);// 1.2修改bd屬性this.processBeanDefinitions(beanDefinitions);return beanDefinitions;
}
  • 1.1掃描
ClassPathScanningCandidateComponentProvider#findCandidateComponents-->> scanCandidateComponents -->> 

路徑全類名:classpath*:com/mjp/mysql/mapper/**/*.class

找到UserPOMapper.class

// 滿足指定條件 才 bd可以加入容器,子類可以重寫此isCandidateComponent決定是否將掃描出來的bd加入容器
if (isCandidateComponent(sbd)) {candidates.add(sbd);
}

ScannedGenericBeanDefinition,beanClass為:com.mjp.mysql.mapper.UserPOMapper,加入

  • 1.2修改bd屬性processBeanDefinitions:設置為Mapperfactorybean

–>> ClassPathMapperScanner#processBeanDefinitions

// 調用構造函數,為其設置屬性值為mapperInterface類型
definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
//將Mapper的beanClass屬性設置為Mapperfactorybean
definition.setBeanClass(this.mapperFactoryBeanClass);
definition.setAutowireMode(2);//AUTOWIRE_BY_TYPE = 2

等效@MapperScan注解
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();mapperScannerConfigurer.setBasePackage("com.mjp.mybatis");//@MapperScan中的包路徑return mapperScannerConfigurer;
}

通過@MapperScan注解,已經完成所有Mapper接口的掃描。bdMap中key就是xxxMapper(beanClass類型就是MapperFactoryBean)


MapperFactoryBean-創建bean

1、類結構

1)構造方法

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T>{private Class<T> mapperInterface;//@MapperScan路徑下的Mapperpublic MapperFactoryBean(Class<T> mapperInterface) {this.mapperInterface = mapperInterface;}//
}

2)getObject

  • 這個自定義創建邏輯,就可以使用MyBatis的那套創建Mapper代理對象流程
// 自定義Mapper的創建 和 Mybatis中步驟五底層一樣
public T getObject() throws Exception {return this.getSqlSession().getMapper(this.mapperInterface);
}
創建MapperFactoryBean

Mapper接口作為參數,傳入MapperFactoryBean的構造函數流程

執行registerBeanPostProcessors時實例化實現了Order接口的bpp- dataSourceInitializerPostProcessor -->>

->> getBean -->> populateBean -->> AutowiredAnnotationBeanPostProcessor#postProcessProperties

– >> inject -->>resolveDependency -->> doResolveDependency -->> findAutowireCandidates

– >> beanNamesForTypeIncludingAncestors -->>getBeanNamesForType -->>doGetBeanNamesForType

遍歷所有的bd,找到beanClass類型 為 FactoryBean的(以userPOMapper為例)

其beanClass為MapperFactoryBean是FactoryBean類型

– >> isTypeMatch(“userPOMapper”, org.springframework.beans.factory.BeanFactory) -->>

getTypeForFactoryBean(beanName, mbd) -->> getSingletonFactoryBeanForTypeCheck -->> createBeanInstance -->> autowireConstructor :調用有參構造函數,創建beanClass -->> instantiate

public MapperFactoryBean(Class<T> mapperInterface) {this.mapperInterface = mapperInterface;
}
  • 這里完成了MapperFactoryBean構造函數的調用,完成了與相關Mapper接口的綁定
  • 依次找到所有Mapper接口完成賦值

創建資源配置類

1、大體流程

因為在StudentService中調用的UserPOMapper.selectByExample方法。

所以需要先創建StudentService,再創建其屬性值UserPOMapper

StudentService的初始化流程如下:

populateBean -->> CommonAnnotationBeanPostProcessor#postProcessProperties -->>InjectionMetadata#inject填充屬性UserPOMapper – >> getResourceToInject -->> getResource -->> autowireResource

– >> AbstractAutowireCapableBeanFactory#resolveBeanByName -->> getBean創建屬性UserPOMapper–>>

因為UserPOMapper的beanClass類型是MapperFactoryBean,這里創建UserPOMapper時,會先創建MapperFactoryBean(FactoryBean,先創建FactoryBean再創建T)

之前已經調用過有參構造函數創建過MapperFactoryBean了,這里直接初始化MapperFactoryBean

populateBean–>> autowireByType

// sqlSessionFactory 和 sqlSessionTemplate:循環創建這2個資源配置
String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw);

資源初始化開始===

2、MapperFactoryBean初始化資源1-sqlSessionFactory

對應Mybatis創建Mapper的步驟四(依賴步驟一、二執行步驟四)

resolveDependency -->> doResolveDependency–>> doGetBeanNamesForType:從bdMap中找類型是

SqlSessionFactory的bd

  • beanClass = null
  • factoryMethodName = sqlSessionFactory
  • factoryBeanName = MybatisAutoConfig

doCreateBean -->> createBeanInstance -->> instantiateUsingFactoryMethods使用FactoryMethod方式實例化

2.1 創建MybatisAutoConfig

對應Mybatis創建Mapper的步驟一

1)創建屬性properties:MybatisProperties

  • mapperLocations :mybatis.mapper-locations = classpath*:mapper/*.xml
  • configuration

2)populateBean-MybatisProperties

對應Mybatis中創建Mapper的步驟二

  • 創建DataSource:使用FactoryMethod方式實例化$$Hirari

    factoryMethodName:dataSource

    factoryBeanName : DataSourceConfiguration$Hikari

    • DataSourceConfiguration$Hikari
      • 創建DataSourcePorperties

        使用AutowiredAnnotationBeanPostProcessor#postProcessProperties完成屬性填充(driverClassName、url、user、password)

3、MapperFactoryBean初始化資源2-sqlSessionTemplate

  • SqlSessionFactory
  • Interceptor

資源初始化結束===

創建UserPOMapper

上述MapperFactoryBean的初始化也完成了,可以創建即UserPOMapper了

1、sqlsessionTemplate屬性

  • sqlSessionFactory

    • configutaion

      • environment
    • id:“sqlSessionFactoryBean”

      • DataSource
      • transactionFactory: “SpringManagedTransactionFactory”
    • SqlSessionFactoryBean

    • mapperRegistry

      • knowmapper

        key:Mapper接口的全路徑

        val:MapperFactoryBean@xxx

  • sqlSessionProxy:DefaultSqlSession

2、返回UserPOMapper代理對象

1)getBean(“xxMapper”)

SqlSessionTemplate#getMapper -->> Configuration#getMapper -->> mapperRegistry的knowmapper此map中返回對應的MapperFactoryBean@xxx

2)getObject

MapperFactoryBean#getObject:對應Mybatis中創建Mapper的步驟五

return this.getSqlSession().getMapper(this.mapperInterface);
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {// 1.從map中獲取val:MapperFactoryBean// 并轉為MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);// 2.生成MapperProxy代理類return mapperProxyFactory.newInstance(sqlSession);      
}
  • 創建代理對象并返回
public T newInstance(SqlSession sqlSession) {MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);return this.newInstance(mapperProxy);
}
  • org.apache.ibatis.binding.MapperProxy@252a8aae

5.2 MapperPrxoy

1、舉例(后續都以次例子為例

  • MyMapper
public interface MyMapper extends UserPOMapper{@Select("SELECT * FROM tb_user WHERE name = #{name} and age = #{age}")UserPO selectByNameAndAge(@Param("name") String name, @Param("age")  int age);
}
  • 測試
@Resource
private MyMapper myMapper;@Test
public UserPO testMybatis() {return myMapper.selectByNameAndAge(name, age);
}

2、動態代理前置執行流程

myMapper#selectByNameAndAge – >>

MapperProxy#invoke -->> 內部類PlainMethodInvoker#invoke – >>

執行PlainMethodInvoker屬性mapperMethod.execute -->>

switch-case選擇,CRUD類型(以select查詢為例)

– >> case SELECT

// 方法參數map
Object param = this.method.convertArgsToSqlCommandParam(args);
// 使用sqlSession執行查詢
result = sqlSession.selectOne(this.command.getName(), param);

至此MapperProxy代理相關的執行結束,后續流程主要是兩大步:方法參數解析、sqlSession.selectOne


5.3 方法參數解析

Object param = this.method.convertArgsToSqlCommandParam(args);//實際上這個param是Map
ParamNameResolver構造函數

MapperPrxoy執行過程中創建內部類new PlainMethodInvoker

  • new MapperMethod
    • new MethodSignature
      • new ParamNameResolver
public ParamNameResolver(Configuration config, Method method) {// truethis.useActualParamName = config.isUseActualParamName();// 1.反射獲取selectByNameAndAge方法的所有參數類型(String 和 int)Class<?>[] paramTypes = method.getParameterTypes();// 2.反射獲取selectByNameAndAge方法的所有參數以及參數上的注解修飾(一維數據中為參數index、而且數組中為修飾參數的注解,因為一個參數可能有多個注解,所以用了二維數組,正常情況下就一個注解修飾一個參數)// 這里[0][0]表示方法一個參數對應的第一個注解,很顯然是@Param注解,[1][0]表示第二個參數對應的第一個注解顯然也是@Param注解Annotation[][] paramAnnotations = method.getParameterAnnotations();// 3.創建一個mapSortedMap<Integer, String> map = new TreeMap();// 4.二維數組.length表示一維數組中元素個數,即selectByNameAndAge方法入參個數:2int paramCount = paramAnnotations.length;// 5.循環遍歷解析for(int paramIndex = 0; paramIndex < paramCount; ++paramIndex) {if (!isSpecialParameter(paramTypes[paramIndex])) {String name = null;// 5.1 方法第一個參數所對應的注解數組,正常就一個@ParamAnnotation[] var9 = paramAnnotations[paramIndex];   int var10 = var9.length;// 5.2 循環遍歷二維數組,正常情況下就一個@Param注解for(int var11 = 0; var11 < var10; ++var11) {Annotation annotation = var9[var11];// 5.3 滿足if (annotation instanceof Param) {// 5.4 將hasParamAnnotation參數設置為truethis.hasParamAnnotation = true;// 5.5 獲取@Param注解的value值,即@Param("name")name = ((Param)annotation).value();break;}}// 6.如果參數沒有使用注解,則name = 參數名if (name == null) {if (this.useActualParamName) {name = this.getActualParamName(method, paramIndex);}}// 6.存入map,k1=0,v1 = "name" ; k2 = 1,v2 = "age"map.put(paramIndex, name);}}// 7.存入另外一個map中this.names = Collections.unmodifiableSortedMap(map);
}

convertArgsToSqlCommandParam方法參數解析

– >> getNamedParams(將方法入參解析成map)

public Object getNamedParams(Object[] args) {// 1.names就是上述map,size=2int paramCount = this.names.size();// 2.這里size=2if (!this.hasParamAnnotation && paramCount == 1) {// 如果就一個參數,則直接args[0]即獲取方法入參的一個參數值"mjp"即可Object value = args[(Integer)this.names.firstKey()];return wrapToMapIfCollection(value, this.useActualParamName ? (String)this.names.get(0) : null);} else {Map<String, Object> param = new MapperMethod.ParamMap();int i = 0;// 3.遍歷map的entryfor(Iterator var5 = this.names.entrySet().iterator(); var5.hasNext(); ++i) {// 3.1 entry內容為0-"name"、1-"age"Map.Entry<Integer, String> entry = (Map.Entry)var5.next();// 3.2 存入新的map,k1:"name",v1:args[0]="mjp"// 同理k2: "age" v2 = 23param.put(entry.getValue(), args[(Integer)entry.getKey()]);// 3.3 生成param1、param2字符串String genericParamName = "param" + (i + 1);// 3.4 如果param此Map中沒有param1這個key,則存入 "param1"-"mjp"// 同理如果沒有param2這個key,則存入"param2"-23if (!this.names.containsValue(genericParamName)) {param.put(genericParamName, args[(Integer)entry.getKey()]);}}// 4.最終的param此map中有四個entry對象分別為"name":"mjp""age":23"param1":"mjp""param2":23return param;}
}

補充:

@Select("SELECT * FROM tb_user WHERE name = #{name} and age = #{age}")
UserPO selectByNameAndAge(String name, int age);

不使用@Param注解

// 4.最終的param此map中,也是有四個entry對象分別為
"arg0":"mjp""arg1":23"param1":"mjp""param2":23

建議方法

  • 多個參數時:加上@Param注解,這樣可以減少參數解析流程(底層是基于反射轉換)、而且只有Jdk1.8之后才可以
@Select("SELECT * FROM tb_user WHERE name = #{name} and age = #{age}")
UserPO selectByNameAndAge(String name, int age);

jdk1.8之前必須這樣寫

@Select("SELECT * FROM tb_user WHERE name = #{arg0} and age = #{arg1}")
UserPO selectByNameAndAge(String name, int age);
  • 單個參數:不做任何處理,名稱可以任意,就是唯一對應
@Select("SELECT * FROM tb_user WHERE name = #{這里的名稱可以任意}")
UserPO selectByNameAndAge(String name);

5.4 sqlSession.selectOne

5.4.1 SqlSessionTemplate

sqlSession.selectOne–>> SqlSessionTemplate#selectOne -->> 內部屬性代理對象sqlSessionProxy.selectOne

1、區別

spring整合mybatis方法調用流程,為什么不直接像mybatis中那樣sqlSession.selectOne–>> 直接調用DefaultSqlSession#selectOne,為什么要創建DefaultSqlSession代理對象

2、原因

  • Mybatis中DefaultSqlSession是線程不安全的

  • Spring中對DefaultSqlSession代理,實現增強

    在方法執行前,實現增強:即為每個線程創建一個SqlSession

(實現方式:通過ThreadLocal-bindSource將線程和資源綁定,達到線程安全效果)

public class SqlSessionTemplate implements SqlSession, DisposableBean {private final SqlSessionFactory sqlSessionFactory;private final ExecutorType executorType;// 5.4.2.DefaultSqlSession代理對象private final SqlSession sqlSessionProxy;private final PersistenceExceptionTranslator exceptionTranslator;// 5.4.3.invoke增強private class SqlSessionInterceptor implements InvocationHandler {public Object invoke(Object proxy, Method method, Object[] args) {// 1.在方法執行前,完成增強// 增強功能:創建線程安全的SqlSessionSqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);Object unwrapped;try {// 2.執行目標了的目標方法:DefaultSqlSession#selectOne// sqlSession:DefaultSqlSession// method:selectOne// args:map參數Object result = method.invoke(sqlSession, args);if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {// =============這里一定要commit============// 1、完成connection.commit()// 2、完成了查詢結果對象,存入緩存等步驟sqlSession.commit(true);}unwrapped = result;} finally {// 3.關閉SqlSessionif (sqlSession != null) {SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);}}return unwrapped;}}
}

5.4.2 sqlSessionProxy.selectOne

SqlSessionTemplate內部屬性sqlSessionProxy代理對象#selectOne – >>

SqlSessionTemplate內部類攔截處理器SqlSessionInterceptor#invoke


5.4.2.1 getSqlSession創建SqlSession
線程安全

1、背景

UserPOMapper mapper1 = sqlSession.getMapper(UserPOMapper.class);
mapper1.selectById(1L);StudentPOMapper mapper2 = sqlSession.getMapper(StudentPOMapper.class);
mapper2.selectById(2L);

Mybatis中使用相同的SqlSession創建Mapper,不同的Mapper都使用相同的SqlSession,存在線程安全問題

2、Spring整合Mybatis解決不同Mapper公用一個SqlSession線程安全問題

每個線程有單獨的DefaultSqlSession

3、實現

ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal(“Transactional resources”);

  • k1:v1( SqlSessionFactory:SqlSessionHolder)
    • SqlSessionHolder中持有SqlSession對象
  • k1:v1(DataSource:JDBCConnection)
    • JDBCConnection持有con連接對象

4、源碼剖析

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {// 步驟一.嘗試從resources中獲取SqlSessionHolderSqlSessionHolder holder = (SqlSessionHolder)TransactionSynchronizationManager.getResource(sessionFactory);// 步驟二.如果SqlSessionHolder不為空,則直接拿到sqlSession,并返回SqlSession session = sessionHolder(executorType, holder);if (session != null) {return session;} else {   // 步驟三.否則,執行創建一個DefaultSqlSessionsession = sessionFactory.openSession(executorType);// 步驟四.創建一個SqlSessionHolder// 將新創建的DefaultSqlSession賦值給holder// 存入resources(k:sessionFactory, v:holder)registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);return session;}
}

1)步驟三openSession

// 1.獲取環境字眼
Environment environment = this.configuration.getEnvironment();
// 2.獲取事務工廠
TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);// 3.創建事務 new SpringManagedTransaction(dataSource)
Transaction tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);// 4.創建執行器SimpleExecutor
Executor executor = this.configuration.newExecutor(tx, execType);// 5.使用有參構造創建DefaultSqlSession
DefaultSqlSession var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);

=======創建Executor=

Executor-newExecutor

1、創建Executor

protected final InterceptorChain interceptorChain;public Executor newExecutor(Transaction transaction, ExecutorType executorType) {// 1.創建SimpleExecutorexecutor = new SimpleExecutor(this, transaction);// 2.裝飾者模式:二級緩存相關// 后續Executor都是CachingExecutor類型if (this.cacheEnabled) {executor = new CachingExecutor((Executor)executor);}// 3.攔截器鏈:插件相關Executor executor = (Executor)this.interceptorChain.pluginAll(executor);eturn executor;
}

2、裝飾著設計模式

參考下文:六、設計模式-6.2裝飾著模式

3、組件-攔截器鏈

一旦涉及interceptorChain,就和插件有關,后續分析

=======創建Executor

2)接著步驟四

private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator, SqlSession session) {//4.1 是否有事務@Transactional即是否開啟了事務// 只有開啟了事務,才會創建一個SqlSessionHolder并將新創建的DefaultSqlSession賦值給holder,最后存入resources(k:sessionFactory, v:holder)if (TransactionSynchronizationManager.isSynchronizationActive()) {Environment environment = sessionFactory.getConfiguration().getEnvironment();if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {// 4.2創建SqlSessionHolder,并為其屬性SqlSession賦值     SqlSessionHolder holder = new SqlSessionHolder(session, executorType, exceptionTranslator);// 4.3 resources.put(SqlSessionFactory, 新創建的holder)// 完成當前線程 和 SqlSession的綁定TransactionSynchronizationManager.bindResource(sessionFactory, holder);TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));holder.setSynchronizedWithTransaction(true);holder.requested();} else {}} else {}
}

5、場景1:不同線程,t1-MyMapperr#selectByNameAndAge、t2-MyMapperr#selectByNameAndAge

t1和t2在查詢時,都會創建一個新的DefaultSqlSession,不存在線程安全問題

6、場景2:同一個線程-未開啟事務

UserPO po1 = myMapper.selectByNameAndAge("mjp", 18);
UserPO po2 = myMapper.selectByNameAndAge("mjp", 18);

1)第一次執行查詢

  • 步驟一:使用SqlSessionFactory去resources中拿SqlSessionHolder,因為是第一次來,所以resources資源中沒有,返回null
  • 步驟二:SqlSessionHolder為null,所以SqlSession也為null,走步驟三去創建
  • 步驟三:創建一個DefaultSqlSession
  • 步驟四:判斷是否開啟了事務,顯然沒有@Transactional注解,未開啟事務,則不會走步驟四中邏輯,即不會將本次新創建的DefaultSqlSession賦值給holder再通過threadLocal綁定給當前線程

2)第二次執行查詢

  • 流程和第一次執行查詢完全一樣。也是會創建一個新的DefaultSqlSession
  • 顯然:Spring整合Mybatis后,一級緩存功能失效了

Spring中一級緩存失效

1、背景

Mybatis中一級緩存是sqlSession級別的,正常情況下上述場景6的第二次執行,會走緩存查詢,而不是再創建一個新的DefaultSqlSession執行db查詢。

2、mybatis中一級緩存失效場景

  • 查詢條件不同
  • 同一個sqlSession,查詢條件相同,但是兩次查詢之間進行了delete、insert、update操作

3、Spring中一級緩存失效場景

由上述場景6結果來看,默認一級緩存就是失效的。


Spring事務一級緩存有效

1、Spring中一級緩存生效場景

由上述步驟四registerSessionHolder方法可知,只要滿足了

if (TransactionSynchronizationManager.isSynchronizationActive()) {// 創建綁定資源
}

就可能將新創建的DefaultSqlSession,綁定到threadLocal中

2、什么場景下,TransactionSynchronizationManager.isSynchronizationActive()返回true

public static boolean isSynchronizationActive() {return synchronizations.get() != null;
}

其中

ThreadLocal<Set<TransactionSynchronization>> synchronizations = 
new NamedThreadLocal("Transaction synchronizations");

只要synchronizations中set集合中有值,則isSynchronizationActive返回ture,則會走到步驟四內部,實現資源綁定

3、什么場景下,synchronizations會添加Set集合

1)使用@Transactional注解,開啟事務時

2)源碼分析

Spring事務可以參考我另一篇:Spring5源碼剖析

文章中4.3.1 創建事務:createTransactionIfNecessary -->>

AbstractPlatformTransactionManager#getTransaction -->>

// 1.先嘗試從緩存中獲取事務-顯然第一次創建事務時,沒有連接對象con的緩存,con需要走后續的創建
Object transaction = doGetTransaction();// 2.創建事務
DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);//這里的true表示newTransaction = true新事務// 3.開啟事務和連接-設置連接屬性
doBegin(transaction, definition);//DataSourceTransactionManager#doBegin// 4.新事物設置屬性
prepareSynchronization(status, definition);
return status;

在4.prepareSynchronization方法中

protected void prepareSynchronization(DefaultTransactionStatus status, TransactionDefinition definition) {if (status.isNewSynchronization()) {TransactionSynchronizationManager.initSynchronization();
}

initSynchronization

public static void initSynchronization() throws IllegalStateException {synchronizations.set(new LinkedHashSet());
}

此時synchronizations集合中有值了。


不使用事務一級緩存生效

1、訴求

已知業務場景沒有線程安全問題,在不使用事務的情況下,讓一級緩存生效

2、實現

1)失效根因

上述之所以一級緩存會失效,根本原因就是Spring不是像Mybatis那樣,直接調用DefaultSqlSession.slectOne方法,而且創建了sqlSessionProxy代理對象,在執行DefaultSqlSession.slectOne之前,完成方法增強:實現SqlSession和線程資源綁定

2)解決

可以直接像Mybatis那樣,直接使用DefaultSqlSession.slectOne

3、DefaultSqlSession.slectOne

  • 類實現ApplicationContextAware接口,獲取ApplicationContext屬性
  • 從Spring上下文中,獲取相應的bean
@Service
public classMyService implements ApplicationContextAware {private ApplicationContext applicationContext;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.applicationContext = applicationContext;}public void query(String name, Integer age){//1.獲取DefaultSqlSessionSqlSessionFactory sqlSessionFactory = (SqlSessionFactory) applicationContext.getBean("sqlSessionFactory");SqlSession sqlSession = sqlSessionFactory.openSession();Map<String, Object> param = new HashMap<>();param.put("name", "mjp");param.put("age", "18");// 2.執行DefaultSqlSession#slectOneObject result1 = sqlSession.selectOne("com.mjp.mysql.mapper.MyMapper.selectByNameAndAge", param);// 第二次查詢直接從一級緩存中取,不會查dbObject result2 = sqlSession.selectOne("com.mjp.mysql.mapper.MyMapper.selectByNameAndAge", param);}
}

5.4.2.2 method.invoke

method.invoke(sqlSession, args) -->> DefaultSqlSession#selectOne -->> selectList

public class DefaultSqlSession implements SqlSession {private final Configuration configuration;private final Executor executor;private final boolean autoCommit;private List<Cursor<?>> cursorList;/*** statement:方法全路徑(com.xxx.MyMapper.selectByNameAndAge)* parameter:方法參數map("name":"mjp"、"age":23、"param1":"mjp"、"param2":23)* rowBounds:分頁查詢條件,默認RowBounds.DEFAULT*/public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {// Mapper相關聲明的信息MappedStatement ms = this.configuration.getMappedStatement(statement);// 執行器執行查詢,這里的執行器是裝飾者CachingExecutorList var5 = this.executor.query(ms, this.wrapCollection(parameter), RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);return var5;}
}
RowBounds

this.executor.query方法入參RowBounds.DEFAULT

public class RowBounds {public static final RowBounds DEFAULT = new RowBounds();// 默認無參構造public RowBounds() {this.offset = 0;this.limit = Integer.MAX_VALUE;}
}

這就解釋了明明我們代碼中sql:select * from tb_user where name = ‘mjp’ and age = 18;

但打印查詢語句時sql: select * from tb_user where name = ‘mjp’ and age = 18 limit 0 ,2147483647;

就是因為查詢時,RowBounds參數使用的默認值RowBounds.DEFAULT


MappedStatement

1、內容

方法聲明,內含方法信息(方法全路徑、方法CRUD類型、方法返回值類型)

2、數據層級

configuration

  • mappedStatements(map)

    • k1:“com.mjp.mysql.mapper.MyMapper.selectByNameAndAge”(查詢方法全路徑)
    • v1: MappedStatement@6781
      • id:“com.mjp.mysql.mapper.MyMapper.selectByNameAndAge”
      • sqlCommandType:SELECT(查詢操作類型)
      • resultMaps
        • type:class com.mjp.mysql.entity.UserPO(查詢方法返回類型)
      • useCache:true(使用二級緩存)
      • cache:Cache對象(二級緩存對象),同一個Mapper下的mappedStatements都使用同一個Cache對象
      • statementType:PREPARED(用于創建StatementHandler)
  • caches:二級緩存

    內容如圖4所示
    在這里插入圖片描述

    緩存key中包含了SqlSessionFactoryBean這也是為什么有文章說二級緩存是基于SqlSessionFactory級別的


executor.query

–>> 裝飾者CachingExecutor#query

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) {// 步驟一:創建動態SQLBoundSql boundSql = ms.getBoundSql(parameterObject);// 步驟二:創建二級緩存keyCacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);// 步驟三:執行查詢return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
創建BoundSql

1、方法

ms.getBoundSql(parameterObject)

  • ms:MappedStatement,含有Mapper接口的信息(方法全路徑、方法返回值類型、方法操作類型)
  • parameterObject:方法參數(map)

2、方法內容

  • 調用有參構造器new BoundSql(x,x,x)創建BoundSql,并完成sql語句 和 方法參數等屬性賦值

3、對象屬性

BoundSql

  • sql:SELECT * FROM tb_user WHERE name = ? and age = ?
  • parameterObject:方法入參map(“name”:“mjp”、“age”:23、“param1”:“mjp”、“param2”:23)
  • parameterMappings
    • ParameterMapping對象
      • property:參數名"name"
      • javaType:Object
      • jdbcType:mysql屬性類型

創建二級緩存CacheKey

1、方法

createCacheKey -->> SimpleExecutor父類BaseExecutor#createCacheKey

2、方法內容

就是從MappedStatement對象、rowBound對象、BoundSql對象、以及configuration對象中獲取相關屬性,填充創建的CacheKey

二級緩存的key內容如圖4所示
在這里插入圖片描述


執行查詢query
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql){// 二級緩存Cache cache = ms.getCache();if (cache != null) {//使用緩存}// 沒有緩存,直接查詢return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}

1、使用二級緩存(裝飾著增強功能)

內容在5.6.2模塊中

2、原始類(目標方法)

內容較多,單獨起一個模塊5.5


5.5 Executor.query

原生JDBC

// 1.注冊驅動
Class.forName("com.mysql.jdbc.Driver");
// 2.獲取數據庫連接對象
con = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "root");
// 3.解析參數
String myName = "mjp or 1==1";
// 4.解析sql
String sql = "select * from table_user where name =  ?";
// 5.將sql交由pstm,動態生成sql
PreparedStatement pst = con.prepareStatement(sql);
pst.setString(1, myName);
// 6.執行查詢
ResultSet resultSet = preparedStatement.executeQuery();

this.delegate.query -->> CachingExecutor.SimpleExecutor.query -->> 調用SimpleExecutor父類BaseExecutor#query -->> queryFromDatabase -->> this.doQuery —>> SimpleExecutor#doQuery

public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {Statement stmt = null;List var9;try {Configuration configuration = ms.getConfiguration();// 步驟一:創建PreparedStatementHandlerStatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);// 步驟二:創建Statement(2-5)stmt = this.prepareStatement(handler, ms.getStatementLog());// 步驟三:執行查詢(6)var9 = handler.query(stmt, resultHandler);} finally {this.closeStatement(stmt);}return var9;
}

5.5.1 newStatementHandler

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {// 1.構造方法RoutingStatementHandler// 內含屬性StatementHandler delegate為PreparedStatementHandler類型StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);// 2.執行攔截器鏈(后續5.8模塊會統一分析組件攔截器)StatementHandler statementHandler = (StatementHandler)this.interceptorChain.pluginAll(statementHandler);return statementHandler;
}
創建PreparedStatementHandler
public class RoutingStatementHandler implements StatementHandler {// 1.屬性private final StatementHandler delegate;//2.構造方法public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {//3.屬性賦值this.delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);}
}

在new PreparedStatementHandler時,完成parameterHandler和resultSetHandler的創建,并賦值給PreparedStatementHandler


創建parameterHandler
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {// 1.創建DefaultParameterHandlerParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);//2. 執行攔截器鏈(后續5.8模塊會統一分析組件攔截器)parameterHandler = (ParameterHandler)this.interceptorChain.pluginAll(parameterHandler);return parameterHandler;
}

創建resultSetHandler
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {// 1.創建DefaultResultSetHandler(內含resultHandler)ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);//2. 執行攔截器鏈(后續5.8模塊會統一分析組件攔截器)ResultSetHandler resultSetHandler = (ResultSetHandler)this.interceptorChain.pluginAll(resultSetHandler);return resultSetHandler;
}

5.5.2 prepareStatement

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {// 1.創建con數據庫連接Connection connection = this.getConnection(statementLog);// 2.創建PrepareStatementStatement stmt = handler.prepare(connection, this.transaction.getTimeout());// 3.完成sql占位符解析handler.parameterize(stmt);return stmt;
}
創建Connection
(Connection)Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);

這里創建的是Connection代理對象$Proxy@xxx org.apache.ibatis.logging.jdbc.ConnectionLogger@5c0c4eec

  • 其中ConnectionLogger
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {//HikariProxyConnection@490154188 wrapping com.mysql.jdbc.JDBC4Connection@1f22ee76// 真正的Conprivate final Connection connection;
}

所以這里先創建代理對象ConnectionLogger,目的是在使用真正JDBC4Connection創建PrepareStatement之前或之后,完成某些功能的增強。看名稱和Logger相關,應該是日志增強


創建PrepareStatement

handler.prepare -->> RoutingStatementHandler#prepare -->>

delegate.prepare -->> PreparedStatementHandler#prepare -->> 調用父類BaseStatementHandler#prepare

statement = this.instantiateStatement(connection);

–>> this.instantiateStatement -->> PreparedStatementHandler#instantiateStatement

connection.prepareStatement(sql)

因為connection是Jdk的$Proxy代理對象,所以會執行ConnectionLogger#invoke

/**
* method:			prepareStatement
* this:				ConnectionLogger
* this.connection:	JDBC4Connection
* params:			sql語句
*/
// 1.JDBC4Connection#prepareStatement -->> ConnectionImpl#prepareStatement
// JDBC4PreparedStatement@4e940fcf
PreparedStatement stmt = (PreparedStatement)method.invoke(this.connection, params);// 2.代理增強功能
// (PreparedStatement)Proxy.newProxyInstance(cl, new Class[]{PreparedStatement.class, CallableStatement.class}, handler)
// 此時stmt 從 JDBC4PreparedStatement ==>> $Proxy@xxx:PreparedStatementLogger
stmt = PreparedStatementLogger.newInstance(stmt, this.statementLog, this.queryStack);
return stmt;

最終返回的stmt:$Proxy@xxx:PreparedStatementLogger


parameterize動態SQL參數映射

–>> RoutingStatementHandler#parameterize -->>

this.delegate.parameterize -->> PreparedStatementHandler#parameterize -->> this.parameterHandler.setParameters -->> DefaultParameterHandler#setParameters

1、方法參數映射

if (this.boundSql.hasAdditionalParameter(propertyName)) {//<for each>標簽轉換value = this.boundSql.getAdditionalParameter(propertyName);
} else if (this.parameterObject == null) {// 無參value = null;
} else if (this.typeHandlerRegistry.hasTypeHandler(this.parameterObject.getClass())) {// 單參value = this.parameterObject;
} else {// 多參// 參數mapMetaObject metaObject = this.configuration.newMetaObject(this.parameterObject);// 根據key(方法參數名稱)獲取val屬性值(參數值)value = metaObject.getValue(propertyName);
}//根據val類型,獲取相應的TypeHandler類型。如果參數類型是String,則TypeHandler為StringTypeHandler
TypeHandler typeHandler = parameterMapping.getTypeHandler();
// 獲取JDBC類型
JdbcType jdbcType = parameterMapping.getJdbcType();// 完成sql占位符的解析 和 填充屬性值
typeHandler.setParameter(pst, i + 1, value, jdbcType);

所以真正完成sql映射的組件的是 XxxTypeHandler


5.5.3 handler.query

handler.query -->> RoutingStatementHandler —>>this.delegate.query -->>PreparedStatementHandler#query

public <E> List<E> query(Statement statement, ResultHandler resultHandler) {// $Proxy@xxx:PreparedStatementLoggerPreparedStatement ps = (PreparedStatement)statement;// 1.執行PreparedStatementLogger#invoke,完成execute增強(this.debug打印了日志)ps.execute();// 2.處理結果集return this.resultSetHandler.handleResultSets(ps);
}
ps.execute

底層調用PreparedStatement#execute

–>> executeInternal–>> execSQL -->> this.io.sqlQueryDirect(使用MySQLIO對象查詢)–>>readAllResults -->> readResultsForQueryOrUpdate

獲取到sql查詢結果


handleResultSets
public List<Object> handleResultSets(Statement stmt)n {// 1.定義集合,存儲最終結果// 正常情況下就一個結果返回(沒有子查詢、嵌套查詢的情況)List<Object> multipleResults = new ArrayList();int resultSetCount = 0;// 2.從prepareStatement中獲取結果ResultSet并封裝一層為rswResultSetWrapper rsw = this.getFirstResultSet(stmt);// 3.從ms中獲取我們方法返回值類型// class com.mjp.mysql.entity.UserPOList<ResultMap> resultMaps = this.mappedStatement.getResultMaps();int resultMapCount = resultMaps.size();while(rsw != null && resultMapCount > resultSetCount) {// 3.1 獲取方法返回值類型// class com.mjp.mysql.entity.UserPO// 無論方法返回值是List<UserPO> 還是UserPO,都是這ResultMap resultMap = (ResultMap)resultMaps.get(resultSetCount);// 4.處理結果// 將mysql查詢結果rsw ==>> Java方法查詢結果resultMap// 將結果存入multipleResultsthis.handleResultSet(rsw, resultMap, multipleResults, (ResultMapping)null);// 沒有嵌套查詢的情況下,只有單結果,stmt中不會有nextResultSet,不會while循環rsw = this.getNextResultSet(stmt);this.cleanUpAfterHandlingResultSet();++resultSetCount;}return this.collapseSingleResultList(multipleResults);
}

獲取ResultSet
  • 方法名稱:getFirstResultSet
  • 方法內容:從PrepareStatement中獲取ResultSet

ResultSet結構如圖7所示
在這里插入圖片描述

2、封裝為ResultSetWrapper對象

ResultSetWrapper結構如圖8所示
在這里插入圖片描述

如果是select count() from t ,則columnNames = "count()"


handleResultSet
// 1.創建ResultSetHandler:DefaultResultHand類型
DefaultResultHandler defaultResultHandler = new DefaultResultHandler(this.objectFactory);// 2.處理結果集(while循環一行一行處理row -> Object)
this.handleRowValues(rsw, resultMap, defaultResultHandler, this.rowBounds, (ResultMapping)null);// 3.將結果集list添加到multipleResults
multipleResults.add(defaultResultHandler.getResultList());
1.創建ResultHandler

創建ResultSetHandler:DefaultResultHandler類型

public class DefaultResultHandler implements ResultHandler<Object> {// 最終方法返回結果集private final List<Object> list;// 添加每一行的返回對象public void handleResult(ResultContext<?> context) {//從ResultContext中獲取row -> Object的單行數據,存入lsitthis.list.add(context.getResultObject());}
}

2.handleRowValues

this.handleRowValues–>> DefaultResultSetHandler#handleRowValuesForSimpleResultMap

// 2.1.創建ResultContext:存入解析的單行數據 以及 查詢的total
DefaultResultContext<Object> resultContext = new DefaultResultContext();
ResultSet resultSet = rsw.getResultSet();// 2.2 循環條件
while(this.shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {// 2.3單行處理,Java對象放入resultContext-ObjectObject rowValue = this.getRowValue(rsw, discriminatedResultMap, (String)null);// 2.4處理結果集放resultHandler中listthis.storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}

2.1、創建ResultContext

public class DefaultResultContext<T> implements ResultContext<T> {// 1.row->Objectprivate T resultObject = null;// 2.解析的行數private int resultCount = 0;// 3.是否停止解析private boolean stopped = false;public T getResultObject() {return this.resultObject;}public void nextResultObject(T resultObject) {++this.resultCount;this.resultObject = resultObject;}public boolean isStopped() {return this.stopped;}
}

ResultSetHandler、ResultSet、ResultHandler、ResultContext之間關系如圖9所示
在這里插入圖片描述

為什么不直接將row解析出來的object直接放入ResultHandler結果集中,而是中間多一層ResultContext結果上下文

  • 有些場景:我們解析了第一行后存儲后,再解析第二行的時,發現我們結果集中已經有滿足條件的結果了。后續的row行結果就不需要再解析并存儲了。
  • 所以在ResultContext結果上下文中有個屬性boolean stop表明是否停止解析存儲

2.2 while條件

1)shouldProcessMoreRows

只有當!context.isStopped()時,才會進行row -> Object解析轉換

2)resultSet.next()

ResultSet屬性rowData屬性row(List)仍有元素

// 底層源碼
boolean b;
this.thisRow = this.rowData.next();
if (this.thisRow == null) {b = false;
} else {this.clearWarnings();// 說明next仍有元素b = true;
}

2.3、getRowValue

1)方法內容

圖中row -> object,方法入參rsw(內含rs內含row)

2)源碼分析

private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) {// 1.創建方法返回值Java對象(基本類型或封裝類型),此時屬性未賦值Object rowValue = this.createResultObject(rsw, resultMap, lazyLoader, columnPrefix);// 將Java對象再封裝一層,創建MetaObject對象,用于反射!!!MetaObject metaObject = this.configuration.newMetaObject(rowValue);boolean foundValues = this.useConstructorMappings;// 2.sql不是嵌套查詢的,返回結果也不是嵌套的if (this.shouldApplyAutomaticMappings(resultMap, false)) {// 3.自動填充創建的Java對象foundValues = this.applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;}return rowValue;
}

applyAutomaticMappings

  • 從rs獲取單行的所有列(select 列)集合(id、user_name、age—)
  • 遍歷這些列名稱
    • user_name
    • 從rs中根據user_name列名,獲取對應的值"mjp"
    • 通過反射框架MetaObject,將"mjp"值,通過映射找到對應JavaBeanuserName屬性,底層通過反射調用setUserName,設置UserPO的userName屬性值
  • 完成單個Java對象的屬性填充

2.4、storeObject

1)方法內容

object存ResultContext,再存ResultHandler

2)源碼分析

storeObject -->> callResultHandler

private void callResultHandler(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue) {// 1.將解析出來的單行java對象,存入resultContext的object屬性,并且count++resultContext.nextResultObject(rowValue);// 2.將resultContext中剛剛賦值的object屬性,再存入resultHandler的list中,完成返回集resultHandler.handleResult(resultContext);
}

最后是將resultHandler的list返給multipleResults.add(defaultResultHandler.getList());

最終返回的就是multipleResults


5.6 MetaObject

1、背景:

我們都知道Mybatis是ORM框架,它強大在可以將JavaBean及其屬性和MySQL表及其列,之前相互關聯映射。

而關聯、映射,底層就是使用的反射框架MetaObject

2、應用

對于Mybatis而言,單行結果就是Object對象,根本不知道此JavaBean對象以及其屬性 和 表列名的映射關系。

此時就可以使用MetaObject封裝的反射框架

3、快速開始

在Mybatis結果映射源碼中

MetaObject metaObject = this.configuration.newMetaObject(rowValue);//其中rowValue就是Object,我們知道是UserPO對象,但是Mybatis不知道
  • 自定義-屬性賦值
// 1.對于Mybatis而言,obj類型不知道,只知道是Object類型
Object obj = new UserPO();
// 2.創建MetaObject
MetaObject metaObject = new Configuration().newMetaObject(obj);
// 3.源碼中也類似,通過rs中row,獲取列名user_name,以及列值"mjp",然后metaObject.setValue
metaObject.setValue("user_name", "mjp");
System.out.println(metaObject.getValue("user_name"));//mjp
System.out.println((UserPO)obj.getUserName());//mjp
  • 自定義-根據表列名,獲取javaBean屬性名
Object obj = new UserPO();
MetaObject metaObject = new Configuration().newMetaObject(obj);
String property = metaObject.findProperty("user_name", true);
//System.out.println(property);userName

5.6.1 PropertyTokenizer分詞器

Demo類屬性

  • List userDemoList
    • UserDemo
      • SkuInfo skuInfo
        • skuId
        • skuName

專門解析userDemoList[0].skuInfo.skuName這種表達式。

SkuInfo skuInfo = new SkuInfo();
skuInfo.setSkuId(1L);
skuInfo.setSkuName("apple");UserDemo userDemo = new UserDemo();
userDemo.setSkuInfo(skuInfo);List<UserDemo> userDemoList = Lists.newArrayList(userDemo);Object obj = new Demo(userDemoList);MetaObject metaObject = new Configuration().newMetaObject(obj);
Object value = metaObject.getValue("userDemoList[0].skuInfo.skuName");
System.out.println(value);//apple

5.7 緩存

緩存查詢順序二級 -> 一級 -> db

5.6.1 一級緩存

1、范圍

sqlSession級別的

  • 驗證是否命中緩存:同一個方法內執行兩次,如果只打印了一條sql語句,說明后者走的是緩存
MyMapper mapper1 = sqlSession.getMapper(MyMapper.class);
MyDO myDO1 = mapper.selectByPrimaryKey(1L);MyMapper mapper2 = sqlSession.getMapper(MyMapper.class);
MyDO myDO2 = mapper2.selectByPrimaryKey(1L);
  • 定義:同一個sqlSession創建的不同Mapper接口,第一次查詢的數據會被緩存。下次查詢相同的數據時,先從緩存中取

2、失效場景

  • Spring整合Mybaits默認失效

  • 查詢條件不同

  • 同一個sqlSession,查詢條件相同,但是兩次查詢之間進行了delete、insert、update操作

3、一級緩存名稱

localCache


5.6.2 二級緩存

1、范圍

1)定義

基于命名空間(Mapper)進行緩存 ,即一個Mapper中一個Cache。

相同Mapper中的不同MappedStatement公用一個Cache

2)舉例

UserPOMapper接口對應UserPOMapper.xml,則此xml中一個Cache

此xml中不同的CRUD方法(主要是查詢相關的方法select、count)對應不同的MappedStatement對象,這些CRUD公用此Cache

默認是關閉的


2、開啟二級緩存

1)基于xml的Mapper.xml添加標簽

<cache eviction="LRU" flushInterval="60000">
</cache>
  • Eviction:緩存策略:LRU算法
  • flushInterval:緩存刷新的間隔,單位是ms。默認是只有delete、update、insert語句,才會刷新緩存
  • type:默認是mybatis自帶的,可以在此指定第三方的緩存

2)基于接口的Mapper添加@CacheNamespace注解

@CacheNamespace
public interface MyMapper extends UserPOMapper{@Select("SELECT * FROM tb_user WHERE name = #{name} and age = #{age}")UserPO selectByNameAndAge(@Param("name") String name, @Param("age")  int age);
}

自定義udfMapper,繼承mybatis-Generator.xml逆向工程生成的Mapper。逆向工程生成的Mapper使用@CacheNamespace不生效,必須在逆向工程生成的接口對應的Mapper.xml中使用

  • Mapper接口全局啟用二級緩存,但是想某個方法不使用二級緩存
@Options(useCache = false)
@Select("SELECT * FROM user WHERE name = #{name} and age = #{age}")
MyDO selectByNameAndAge(@Param("name") String name, @Param("age")  int age);

3)緩存引用

@CacheNamespaceRef(XxxMapper.class)
public interface UserPOMapper {
}

二者公用一個緩存,其中一個清除了,二者都清除


3、失效場景
  • 查詢結果實體類,沒有實現序列化接口(mybatisGenerator默認生成的DO會實現序列化接口)

  • 發生了數據變更(insert、update、delete)

    所以二級緩存更適合查詢不經常變更的數據(個人信息、國家省市信息等)


4. 解析@CacheNamespace注解

用于解析各種配置的類

  • XMLConfigBuilder#parse:解析mybatis-config.xml文件

  • MapperAnnotationBuilder#parse

    • 解析Mapper接口上的注解

    • Mapper的方法以及方法上的注解

    • 順便解析與XxxMapper接口同名的XxxMapper.xml:loadXmlResourced

      底層使用的XMLMapperBuilder#parse -->> configurationElement:解析XxxMapper.xml

最終將所有屬性信息,填充給Configuration對象

4.1 解析注解MapperAnnotationBuilder#parse方法入口

Spring在創建Mapper接口對應的bean時

  • 實例化
  • 初始化

在初始化過程中,執行invokeInitMethods -->> ((InitializingBean) bean).afterPropertiesSet()

其中bean為MapperFactoryBean(父類的父類為DaoSupport) -->>

DaoSupport#afterPropertiesSet -->>checkDaoConfig -->> 子類MapperFactoryBean實現checkDaoConfig -->>

configuration.addMapper -->> parse -->> MapperAnnotationBuilder#parse

4.2 創建Cache對象

public class MapperAnnotationBuilder {private final MapperBuilderAssistant assistant;private final Class<?> type;//name :com.mjp.mysql.mapper.UserPOMapper此接口的class對象private void parseCache() {//解析@CacheNamespace}
}

parse -->> parseCache

private void parseCache() {// 獲取@CacheNamespace注解屬性CacheNamespace cacheDomain = (CacheNamespace)this.type.getAnnotation(CacheNamespace.class);if (cacheDomain != null) {// 創建Cache對象this.assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props);}
}

useNewCache -->> Cache cache = (new CacheBuilder).build()

4.3 為Mapper接口中每個CRUD方法創建MappedStatement對象,并將Mapper接口級別的Cache對象賦值給每個方法

parse -->> parseStatement

// 獲取接口中的所有CRUD方法
Method[] var2 = this.type.getMethods();
for(int var4 = 0; var4 < var3; ++var4) {// 遍歷每一個方法,為其創建MappedStatement對象Method method = var2[var4];this.parseStatement(method);
}

–>> addMappedStatement -->>MappedStatement.Builder#build.cache(this.currentCache)為MappedStatement對象賦值cache屬性


5. 解析-查詢使用二級緩存
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) {//1.從MappedStatement對象中獲取Cache對象Cache cache = ms.getCache();if (cache != null) {// 2.是否將緩存中數據clear掉()this.flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) {this.ensureNoOutParams(ms, boundSql);// 3.從緩存中獲取結果List<E> list = (List)this.tcm.getObject(cache, key);if (list == null) {// 3.2緩存中沒有結果,繼續查詢list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);// 3.2.1將查詢db結果存儲到二級緩存中this.tcm.putObject(cache, key, list);}// 3.1緩存中有結果,則直接返回return list;}}
}
flushCacheIfRequired

1、作用

是否清空二級緩存。insert、update、delete語句執行前,都會先執行此方法。即清空二級緩存,這樣下次查詢的時候,二級緩存中沒數據,只能強制查詢db,這樣才能查詢到最新的數據

2、內容

判斷MappedStatement中flushCacheRequired屬性

  • false:則不需要flushCache,即走緩存查詢
  • ture,將緩存內容清除,后續走新的查詢

底層就是調用map.clear方法,將緩存清除這里的緩存清除,不是清除當前CacheKey的,而是將整個二級緩存map.clear

3、flushCacheRequired屬性值

MapperAnnotationBuilder#parseStatement -->>

在創建MappedStatement時,使用Build建造者build時,會判斷,如果是查詢語句,則flushCache為false,表明不需要清除緩存數據

boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = !isSelect;//如果是Select,則flushCache = false表示不清空緩存,其它查詢則true
boolean useCache = isSelect;//使用緩存

二級緩存開啟后的第一次查詢

1、結果

getObject沒值,需要繼續查詢

2、原因

MappedStatement對象中的Cache對象結構如圖4

在這里插入圖片描述

底層就是根據CacheKey,從HashMap中獲取value

第一次查詢顯然沒有結果

3、后續動作

1)走新的查詢SimpleExecutor#query

2)將查詢結果存入緩存:this.tcm.putObject(cache, key, list)

public void putObject(Cache cache, CacheKey key, Object value) {this.getTransactionalCache(cache).putObject(key, value);}

2.1)獲取Cache實例對應的TransactionCache

private TransactionalCache getTransactionalCache(Cache cache) {// 有就直接拿,沒有就創建一個,然后和Cache綁定// 這樣就實現了Cache對象和TransactionCache的綁定return (TransactionalCache)this.transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}

這樣做的原因:Cache對象線程不安全

  • 因為Cache對象是從MappedStatement中獲取的

  • 而MappedStatement對象是從從Configuration對象中獲取的

  • Configuration對象是全局的變量

  • 如果不將Cache實例(Mapper接口級別)和 TransactionCache綁定,則不同的Mapper接口或Mapper.xml文件,都可以獲取到彼此的Cache(暫存區)

  • 如圖6所示:Cache實例(Mapper級別) 和 對應TransactionCache(內含entriesToAddOnCommit屬性:暫存區)綁定后,當前Mapper的Cache,就只訪問當前對應TransactionCache中的暫存區,即Mapper1就只訪問暫存區1
    在這里插入圖片描述

未sqlSession.commit之前,只是對暫存區map-entriesToAddOnCommit進行操作。只有當執行了sqlSession.commit,才會真正的操作二級緩存內容

2.2)putObject

  • 先將cachekey - List存入TransactionalCache屬性entriesToAddOnCommit(對應圖6中的暫存區)此map

  • 后續SimpleExecutor#query整個查詢完成后,會回到SqlSessionTemplate中內部類SqlSessionInterceptor#invoke -->> sqlSession.commit

// DefaultSqlSession查詢
Object result = method.invoke(sqlSession, args);if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {//con.commit、將查詢結果存儲到二級緩存中sqlSession.commit(true);
}

sqlSession.commit–>> CachingExecutor#commit

public void commit(boolean required) throws SQLException {// 1.底層是con.committhis.delegate.commit(required);// 2.將查詢結果存入二級緩存this.tcm.commit();
}

tcm.commit–>> txCache.commit -->>flushPendingEntries

將entriesToAddOnCommit中內容,存入cache

private void flushPendingEntries() {Iterator var1 = this.entriesToAddOnCommit.entrySet().iterator();while(var1.hasNext()) {Map.Entry<Object, Object> entry = (Map.Entry)var1.next();// 這里的this.delegate,就是我們Cache對象this.delegate.putObject(entry.getKey(), entry.getValue());}
}
  • 為什么查詢到結果后,不直接將結果存入二級緩存,而是先存entriesToAddOnCommit,而是等sqlSession.commit后,再存二級緩存

    如果不這樣做,可能會造成臟讀

    • 事務1:Mapper1級別的Cache,查詢出結果,尚未執行sqlSession.commit,就直接存儲到真正的二級緩存了
    • 事務2:也可以訪問Mapper1級別的Cache,可能會讀取到事務1尚未提交的數據
    • 假如此時,事務1回滾了。事務2就相當于臟讀了

所以,只有當sqlSession.commit之后,查詢結果才會真正的存入二級緩存


二級緩存開啟后的第二次查詢

1、查詢this.tcm.getObject

public Object getObject(Object key) {// 步驟一.從二級緩存中查詢數據Object object = this.delegate.getObject(key);// 2.如果clearOnCommit=true,說明期間有編輯操作(雖然尚未commit)// 無論緩存是否查詢到數據,都直接返回nullreturn this.clearOnCommit ? null : object;
}

2、步驟一

底層就是根據CacheKey,從HashMap中獲取value返回

3、步驟二

1)如果在第二次查詢期間,有insert、delete、update操作,且執行了sqlSession.Commitz則

  • 二級緩存被清空
  • 查詢不到緩存

2)如果在第二次查詢期間,有insert、delete、update操作,尚未執行sqlSession.Commitz則

public int update(MappedStatement ms, Object parameter) throws SQLException {// 1.給二級緩存打個標clearOnCommit=true,表明后續查詢操作不要使用查詢出來的二級緩存結果了this.clearLocalCache();return this.doUpdate(ms, parameter);
}

clearLocalCache -->> TransactionalCache#clear

public void clear() {this.clearOnCommit = true;this.entriesToAddOnCommit.clear();
}
  • 此時update操作尚未commit,但是update操作有很大概率會成功,不會回滾
  • 如果查詢到了二級緩存返回,此時update也commit了
  • 顯然查詢出來的緩存內容 和 更新后的內容可能不符,臟讀。

所以,先給二級緩存打個標clearOnCommit = true,這樣查詢時,即使從二級緩存中查詢到了數據,也不使用直接返回null


6、二級緩存存在問題

默認緩存Cache接口實現類PerpetualCache存在的問題

分布式服務中,緩存map可能不起作用:緩存存入機器1,下一次查詢查詢機器2,查不到緩存

7、解決1-使用redis分布式緩存

1)引入pom依賴

<!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-redis -->
<dependency><groupId>org.mybatis.caches</groupId><artifactId>mybatis-redis</artifactId><version>1.0.0-beta2</version>
</dependency>

2)指定緩存Cache接口具體實現類

@CacheNamespace(implementation = RedisCache.class)
public interface MyMapper{}

3)啟動類指定緩存生效

@EnableCaching
public class ApplicationLoader{
}
  • resources目錄下配置redis.properties連接信息文件

    名稱必須使用這個,因為RedisCache讀取用戶自定義的配置文件,名稱默認就是這個

4)RedisCache源碼分析

4.1)加載配置信息

  • 時機:在SpringBoot加載mybatis的時候,會去加載RedisCache
  • RedisCache會調用構造方法,創建jedisPool
public RedisCache(String id) {this.id = id;// 1.parseConfiguration方法會去解析用戶自定義的redis.properteis配置信息(沒有此文件,則使用默認配置localhost的redis),并new RedisConfigRedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration();// 2.創建JedisPool poolpool = new JedisPool(redisConfig, redisConfig.getHost(), redisConfig.getPort(), redisConfig.getConnectionTimeout(), redisConfig.getSoTimeout(), redisConfig.getPassword(), redisConfig.getDatabase(), redisConfig.getClientName());
}

4.2)存

  • putObject -->> jedis.hset(key,序列化val)

  • 存儲數據結構是hash

  • val內容

    不是具體的DO對象,而是被序列化的數據內容(一級緩存存的是具體的DO對象)

4.3)取

  • getObject -->> jedis.hget將取出來的val再反序列化
  • 序列化和反序列化工具:SerializeUtil

8、解決方式2:ehcache

9、解決方式3:自定義實現類,實現了Cache接口

無論哪種緩存,本質都是要實現Cache接口

5.8 懶加載

1、背景

用戶A有100條訂單信息

1)懶加載(延時加載)

當查詢用戶信息時,暫時用不到訂單信息,那么只需要查出來用戶信息即可,等什么時候用到訂單信息了(主動user.getOrderList()),什么時候再去查用戶對應的訂單信息

2)立即加載

查詢出來的訂單信息,一般都需要知道其對應的用戶信息,所以查詢訂單信息時,應該立即也把對應的用戶信息查詢出來,即立即加載

2、場景

1:N時,一般建議延時加載

  • 根據手機號查詢用戶的信息

N:1時,建議立即加載

  • 根據訂單號查詢出訂單信息
  • 訂單信息中有用戶手機號
  • 立即根據手機號查詢對應的用戶信息

1:1時,都是立即加載

3、查詢方式

延時加載底層是既有嵌套查詢實現的,Mybatis默認是立即加載

  • application.properties全局配置
mybatis.configuration.lazy-loading-enabled=false  
  • Mapper.xml局部配置
<resultMap id="BaseResultMap" type="com.mjp.mysql.entity.UserPO"><association property="" fetchType="lazy"></association>
</>

嵌套查詢,即子查詢。將聯合查詢join on 分為多次查詢

4、實現原理

底層是基于動態代理

1)立即加載情況下

UserPO result = userPOMapper.selectByExample(xxx);

返回正常的UserPO對象

2)懶加載情況下

  • 返回的是UserPO代理對象$Proxy
  • 當執行result.getOrderList()時,會調用Proxy的攔截器的invoke,再執行一次查詢訂單信息

然后把訂單信息set給userPO

5、注意事項

mysql操作手冊,一般不允許子查詢。都是單表查詢結果之后,在內存做完邏輯之后,再發起一次單表查詢。

除非聯合查詢索引性能比較好,一般都是單表查詢。

所以,這里不對懶加載做過多闡述


5.9 Configuration

1、內容

將mybatis-config.xml 、application.properties、Mapper.xml等配置文件內容,設置到 org.apache.ibatis.session.Configuration 對象屬性中。

通過Configuration 對象,可以獲取所有和Mybatis相關的配置信息

  • DataSource
  • Mapper接口、Mapper.xml
  • 等等

2、使用

public class Xxxx implements ApplicationContextAware{private ApplicationContext applicationContext;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.applicationContext = applicationContext;}public void func(){// 從Spring容器中獲取sqlSessionFactorySqlSessionFactory sqlSessionFactory = (SqlSessionFactory)applicationContext.getBean("sqlSessionFactory");// 從sqlSessionFactory中獲取configuration配置對象Configuration configuration = sqlSessionFactory.getConfiguration();// 獲取配置的各種屬性值boolean lazyLoadingEnabled = configuration.isLazyLoadingEnabled();}
}

其它屬性的獲取,可以參考官網:Mybatis3.5.16中文文檔中的XML配置


5.10 插件

5.10.1 四大組件

Executor

1、作用

執行器,執行CRUD操作

2、默認實現類

CacheExecutor

3、創建實現類:Configuration#newExecutor

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 {// 1.默認實現類SimpleExecutor(外層又被包裝為CacheExecutor)executor = new SimpleExecutor(this, transaction);}if (cacheEnabled) {executor = new CachingExecutor(executor);}// 2.攔截器鏈executor = (Executor) interceptorChain.pluginAll(executor);return executor;
}
StatementHandler

1、作用

sql語法構建器,完成sql預編譯(PrepareStatement)

2、默認實現類

RoutingStatementHandler

3、創建實現類:Configuration#newStatementHandler

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {// 1.創建默認實現類RoutingStatementHandlerStatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);// 2:攔截器鏈statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);return statementHandler;
}
ParamterHandler

1、作用

參數處理器,完成參數解析和設置

2、默認實現類

DefaultParamterHandler

3、創建實現類:Configuration#newParameterHandler

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {// 1.創建默認實現類DefaultParameterHandlerParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);// 2.攔截器鏈parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);return parameterHandler;
}
ResultSetHandler

1、作用

結果集處理器,處理返回結果

2、默認實現類DefaultResultSetHandler

3、創建實現類:Configuration#newResultSetHandler

public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,ResultHandler resultHandler, BoundSql boundSql) {// 1.創建默認實現類DefaultResultSetHandlerResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);// 2.攔截器鏈resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);return resultSetHandler;
}

5.10.2 四大組件代理對象

1、組件代理對象

上述Configuration#newXxx四大組件的方法中,都含有攔截器鏈

組件代理對象 = interceptorChain.pluginAll(原組件);

創建的不是Xxx組件,而是組件的Jdk動態代理對象

2、增強

所以,后續在執行組件的方法時,不是直接調用組件的方法,而是調用的$Proxy#方法


5.10.3 目標方法

允許攔截的方法

1)Executor:update、query、commit、rollback

2)StatementHandler:prepare、parameterize、batch、update、query、getBoundSql、getParameterHandler

3)ParamterHandler:getParameterObject、setParameters

4)ResultSetHandler:handleResultSets、handleCursorResultSets、handleOutputParameters


5.10.4 自定義插件

可參考我另一篇:Mybatis自定義explain插件

1.對于單mybatis項目

  • 先自定義攔截器:MyInterceptor

  • 再在mybatis-config.xml中配置攔截器

<plugins><plugin interceptor="com.xxx.interceptor.MyInterceptor"></plugin>
</plugins>

2、SpringBoot整合mybatis的項目

1)自定義插件

實現的功能為:

  • 當執行sql查詢的時候,先執行explain sql查詢,判斷下用到的索引情況。
  • 如果使用的全文搜索,說明查詢效果很差,則進行攔截(拋異常 或 打印日志)。如果走到了索引查詢,則繼續執行SQL
// @Signature指定攔截哪個插件的哪個方法(args指定方法入參,因為有重載方法)
@Intercepts({@Signature(type = Executor.class,method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class MyInterceptor implements Interceptor {private Long longSqlTime;// invocation內含屬性目標對象、目標方法、目標方法參數數組@Overridepublic Object intercept(Invocation invocation) throws Throwable {System.out.println("begin");// 1.獲取目標方法的第一個參數MappedStatement ms = (MappedStatement) invocation.getArgs()[0];if (ms.getSqlCommandType() == SqlCommandType.SELECT) {// 2.獲取目標對象Executor executor = (Executor) invocation.getTarget();Configuration configuration = ms.getConfiguration();Object parameter = invocation.getArgs()[1];BoundSql boundSql = ms.getBoundSql(parameter);Connection connection = executor.getTransaction().getConnection();// 3.增強功能sqlExplain(configuration, ms, boundSql, connection, parameter);}// 4.target對象應執行的方法Object result = invocation.proceed();return result;}private void sqlExplain(Configuration configuration, MappedStatement mappedStatement, BoundSql boundSql, Connection connection, Object parameter) {// 這里注意:EXPLAIN后面必須要有空格,否則sql為: explainselect報錯StringBuilder explain = new StringBuilder("EXPLAIN ");String sqlExplain = explain.append(boundSql.getSql()).toString();StaticSqlSource sqlSource = new StaticSqlSource(configuration, sqlExplain, boundSql.getParameterMappings());MappedStatement.Builder builder = new MappedStatement.Builder(configuration, "explain_sql", sqlSource, SqlCommandType.SELECT);MappedStatement queryStatement = builder.build();builder.resultMaps(mappedStatement.getResultMaps()).resultSetType(mappedStatement.getResultSetType()).statementType(mappedStatement.getStatementType());DefaultParameterHandler handler = new DefaultParameterHandler(queryStatement, parameter, boundSql);try {PreparedStatement stmt = connection.prepareStatement(sqlExplain);handler.setParameters(stmt);ResultSet rs = stmt.executeQuery();while (rs.next()){String extra = rs.getString("Extra");int index = extra.indexOf("Using index");//判斷,是否Using indexif (index == -1){// index == -1表示沒有使用Using index,可能是Using where// 做對應的處理if (extra.contains("Using where")) {//}}//判斷,是否走到索引idx_ProName上if (!"idx_ProName".equals(rs.getString("key"))){// 異常}}} catch (SQLException e) {e.printStackTrace();}}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}// 獲取攔截器配置的參數@Overridepublic void setProperties(Properties properties) {Object val = properties.get("longSqlTime");this.longSqlTime = (Long) val;}
}

2)讓插件生效:即將自定義插件添加到連接器鏈interceptorChain中

@Configuration
public class MyBatisInterceptorConfig {// 方式一@Beanpublic MyInterceptor MyInterceptor() {MyInterceptor myInterceptor = new MyInterceptor();Properties properties = new Properties();properties.put("longSqlTime", 100L);myInterceptor.setProperties(properties);return myInterceptor;}// 方式二//@Bean//public ConfigurationCustomizer configurationCustomizer() {//return configuration -> configuration.addInterceptor(new MyInterceptor());//}
}

5.10.5 插件源碼分析

1、將自定義攔截器,添加到攔截器類的鏈中(這里以上述方式二為例)

1)configurationCustomizer

  • 模塊5.1.2 在創建SqlSessionFactory時,會先創建MybatisAutoConfig,此時會執行configurationCustomizer

2)configuration.addInterceptor

configuration.addInterceptor(new MyInterceptor())
  • 將自定義的插件攔截器MyInterceptor,添加到configuration的屬性interceptorChain攔截器鏈類
public void addInterceptor(Interceptor interceptor) {interceptorChain.addInterceptor(interceptor);
}

3)interceptorChain.addInterceptor

  • 將自定義攔截器,添加到interceptorChain類的List interceptors集合中
public void addInterceptor(Interceptor interceptor) {interceptors.add(interceptor);
}

此時InterceptorChain屬性interceptors中含有自定義攔截器MyInterceptor

2、創建CachingExecutor的Jdk動態代理對象$Proxy

在執行myMapper.selectByExample(example)時

SqlSession#openSession -->>openSessionFromDataSource -->>Executor executor = configuration.newExecutor(tx, execType) -->>	// 此處攔截器鏈會作用于原生的Executor組件,返回代理對象Executor executor = (Executor)this.interceptorChain.pluginAll(executor);

1)interceptorChain.pluginAll

List<Interceptor> interceptors = new ArrayList<>();//內含自定義攔截器public Object pluginAll(Object target) {for (Interceptor interceptor : interceptors) {target = interceptor.plugin(target);//方法入參target為CachingExecutor}return target;
}
  • 執行自定義攔截器MyInterceptor#plugin
@Override
public Object plugin(Object target) {return Plugin.wrap(target, this);
}
  • 執行Plugin.wrap(target, this)
    • target:CachingExecutor
    • this:MyInterceptor
public static Object wrap(Object target, Interceptor interceptor) {// 1.找到自定義攔截器MyInterceptor上的注解方法// 因為@Intercepts注解可以攔截多個組件,每個組件又可以攔截多個方法,所說是map// 我們自定義的攔截器MyInterceptor,只攔截Executor組件的query方法// 所以這里的map(key:Executor, val:query方法)Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);// 2.獲取目標類target的class類型:CachingExecutorClass<?> type = target.getClass();// 3.獲取CachingExecutor的接口ExecutorClass<?>[] interfaces = getAllInterfaces(type, signatureMap);// 4.如果目標類實現了接口,則使用Jdk動態代理創建代理類if (interfaces.length > 0) {return Proxy.newProxyInstance(type.getClassLoader(),interfaces,new Plugin(target, interceptor, signatureMap));}// 5.否則直接返回原目標類return target;
}
  • 使用JDK動態代理,為CachingExecutor目標類生成代理對象

    • interfaces:Executor接口
    • new Plugin(target, interceptor, signatureMap)
      • target:CachingExecutor目標類
      • interceptor:自定義攔截器MyInterceptor
      • signatureMap:要攔截的組件以及組件方法,Executor#query
    public class Plugin implements InvocationHandler {//實現了InvocationHandler接口private final Object target;//目標類private final Interceptor interceptor;//自定義攔截器private final Map<Class<?>, Set<Method>> signatureMap;// 攔截的組件以及其方法}
    
  • 代理對象$Proxy,簡略如下

public final class $Proxy1 extends Proxy implements Executor {private static Method m3;public $Proxy1(InvocationHandler var1) throws  {super(var1);}public final List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws  {return List<E>super.h.invoke(this, m3, new Object[]{var1,var2,var3,var4});}static {m3 = Class.forName("org.apache.ibatis.executor").getMethod("query",方法參數);}
}

3、執行代理對象方法

execute.query–>> $Proxy#query -->>super.h.invoke(this, m3, new Object[]{var1,var2,var3,var4})

1)執行Plugin#invoke

  • super:Proxy
  • super.h:Proxy.Plugin
return Proxy.newProxyInstance(type.getClassLoader(),interfaces,new Plugin(target, interceptor, signatureMap));
  • super.h.invoke:Plugin#invoke(this, m3, new Object[]{xxx})
    • this:$Proxy
    • m3:query
    • []:方法入參
public class Plugin implements InvocationHandler {private final Object target;//目標類CachingExecutorprivate final Interceptor interceptor;// 自定義攔截器MyInterceptorprivate final Map<Class<?>, Set<Method>> signatureMap; // 自定義攔截器攔截組件以及方法   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 1.獲取要攔截的方法(query)Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());// 2.如果目標方法method(query),在自定義攔截器要攔截的方法集合中(query),則執行攔截增強邏輯if (methods != null && methods.contains(method)) {return interceptor.intercept(new Invocation(target, method, args));}// 3.否則直接執行目標類的目標方法return method.invoke(target, args);}
}

2)執行自定義攔截器MyInterceptor#intercept(new Invocation(target, method, args))

new Invocation(target, method, args)

  • target:目標類CachingExecutor
  • 目標方法query
  • args目標方法的參數數組

MyInterceptor#intercept

	@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 1.前置增強// 2.執行目標類CachingExecutor的目標方法queryObject result = invocation.proceed();// 3.后置增強return result;}

3)增強方法執行完,執行目標類的目標方法invocation.proceed()

public Object proceed() throws InvocationTargetException, IllegalAccessException {return method.invoke(target, args);//即執行CachingExecutor#query(args)
}

5.10.6 分頁插件pageHelper

1、單mybatis項目中mybatis-config.xml

<plugins><plugin interceptor = "com.github.pagehelper.PageInterceptor"></plugin>
</plugins>

2、springboot整合mybatis,applicaiton.properties

1)pom依賴jar

<!-- pagehelper 分頁插件 -->
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>1.3.0</version>
</dependency><!-- 貌似不需要??? -->
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>5.2.0</version>
</dependency>

2)application.properties文件

#pagehelper分頁插件的數據庫類型配置,因為不同數據庫的方言不一樣,mysql和oracle的limit分頁就不一樣
#如果下面bean中注入了,這里就不需要了
pagehelper.helper-dialect=mysql 

3)注入Spring

@Configuration
public class PageHelperConfig {@Bean(name = "pageHelperPage")public Interceptor pageHelperInterceptor() {Properties props = new Properties();props.put("helperDialect", "mysql");//如果applicaiton.properties中指定了,這里就不需要了PageInterceptor interceptor = new PageInterceptor();interceptor.setProperties(props);return interceptor;}
}

4)使用

  • 方式一:查詢某一頁,展示多少條
PageHelper.startPage(1, 20);//第1頁,顯示20條數據(pageNum,pageSize)
List<MyDO> result = myMapper.selectIdGreaterThan(0);
PageInfo<User> pageInfo = new PageInfo<>(result);//返回1-20的數據
  • 方式二:offset、limit(推薦)
Page<Object> pageObject = PageHelper.offsetPage(20,20).doSelectPage(() -> myMapper.selectByIdGreaterThan(0));// 補充說明:這里可以直接通過long total = pageObject.getTotal();獲取滿足條件count(*)List<MyDO> result = pageObject.getResult().stream().map(MyDO.class::cast).collect(Collectors.toList());
  • 方式三:信息更豐富的pageInfo

上一頁、下一頁、是否為第一頁、是否為最后一頁等

5)源碼解析

  • 分頁插件攔截器PageInterceptor

3、m的springBoot整合mybatis項目,zeb.properties

zeb[0].pluginBeanNames=pageHelperPage

5.10.7 通用Mapper插件

0、背景

  • 如果不使用mybatisGenerator逆向工程,自動生成AutoMapper

  • 每個實體DO都對應一個Mapper接口,每個Mapper接口中都需要寫CRUD方法。但是實際上每個Mapper的CRUD方法,除了表名稱不一樣,sql內容基本都一樣

  • 這時,就可以使用通用Mapper插件。這樣我們每個Mapper接口就不需要再手動寫CRUD了

1、依賴jar

<!-- https://mvnrepository.com/artifact/tk.mybatis/mapper -->
<dependency><groupId>tk.mybatis</groupId><artifactId>mapper</artifactId><version>4.3.0</version>
</dependency>

2、自定義MyMapper

public interface MyMapper extends Mapper<FulfillAssessDO> {
}

3、自定義實體類MyDO

@Table(name = "fulfill_assess")
public class MyDO implements Serializable {/***   字段: id*   說明: 主鍵*/@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;// 其它屬性
}

4、使用

    @Resourceprivate MyMapper myMapper;@Testpublic void testMybatis() {Example example = new Example(MyDO.class);Example.Criteria criteria = example.createCriteria();criteria.andEqualTo("id",1);List<MyDO> list = myMapper.selectByExample(example);System.out.println(list);}

5、異常

tk.mybatis.mapper.MapperException: 無法獲取實體類MyDO對應的表名!

@MapperScan("com.xxx.mysql.mapper")//此注解使用tk的,不要使用ibatis的
啟動類

六、設計模式

6.1 Proxy代理模式

場景1

@Resource
private MyMapper myMapper;

Spring為myMapper接口生成MapperProxy代理對象。具體過程參考上文模塊5.1創建UserPOMapper

場景2:SqlSessionProxy

  • Mybatis中是使用DefaultSqlSession#selectOne
  • Spring整合Mybaits后,使用的SqlSessionProxy#selectOne
    • 完成了SqlSessionJdk動態代理增強,實現了線程安全
    • 執行DefaultSqlSession#selectOne

場景3:InterceptorChain

Executor、Statement、ParameteHandler、ResultSetHandler,被攔截器鏈增強,即Mybatis中的四大組件可以被自定義插件進行攔截(增強)

參考5.10


6.2 裝飾者模式

模式原理參考我另一篇:設計模式實戰

Mybatis二級緩存查詢

1、作用
給原始類SimpleExecutor增加緩存讀功能

2、實現

  • 裝飾器類(CachingExecutor)需要跟原始類(SimpleExecutor)繼承相同的抽象類(AbstractA)或 接口(Executor)

  • 裝飾器類(CachingExecutor)中組合原始類(SimpleExecutor)

  • 實現裝飾

Configuration#newExecutor

if (this.cacheEnabled) {executor = new CachingExecutor((Executor)executor);
}
public class CachingExecutor implements Executor {// 裝飾著類持有原始類SimpleExecutorprivate final Executor delegate;public CachingExecutor(Executor delegate) {this.delegate = delegate;delegate.setExecutorWrapper(this);}// 方法
}

3、裝飾方法

  • 接口Executor#query
<E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4, CacheKey var5, BoundSql var6);
  • 原始類SimpleExecutor#query

    這里直接繼承了其父類BaseExecutor的query方法

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) {// 業務
}
  • 裝飾器類CachingExecutor#query
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) {// 1.獲取緩存key:查詢Mapper的全路徑 + 方法名稱 + 其它等Cache cache = ms.getCache();// 2.裝飾增強功能!// 如果緩存key不為空,則嘗試走緩存查詢if (cache != null) {this.flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) {this.ensureNoOutParams(ms, boundSql);// 2.1嘗試查詢緩存,緩存中有則直接返回List<E> list = (List)this.tcm.getObject(cache, key);if (list == null) {// 2.2 緩存中沒有,則正常的走原始類的查詢方法// this.delegate即原始類SimpleExecutorlist = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);// 2.3 將查詢結果緩存this.tcm.putObject(cache, key, list);}return list;}}return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

6.3 構建者模式

二級緩存相關對象的創建,都是使用的構建者Build模式,然后在build方法中對構建出來的對象的屬性參數進行統一校驗

使用CacheBuilder創建Cache

使用MappedStatementBuilder創建MappedStatement

統一在在build方法中完成所有屬性校驗等邏輯


6.4 職責鏈模式

1、背景

Mybatis的二級緩存頂級接口是Cache,緩存具有的功能如圖5所示
在這里插入圖片描述

Mybatis沒有將緩存的功能都寫在一個Cache接口實現類中,然后提供所有功能方法。而是采取的:一個功能對應一個實現類的方式。然后通過有序職責鏈,將所有實現類串聯起來。

先執行同步-記錄-LRU算法-過期清理-放穿透-內存存儲

2、優點

職責鏈:可擴展

3、Mybatis的實現源碼

  • 正常情況下,我們在Spring中,通過@Order注解 + 接口實現類的List的方式,就可以實現有序的責任鏈;

  • 但這里Mybaits中沒有使用@Order注解指定序列的方式實現有序,而是使用裝飾者模式實現的有序:即在

MapperAnnotationBuilder#parseCache使用構建者模式創建Cache對象時:

Cache cache = (new CacheBuilder(this.currentNamespace))
// 先設置一個頂層的實現類,即職責鏈的頭
.implementation((Class)this.valueOrDefault(typeClass, PerpetualCache.class))
// 添加職責鏈
.addDecorator((Class)this.valueOrDefault(evictionClass, LruCache.class))
.build();

1)有序化

  • implementation:指定鏈頭org.apache.ibatis.cache.impl.PerpetualCache
  • build:有序化
1.即通過構造方法參數的形式,將下一個鏈LRU傳遞給當前鏈PerpetualCache
cache = this.newCacheDecoratorInstance(decorator, (Cache)cache);

setStandardDecorators:裝飾者模式(通過構造參數的方式,持有并表明下一個鏈)完成職責鏈的有序

if (this.clearInterval != null) {cache = new ScheduledCache((Cache)cache);((ScheduledCache)cache).setClearInterval(this.clearInterval);
}if (this.readWrite) {cache = new SerializedCache((Cache)cache);
}Cache cache = new LoggingCache((Cache)cache);
cache = new SynchronizedCache(cache);
if (this.blocking) {cache = new BlockingCache((Cache)cache);
}return (Cache)cache;

2)二級緩存查詢getObject邏輯

先調用SynchronizedCache#getObject

  • SynchronizedCache
public synchronized Object getObject(Object key) {return this.delegate.getObject(key);
}
  • LoggingCache
public Object getObject(Object key) {++this.requests;// 1.調用下一個職責鏈Object value = this.delegate.getObject(key);if (value != null) {++this.hits;}// 2.完成本身日志記錄功能if (this.log.isDebugEnabled()) {this.log.debug("Cache Hit Ratio [" + this.getId() + "]: " + this.getHitRatio());}return value;
}
  • SerializedCache
public Object getObject(Object key) {// 1.調用下一個職責鏈Object object = this.delegate.getObject(key);// 2.完成本身序列化功能return object == null ? null : this.deserialize((byte[])((byte[])object));
}
  • 依次到LRU -->> PerpetualCache最后查詢底層的HashMap

4、個人建議

可以使用以下方式

@Resource
private List<Cache> cache;+@Order(指定順序)
public CacheImpl implement Cache{}

具體實現,可參考我另一篇:設計模式


6.5 迭代器模式

參考5.6.1 PropertyTokenizer分詞器


6.6 簡單工廠模式

場景1:創建StatementHandler

public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {switch (ms.getStatementType()) {case STATEMENT:delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;case PREPARED:delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;case CALLABLE:delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;default:throw new ExecutorException("Unknown statement type: " + ms.getStatementType());}
}

場景2:創建Executor執行器newExecutor

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

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/17380.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/17380.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/17380.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

字符串函數(2)<C語言>

前言 快一周沒更博客了&#xff0c;最近有點忙&#xff0c;今天閑下來了&#xff0c;還是不行&#xff0c;繼續干&#xff0c;書接上回繼續介紹字符串函數&#xff1a;strncpy()、strncat()、strcmp()、strtok()使用、strstr()使用以及模擬實現、strerror()使用。 strncpy()、s…

blender serpens3 個人總結

Serpens 全節點個人備注 快捷鍵 &#xff1a;shift v&#xff1a; 從復制版 添加執行操作&#xff08;blender任何執行動作按鈕&#xff0c;右鍵可以獲取操作命令&#xff09; 概念分析&#xff1a; 屬性&#xff08;Properties&#xff09;&#xff1a;用于定義持久性數據…

揭秘網絡編程:同步與異步IO模型的實戰演練

摘要 ? 在網絡編程領域&#xff0c;同步(Synchronous)、異步(Asynchronous)、阻塞(Blocking)與非阻塞(Non-blocking)IO模型是核心概念。盡管這些概念在多篇文章中被廣泛討論&#xff0c;它們的抽象性使得徹底理解并非易事。本文旨在通過具體的實驗案例&#xff0c;將這些抽象…

在React中使用Sass實現Css樣式管理-10

0. 什么是Sass Sass(Syntactically Awesome Stylesheets)是一個 CSS 預處理器&#xff0c;是 CSS 擴展語言&#xff0c;可以幫助我們減少 CSS 重復的代碼&#xff0c;節省開發時間&#xff1a; Sass 引入合理的樣式復用機制&#xff0c;可以節約很多時間來重復。支持變量和函…

【HM】簡單說明白:裝飾器@State、@Prop、@Link、@Provide、@Consume修飾變量,@Watch監聽變量狀態發生變化

首先要明白什么是“狀態變量”&#xff1f;即被狀態裝飾器&#xff08;State、Prop、Link、Provide、Consume&#xff09;修飾的變量&#xff0c;比如 State str : string; str就是狀態變量。狀態變量值的改變會引起UI界面重新渲染。 State State裝飾的變量&#xff0c;是私…

C++之“流”-第2課-C++和C標準輸入輸出同步

為什么C和C的標準輸入輸出不同步時&#xff0c;數據會混亂&#xff1f;同步會帶來多大性能損失&#xff1f;為什么說這個損失通常不用太在乎&#xff1f; 0. 課堂視頻 C之“流”-第2課&#xff1a;和C輸入輸出的同步 1. 理解cin和cout的類型與創建過程 std::cout 是std::ostre…

Ubuntu系統Discover軟件中心簡介

Discover軟件中心是Ubuntu操作系統中默認的軟件管理工具&#xff0c;它提供了一個圖形用戶界面(GUI)來幫助用戶瀏覽、搜索、安裝和卸載軟件包。Discover軟件中心是Ubuntu軟件中心(Ubuntu Software Center)的繼承者&#xff0c;它在Ubuntu 16.04 LTS版本中首次被引入&#xff0c…

添加、修改和刪除字典元素

自學python如何成為大佬(目錄):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 由于字典是可變序列&#xff0c;所以可以隨時在字典中添加“鍵-值對”。向字典中添加元素的語法格式如下&#xff1a; dictionary[key] value 參數…

You don‘t have enough free space或者no space left on device異常

1.磁盤空間不足 Linux安裝軟件顯示 You dont have enough free space 或者docker拉鏡像時&#xff0c;出現磁盤空間不足的情況 no space left on device 如果你是ubuntu系統。查看磁盤空間 df -h 多半是這個目錄滿了/dev/mapper/ubuntu--vg-ubuntu--lv 大多情況我們只希望擴…

學習編程對英語要求高嗎?

學習編程并不一定需要高深的英語水平。我這里有一套編程入門教程&#xff0c;不僅包含了詳細的視頻講解&#xff0c;項目實戰。如果你渴望學習編程&#xff0c;不妨點個關注&#xff0c;給個評論222&#xff0c;私信22&#xff0c;我在后臺發給你。 雖然一些編程資源和文檔可能…

typora自動生成標題序號(修改V1.0)

目錄 帶序號效果圖 解決方法 帶序號效果圖 解決方法 1.進入文件夾&#xff1a;文件–>偏好設置–>外觀–>主題–>打開主題文件夾 2.如果沒有base.user.css文件&#xff0c;新建一個。如果有直接用記事本打開&#xff0c;把下面代碼拷貝進去保存。 /** initiali…

【JUC編程】-多線程和CompletableFuture的使用

多線程編程 文章目錄 多線程編程[toc]引言創建多線程的方式繼承Thread類實現Runnable接口實現Callable接口Callable和Runnable的區別 Lambda表達式 線程的實現原理Future&FutureTask具體使用submit方法Future到FutureTask類Future注意事項局限性 CompletionService引言使用…

第八大奇跡

目錄 題目描述 輸入描述 輸出描述 輸入輸出樣例 示例 輸入 輸出 運行限制 原題鏈接 代碼思路 題目描述 在一條 R 河流域&#xff0c;繁衍著一個古老的名族 Z。他們世代沿河而居&#xff0c;也在河邊發展出了璀璨的文明。 Z 族在 R 河沿岸修建了很多建筑&#xff0c…

java如何向數組中插入元素

java的數組是不可改變的&#xff0c;因此如果要向數組中插入新的元素&#xff0c;需要新建一個數組&#xff0c;新的數組元素個數減去老數組元素個數的差大于等于要插入新的元素數量。 假如說要插入一個數組元素&#xff0c;需要把新元素插入到中間&#xff0c;把新的數組分為…

Vue組件通訊?組件中通過 provide 來提供變量,然后在?組件中通過 inject 來注?變量例子

在Vue中&#xff0c;provide 和 inject 主要用于依賴注入&#xff0c;允許祖先組件向其所有子孫組件提供一個依賴&#xff0c;而不論組件層次有多深。這在開發高階插件/組件庫時特別有用。 以下是一個簡單的例子&#xff0c;演示了如何在父組件中使用 provide 提供變量&#x…

軟件測試面試題(八)

一&#xff1a;TestDirector有哪些功能&#xff0c;如何對軟件測試過程進行管理&#xff1f; 需求管理 定義測試范圍 定義需求樹 描述需求樹的功能點 測試計劃 定義測試目標和測試策略 分解應用程序&#xff0c;建立測試計劃樹 確定每個功能點的測試方法 將每個功能點連接…

Ps 濾鏡:消失點

Ps菜單&#xff1a;濾鏡/消失點 Filter/Vanishing Point 快捷鍵&#xff1a;Ctrl Alt V 兩條平行的鐵軌或兩排樹木連線相交于很遠很遠的某一點&#xff0c;這點在透視圖中叫做“消失點”&#xff0c;也稱為“滅點”。 消失點 Vanishing Point濾鏡主要用于在圖像中處理具有透視…

C++入門3——類與對象(2)

1.類的6個默認成員函數 如果一個類中什么成員都沒有&#xff0c;簡稱為空類。可是空類中真的什么都沒有嗎&#xff1f; 其實并不是的&#xff0c;任何類在什么都不寫時&#xff0c;編譯器會自動生成以下6個默認成員函數。 默認成員函數&#xff1a;用戶沒有顯式實現&#xf…

libmodbus開發庫介紹

目錄 功能概要源碼獲取源碼內容結構源碼與移植 功能概要 libmodbus是一個免費的跨平臺支持RTU和TCP的Modbus庫&#xff0c;遵循LGPL V2.1協議。libmodbus支持Linux、Mac Os X、FreeBSD、QNX和Windows等操作系統。libmodbus可以向符合Modbus協議的設備發送和接收數據&#xff0…

vector的reverse和resize區別

一 代碼 #include "stdafx.h" #include <iostream> #include <vector> using namespace std;class TEST{ public:TEST(){std::cout << "construct t" << std::endl;} };int main() {std::cout << "hello,world" …