MyBatis中通过package标签加载mapper映射文件的
本文作者:孔维胜,叩丁狼高级讲师。原创文章,转载请注明出处。
MyBatis中通过package标签加载mapper映射文件的方式分析
看文章前的要求
在学习MyBatis的初级篇之前,有两个前提要求,第一.必须学会使用IDEA,因为在文章中,使用的工具为IDEA,文章中的案例也都是基于IDEA的。第二.必须学会使用MAVEN,因为在案例中需要的jar包,都是通过MAVEN来管理的。
文章中的案例的开发环境
JDK 1.8
IDEA 2017.3
MySQL 5.1.38
Apache Maven 3.5.0
Tomcat 9.0.6
MyBatis 3.4.6
案例需要的表和数据
我们使用MyBatis的目的最终是访问数据库,所以在数据库方面,我们先创建相应的数据库,表,导入相关的数据。如:
1.创建mybatis数据库。
2.在mybatis数据库中创建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.准备department(部门表相关的数据)
INSERT INTO `department` VALUES (1, '人力资源', 'HR_DEPT');
INSERT INTO `department` VALUES (2, '销售部', 'SALE_DEPT');
INSERT INTO `department` VALUES (3, '开发部', 'DEVELOP_DEPT');
INSERT INTO `department` VALUES (4, '财务部', 'FINANCE_DEPT');
案例需求
需求:使用Mapper动态代理的方式完成所有数据的查询操作。
需求分析
- 导入相关jar依赖
要使用MyBatis框架,首先需要导入mybatis的核心包,MyBatis主要是操作数据库,替换掉传统的JDBC方式访问数据库,所以需要导入mysql的驱动包。我们要在项目中使用单元测试进行测试,所以需要导入junit包,我们不想写javaBean的setter和getter方法,可以导入lombok的包。
- 2.添加配置文件。
我们使用MyBatis框架,需要两个配置文件,一个是MyBatis的主配置文件,主要用来配置事务管理器和数据库的连接信息,一个是封装SQL语句Mapper映射文件。我们为了数据库的连接信息不写死在主配置文件中,所以我们采用抽取的方式,把连接数据库的信息抽取到db.properties文件中,进行管理。通过package扫描的方式在主配置文件中挂载mapper的文件。如:
<package name="cn.wolfcode.mapper"/>
- 3.添加实体类和接口。
可能查询数据需要查询条件有很多,查询数据需要封装到对象中,所以我们可以定义一个JavaBean,来封装条件和查询的数据。
定义一个接口,编写操作数据库方法。方法的名字保持和sql映射文件中的标签的id一一对应。
- 4.增加工具类。
通过加载主配置文件来获取SqlSessionFactory工厂对象,一般工厂对象都是单例模式的,所以这个操作只需要做一次即可。比如:我们不能每吃一次饭,都去建一所餐厅。两者的道理是一样的。
而在MyBatis的官网给出的建议是SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由对它进行清除或重建。因此 SqlSessionFactory 应该使用其单例模式,只创建一次在整个应用中,都可以使用。
工厂对象的获取思考:
那么把获取工厂对象的操作放在哪里合适呢?如果在本类中进行抽取,放在一个方法中,但是每个DAO的实现类都这样处理,还是会出现代码的冗余。所以最合适的方式定义一个MyBatisUtil工具类,把获取工厂对象的操作抽取到工具类中,那么工厂对象的获取只需要获取一次即可,所以在工具类中,定义在哪里,只会执行一次呢?静态代码块,我们都知道,类中的静态代码块,只会随着类的加载而加载,并且只执行一次。
MyBatis工具类设计思考:
何为工具类,一般我们在定义的工具类的时候,希望使用者只使用而不要修改此类,所以我们会设置这个类使用final进行修饰,这样这个类就是终结类,不能被继承。一般工具类不会让使用者去创建对象,而是采用提供静态方法的方式共使用者调用。
SqlSession对象获取思考:
定义一个方法供外部访问,获取SqlSession对象。这个方法设计成静态的这样,调用方法的时候不用再创建工具类对象。
- 5.添加测试类。
定义一个测试类,编写一个测试方法,通过调用工具类中的方法获取SqlSession对象,通过SqlSession对象调用getMapper方法获取对应的Mapper的代理对象,然后调用接口中的方法获取所有数据。
案例代码
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 表示查询语句的标签。标签体的内容即是查询的SQL语句
id:SQL语句的唯一标识
parameterType:传入这条SQL语句的参数的类的完全限定名或别名,
因为 MyBatis 可以通过 TypeHandler 推断出具体传入语句的参数,故可以省略
resultType:返回期望的类型(类的完全限定名或别名),用来接收查询的结果。
-->
<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;
// 部门简写
private String sn;
}
DepartmentMapper:
public interface DepartmentMapper {
/**
* 查询所有部门信息
* @return 返回所有部门信息的集合
*/
List<Department> selectAll();
}
MyBatisUtil:
public final class MyBatisUtil {
private static SqlSessionFactory factory = null;
static {
// 使用static静态代码块,随着类的加载而加载,只执行一次
try {
// 加载MyBatis的主配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 通过构建器(SqlSessionFactoryBuilder)构建一个SqlSessionFactory工厂对象
factory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (Exception e) {
e.printStackTrace();
}
}
// 获取sqlSession对象
public static SqlSession openSession() {
return factory.openSession();
}
}
DepartmentMapperTest:
public class DepartmentMapperTest {
@Test
public void testQueryOne(){
// 获取sqlSession对象
SqlSession sqlSession = MyBatisUtil.openSession();
// 获取Mapper对象
DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
List<Department> departmentList = departmentMapper.selectAll();
// 关闭资源
sqlSession.close();
// 遍历结果
departmentList.stream().forEach(System.out::println);
}
DepartmentMapper文件加载流程分析
- 1 . 加载主配置文件,通过build方法构建工厂对象。如:
MyBatisUtil.png
-
2 . 创建XML配置构建器的对象(XMLConfigBuilder)。底层使用的是XPath解析器。 在这个方法的finally块中,对外部传入的流,进行了关闭。所以外部不需要进行关闭了。如:
SqlSessionFactoryBuilder.png
-
3 . 通过构建器对象调用parse方法,把解析的数据封装到Configuration对象中。我们主要是关心mapper文件的加载,所以继续往下看。如:
XMLConfigBuilder.png
- 4 .方法中定义了一个while死循环,主要是便利mappers节点下面的所有元素。因为我们采用的是在主配置文件中使用package扫描的方式挂载的mapper映射文件。所以跳入if代码块。在if块中通过获取name属性的值,拿到了mapper文件的所属的包名,通过configuration对象调用addMappers方法把mapper映射文件所在的包传入。如:
- 5 .调用Mapper注册对象中(MapperRegistry)的addMappers方法,添加映射。如:
- 6 .创建ResolverUtil工具类,通过调用find方法把包下面的字节码对象找出来,并存入到Set集合中,通过调用getClasses方法取出,进行遍历。把每一个字节码对象传入addMapper方法。如:
- 7 .在MapperRegistry(映射注册类)中定义一个map容器(knowMappers),用来存入映射。在addMapper方法中,先通过调用isInterface方法看看mapper是不是接口,必须是接口,才会添加。在通过调用hasMapper方法来判断是否已经添加过了,如果已经添加,就抛出一个绑定异常。通过标记loadCompleted,来确保添加成功。如果添加出现了异常,在finally块中删除map中存入的映射。把字节码对象作为key,创建该字节码对象的代理对象作为value,存入knowMappers中。并创建MapperAnnotationBuilder对象如:
- 8 .MapperAnnotationBuilder这个类总会优先解析xml配置文件,并且这个xml配置文件必须与Class对象所在的包路径一致,且文件名要与类名一致。在解析完xml配置文件后,才会开始解析Class对象中包含的注解。里面有个if判断,如果在主配置对象(configuration)添加过接口标记,表示解析过,就不再进入if语句。首先调用loadXmlResource方法,解析指定的xml配置文件。如:
- 9 . 在这个方法中,先通过if判断之前是否解析过,如果没有解析过,则进入if语句,把包名中的"."替换成"/",这样变成了文件夹,然后在后面追加".xml"后缀。这样拼接成一个xml文件的资源路径。然后加载到内存。在通过调用parse方法进行解析xml文件。
所以这也是为何如果使用package扫描的方式,必须要保证接口和mapper映射文件必须在同一个包中,名字也必须相同的原因。如:
- 10 . 继续往下解析。如:
DepartmentMapper文件加载整体流程图
Mapper映射文件通过package方式解析流程图.png想获取更多技术干货,请前往叩丁狼官网:http://www.wolfcode.cn/all_article.html