本文作者:孔維勝,叩丁狼高級(jí)講師。原創(chuàng)文章,轉(zhuǎn)載請(qǐng)注明出處。
MyBatis中通過(guò)package標(biāo)簽加載mapper映射文件的方式分析
看文章前的要求
在學(xué)習(xí)MyBatis的初級(jí)篇之前,有兩個(gè)前提要求,第一.必須學(xué)會(huì)使用IDEA,因?yàn)樵谖恼轮校褂玫墓ぞ邽镮DEA,文章中的案例也都是基于IDEA的。第二.必須學(xué)會(huì)使用MAVEN,因?yàn)樵诎咐行枰膉ar包,都是通過(guò)MAVEN來(lái)管理的。
文章中的案例的開發(fā)環(huán)境
JDK 1.8
IDEA 2017.3
MySQL 5.1.38
Apache Maven 3.5.0
Tomcat 9.0.6
MyBatis 3.4.6
案例需要的表和數(shù)據(jù)
我們使用MyBatis的目的最終是訪問(wèn)數(shù)據(jù)庫(kù),所以在數(shù)據(jù)庫(kù)方面,我們先創(chuàng)建相應(yīng)的數(shù)據(jù)庫(kù),表,導(dǎo)入相關(guān)的數(shù)據(jù)。如:
1.創(chuàng)建mybatis數(shù)據(jù)庫(kù)。
2.在mybatis數(shù)據(jù)庫(kù)中創(chuàng)建department(部門表)。
DROP TABLE IF EXISTS `department`;
CREATE TABLE `department` (
`id` bigint(10) NOT NULL AUTO_INCREMENT COMMENT '部門ID',
`name` varchar(20) DEFAULT NULL COMMENT '部門名稱',
`sn` varchar(20) DEFAULT NULL COMMENT '部門縮寫',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
3.準(zhǔn)備department(部門表相關(guān)的數(shù)據(jù))
INSERT INTO `department` VALUES (1, '人力資源', 'HR_DEPT');
INSERT INTO `department` VALUES (2, '銷售部', 'SALE_DEPT');
INSERT INTO `department` VALUES (3, '開發(fā)部', 'DEVELOP_DEPT');
INSERT INTO `department` VALUES (4, '財(cái)務(wù)部', 'FINANCE_DEPT');
案例需求
需求:使用Mapper動(dòng)態(tài)代理的方式完成所有數(shù)據(jù)的查詢操作。
需求分析
- 導(dǎo)入相關(guān)jar依賴
要使用MyBatis框架,首先需要導(dǎo)入mybatis的核心包,MyBatis主要是操作數(shù)據(jù)庫(kù),替換掉傳統(tǒng)的JDBC方式訪問(wèn)數(shù)據(jù)庫(kù),所以需要導(dǎo)入mysql的驅(qū)動(dòng)包。我們要在項(xiàng)目中使用單元測(cè)試進(jìn)行測(cè)試,所以需要導(dǎo)入junit包,我們不想寫javaBean的setter和getter方法,可以導(dǎo)入lombok的包。
- 2.添加配置文件。
我們使用MyBatis框架,需要兩個(gè)配置文件,一個(gè)是MyBatis的主配置文件,主要用來(lái)配置事務(wù)管理器和數(shù)據(jù)庫(kù)的連接信息,一個(gè)是封裝SQL語(yǔ)句Mapper映射文件。我們?yōu)榱藬?shù)據(jù)庫(kù)的連接信息不寫死在主配置文件中,所以我們采用抽取的方式,把連接數(shù)據(jù)庫(kù)的信息抽取到db.properties文件中,進(jìn)行管理。通過(guò)package掃描的方式在主配置文件中掛載mapper的文件。如:
<package name="cn.wolfcode.mapper"/>
- 3.添加實(shí)體類和接口。
可能查詢數(shù)據(jù)需要查詢條件有很多,查詢數(shù)據(jù)需要封裝到對(duì)象中,所以我們可以定義一個(gè)JavaBean,來(lái)封裝條件和查詢的數(shù)據(jù)。
定義一個(gè)接口,編寫操作數(shù)據(jù)庫(kù)方法。方法的名字保持和sql映射文件中的標(biāo)簽的id一一對(duì)應(yīng)。
- 4.增加工具類。
通過(guò)加載主配置文件來(lái)獲取SqlSessionFactory工廠對(duì)象,一般工廠對(duì)象都是單例模式的,所以這個(gè)操作只需要做一次即可。比如:我們不能每吃一次飯,都去建一所餐廳。兩者的道理是一樣的。
而在MyBatis的官網(wǎng)給出的建議是SqlSessionFactory 一旦被創(chuàng)建就應(yīng)該在應(yīng)用的運(yùn)行期間一直存在,沒有任何理由對(duì)它進(jìn)行清除或重建。因此 SqlSessionFactory 應(yīng)該使用其單例模式,只創(chuàng)建一次在整個(gè)應(yīng)用中,都可以使用。
工廠對(duì)象的獲取思考:
那么把獲取工廠對(duì)象的操作放在哪里合適呢?如果在本類中進(jìn)行抽取,放在一個(gè)方法中,但是每個(gè)DAO的實(shí)現(xiàn)類都這樣處理,還是會(huì)出現(xiàn)代碼的冗余。所以最合適的方式定義一個(gè)MyBatisUtil工具類,把獲取工廠對(duì)象的操作抽取到工具類中,那么工廠對(duì)象的獲取只需要獲取一次即可,所以在工具類中,定義在哪里,只會(huì)執(zhí)行一次呢?靜態(tài)代碼塊,我們都知道,類中的靜態(tài)代碼塊,只會(huì)隨著類的加載而加載,并且只執(zhí)行一次。
MyBatis工具類設(shè)計(jì)思考:
何為工具類,一般我們?cè)诙x的工具類的時(shí)候,希望使用者只使用而不要修改此類,所以我們會(huì)設(shè)置這個(gè)類使用final進(jìn)行修飾,這樣這個(gè)類就是終結(jié)類,不能被繼承。一般工具類不會(huì)讓使用者去創(chuàng)建對(duì)象,而是采用提供靜態(tài)方法的方式共使用者調(diào)用。
SqlSession對(duì)象獲取思考:
定義一個(gè)方法供外部訪問(wèn),獲取SqlSession對(duì)象。這個(gè)方法設(shè)計(jì)成靜態(tài)的這樣,調(diào)用方法的時(shí)候不用再創(chuàng)建工具類對(duì)象。
- 5.添加測(cè)試類。
定義一個(gè)測(cè)試類,編寫一個(gè)測(cè)試方法,通過(guò)調(diào)用工具類中的方法獲取SqlSession對(duì)象,通過(guò)SqlSession對(duì)象調(diào)用getMapper方法獲取對(duì)應(yīng)的Mapper的代理對(duì)象,然后調(diào)用接口中的方法獲取所有數(shù)據(jù)。
案例代碼
pom.xml:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.40</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
</dependency>
mybatis-config.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="db.properties"/>
<typeAliases>
<package name="cn.wolfcode.domain"/>
</typeAliases>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driverName}"/>
<property name="url" value="${url}"/>
<property name="username" value="${userName}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<package name="cn.wolfcode.mapper"/>
</mappers>
</configuration>
db.properties:
driverName=com.mysql.jdbc.Driver
url=jdbc:mysql:///mybatis
userName=root
password=root123
DepartmentMapper.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="cn.wolfcode.mapper.DepartmetMapper">
<!--
select 表示查詢語(yǔ)句的標(biāo)簽。標(biāo)簽體的內(nèi)容即是查詢的SQL語(yǔ)句
id:SQL語(yǔ)句的唯一標(biāo)識(shí)
parameterType:傳入這條SQL語(yǔ)句的參數(shù)的類的完全限定名或別名,
因?yàn)?MyBatis 可以通過(guò) TypeHandler 推斷出具體傳入語(yǔ)句的參數(shù),故可以省略
resultType:返回期望的類型(類的完全限定名或別名),用來(lái)接收查詢的結(jié)果。
-->
<select id="selectAll" resultType="cn.wolfcode.domain.Department">
SELECT id,name,sn FROM department
WHERE id = #{id}
</select>
</mapper>
Department:
@Getter
@Setter
@ToString
public class Department {
// 主鍵id
private Long id;
// 部門名稱
private String name;
// 部門簡(jiǎn)寫
private String sn;
}
DepartmentMapper:
public interface DepartmentMapper {
/**
* 查詢所有部門信息
* @return 返回所有部門信息的集合
*/
List<Department> selectAll();
}
MyBatisUtil:
public final class MyBatisUtil {
private static SqlSessionFactory factory = null;
static {
// 使用static靜態(tài)代碼塊,隨著類的加載而加載,只執(zhí)行一次
try {
// 加載MyBatis的主配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 通過(guò)構(gòu)建器(SqlSessionFactoryBuilder)構(gòu)建一個(gè)SqlSessionFactory工廠對(duì)象
factory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (Exception e) {
e.printStackTrace();
}
}
// 獲取sqlSession對(duì)象
public static SqlSession openSession() {
return factory.openSession();
}
}
DepartmentMapperTest:
public class DepartmentMapperTest {
@Test
public void testQueryOne(){
// 獲取sqlSession對(duì)象
SqlSession sqlSession = MyBatisUtil.openSession();
// 獲取Mapper對(duì)象
DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
List<Department> departmentList = departmentMapper.selectAll();
// 關(guān)閉資源
sqlSession.close();
// 遍歷結(jié)果
departmentList.stream().forEach(System.out::println);
}
DepartmentMapper文件加載流程分析
- 1 . 加載主配置文件,通過(guò)build方法構(gòu)建工廠對(duì)象。如:
MyBatisUtil.png
-
2 . 創(chuàng)建XML配置構(gòu)建器的對(duì)象(XMLConfigBuilder)。底層使用的是XPath解析器。 在這個(gè)方法的finally塊中,對(duì)外部傳入的流,進(jìn)行了關(guān)閉。所以外部不需要進(jìn)行關(guān)閉了。如:
SqlSessionFactoryBuilder.png
-
3 . 通過(guò)構(gòu)建器對(duì)象調(diào)用parse方法,把解析的數(shù)據(jù)封裝到Configuration對(duì)象中。我們主要是關(guān)心mapper文件的加載,所以繼續(xù)往下看。如:
XMLConfigBuilder.png
- 4 .方法中定義了一個(gè)while死循環(huán),主要是便利mappers節(jié)點(diǎn)下面的所有元素。因?yàn)槲覀儾捎玫氖窃谥髋渲梦募惺褂胮ackage掃描的方式掛載的mapper映射文件。所以跳入if代碼塊。在if塊中通過(guò)獲取name屬性的值,拿到了mapper文件的所屬的包名,通過(guò)configuration對(duì)象調(diào)用addMappers方法把mapper映射文件所在的包傳入。如:

- 5 .調(diào)用Mapper注冊(cè)對(duì)象中(MapperRegistry)的addMappers方法,添加映射。如:

- 6 .創(chuàng)建ResolverUtil工具類,通過(guò)調(diào)用find方法把包下面的字節(jié)碼對(duì)象找出來(lái),并存入到Set集合中,通過(guò)調(diào)用getClasses方法取出,進(jìn)行遍歷。把每一個(gè)字節(jié)碼對(duì)象傳入addMapper方法。如:

- 7 .在MapperRegistry(映射注冊(cè)類)中定義一個(gè)map容器(knowMappers),用來(lái)存入映射。在addMapper方法中,先通過(guò)調(diào)用isInterface方法看看mapper是不是接口,必須是接口,才會(huì)添加。在通過(guò)調(diào)用hasMapper方法來(lái)判斷是否已經(jīng)添加過(guò)了,如果已經(jīng)添加,就拋出一個(gè)綁定異常。通過(guò)標(biāo)記loadCompleted,來(lái)確保添加成功。如果添加出現(xiàn)了異常,在finally塊中刪除map中存入的映射。把字節(jié)碼對(duì)象作為key,創(chuàng)建該字節(jié)碼對(duì)象的代理對(duì)象作為value,存入knowMappers中。并創(chuàng)建MapperAnnotationBuilder對(duì)象如:

- 8 .MapperAnnotationBuilder這個(gè)類總會(huì)優(yōu)先解析xml配置文件,并且這個(gè)xml配置文件必須與Class對(duì)象所在的包路徑一致,且文件名要與類名一致。在解析完xml配置文件后,才會(huì)開始解析Class對(duì)象中包含的注解。里面有個(gè)if判斷,如果在主配置對(duì)象(configuration)添加過(guò)接口標(biāo)記,表示解析過(guò),就不再進(jìn)入if語(yǔ)句。首先調(diào)用loadXmlResource方法,解析指定的xml配置文件。如:

- 9 . 在這個(gè)方法中,先通過(guò)if判斷之前是否解析過(guò),如果沒有解析過(guò),則進(jìn)入if語(yǔ)句,把包名中的"."替換成"/",這樣變成了文件夾,然后在后面追加".xml"后綴。這樣拼接成一個(gè)xml文件的資源路徑。然后加載到內(nèi)存。在通過(guò)調(diào)用parse方法進(jìn)行解析xml文件。
所以這也是為何如果使用package掃描的方式,必須要保證接口和mapper映射文件必須在同一個(gè)包中,名字也必須相同的原因。如:

- 10 . 繼續(xù)往下解析。如:

DepartmentMapper文件加載整體流程圖

想獲取更多技術(shù)干貨,請(qǐng)前往叩丁狼官網(wǎng):http://www.wolfcode.cn/all_article.html


