手写一个简单的Mybatis(一体篇)
前言
本文为个人技术总结,不够全面,瑕疵是有的,有用的话参考一下吧
源码
附上源码参考:《自定义mybatis-源码》
一、框架介绍
1.1 什么是框架?
框架就像一个厨房,它由锅碗瓢盆瓜果蔬菜肉调味品一应俱全。
你是一个厨子,做饭的工具已经给你提供好了,不用自己在买菜,至于做什么饭则由你来使用这些厨具和食材操作。
框架是一个半成品,封装了别人的代码,我们拿来用就可以了,拿来主义。
1.2 为什么使用框架?
框架解决的是技术整合问题。
软件开发环境和规模都很大,不可能任何一个项目的代码都从零开始,此时就需要一个非常优秀的框架把基础技术整合完毕,我们在他的基础上进一步开发。提高性能,易扩展,易维护,最终提高整个团队的开发效率。
1.3 什么时候使用框架?如何选型?
框架的使用要结合公司业务,框架多种多样,只有合适的才是最好的。
也不要为了技术而技术,一个单体结构的项目你总不会用SpringCloud吧,要避免大炮打蚊子。
Mybatis 和 Hibernate 都是 ORM 持久层框架,不同点在于,Mybatis是半自动的需要开发人员手动编写SQL。
怎么选型?如果说公司做小型项目,数据量并不大,且公司开发人员的技术栈偏 Hibernate 多一些,推荐使用 JPA、Hibernate 这些无需手动编写 SQL 的持久层框架,提高开发效率、版本迭代速度。
而如果说公司是一家互联网公司,用户数较大,对相关 SQL 执行性能要求较为严格,则推荐使用 Mybatis。
1.4 框架如何使用?
Java的框架使用具有一定的共性:
- 导入jar包
- 框架运行细节定义,即编写配置文件(xml)
- 调用框架中的api
二、Mybatis框架
Mybatis 是一款优秀的 ORM 持久层框架,它对jdbc的操作数据库的过程进行封装,开发者只需要关注 SQL 本身,不需要花费精力再去处理例如注册驱动、创建connection、创建statement、手动设置参数、结果集检索等jdbc繁杂的过程代码。MyBatis 支持 XML 或 注解 两种方式供开发人员编写SQL,最终由框架本身将 Java 对象和 SQL 语句映射生成最终执行的 SQL ,执行后,再将结果映射成 Java 对象返回。
Mybatis是对jdbc的封装,下面来看一下原生jdbc操作SQL的案例。
2.1 原生jdbc案例
- 查询user表
- 以List集合形式返回
- 编写pojo类 (User)
- domain,pojo本质都是相同的
2.1.1 建库建表
CREATE DATABASE mybatis;
USE mybatis;
-- ----------------------------
-- Table structure for `user`
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) NOT NULL COMMENT '用户名称',
`sex` char(1) DEFAULT NULL COMMENT '性别',
`birthday` date DEFAULT NULL COMMENT '生日',
`address` varchar(256) DEFAULT NULL COMMENT '地址',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', '小妖', '0', '1996-07-10', '西安');
INSERT INTO `user` VALUES ('2', '狐狸', '1', '1996-07-10', '西安');
INSERT INTO `user` VALUES ('3', '虫子', '1', '1996-07-10', '北京');
INSERT INTO `user` VALUES ('4', '咕子', '1', '1996-07-10', '甘肃');
2.1.2 创建pojo对象
public class User {
private Integer id;
private String userName;
private String sex;
private Date birthday;
private String address;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", userName='" + userName + '\'' +
", sex='" + sex + '\'' +
", birthday=" + birthday +
", address='" + address + '\'' +
'}';
}
}
2.1.3 jdbc代码
/**
* 原生jdbc
*
* @Author: CYL
* @Date: 2021/4/19 10:18
*/
public class UserDaoImpl implements UserDao {
private String driverClass = "com.mysql.jdbc.Driver";
private String url = "jdbc:mysql://127.0.0.1:3306/mybatis?characterEncoding=utf-8";
private String username = "root";
private String password = "root";
/**
* 查询数据库 中的所有用户信息 并封装成一个 List<User>集合
*
* @return
* @author cyl
* @date 2021/4/19 10:19
*/
@Override
public List<User> queryUserList()throws Exception {
// 注册驱动
Class.forName(driverClass);
// 获取连接
Connection connection = DriverManager.getConnection(url, username, password);
// sql
String sql = "select id, username, sex, birthday, address from user;";
// 获取SQL语句执行平台statement对象
// Statement statement = connection.createStatement();
PreparedStatement statement = connection.prepareStatement(sql); // prepareStatement(预处理)
// 执行sql
ResultSet resultSet = statement.executeQuery();
List<User> list = new ArrayList<>();
User user = null;
// 解析结果集
while (resultSet.next()) {
user = new User();
int id = resultSet.getInt("id");
String userName = resultSet.getString("username");
String sex = resultSet.getString("sex");
Date birthday = resultSet.getDate("birthday");
String address = resultSet.getString("address");
user.setId(id);
user.setUserName(userName);
user.setSex(sex);
user.setBirthday(birthday);
user.setAddress(address);
list.add(user);
}
// 关闭资源
resultSet.close();
statement.close();
connection.close();
return list;
}
}
2.1.4 测试程序
public class MainTest {
@Test
public void testJDBC() throws Exception {
UserDao userDao = new UserDaoImpl();
List<User> list = userDao.queryUserList();
list.forEach(System.out::println);
}
}
2.1.5 原生jdbc存在的问题
- 频繁连接,释放数据库资源,降低系统性能
- SQL语句硬编码,难以维护
- 参数和占位符对应问题
- 结果集解析复杂,列名硬编码
2.2 自定义Mybatis框架
2.2.1 需求及目标:
- 所有的Dao层框架都是以接口的形式给我们提供增删改查的API
- 本次自定义Mybatis框架只完成一个API接口:selectList
- 源码开发 —> 打jar包并安装到本地maven仓库 —> 其他项目引用自定义框架
2.2.2 自定义Mybatis框架主线图
2.2.3 自定义Mybatis
步骤1:创建maven工程,packing为jar,引入依赖
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.39</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!-- 读取xml所需的jar -->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.6</version>
</dependency>
</dependencies>
步骤2:定义框架对外API接口,接口中只定义一个selectList方法
接口 SqlSession.java
/**
* 框架 对外 服务的接口
* 提供的功能都定义在这个接口中 由它的实现类去实现
*
* @Author: CYL
* @Date: 2021/4/19 13:53
*/
public interface SqlSession {
/**
* 这是一个通用方法 查什么类型对象都可以 所以接想到 泛型!!
*
* @return
* @author cyl
* @date 2021/4/19 13:55
*/
<T> List<T> selestLsit() throws Exception;
}
接口实现类 SqlSessionImpl.java
public class SqlSessionImpl implements SqlSession{
private String driverClass = "com.mysql.jdbc.Driver";
private String url = "jdbc:mysql://127.0.0.1:3306/mybatis?characterEncoding=utf-8";
private String username = "root";
private String password = "root";
@Override
public <T> List<T> selestLsit() throws Exception {
List<T> list = new ArrayList<>();
// 注册驱动
Class.forName(driverClass);
// 获取连接
Connection connection = DriverManager.getConnection(url, username, password);
String sql = "select * from User";
// 获取SQL语句执行平台statement对象
PreparedStatement statement = connection.prepareStatement(sql);
// 执行sql
ResultSet resultSet = statement.executeQuery();
User user = null;
// 解析结果集
while (resultSet.next()) {
user = new User();
int id = resultSet.getInt("id");
String userName = resultSet.getString("username");
String sex = resultSet.getString("sex");
Date birthday = resultSet.getDate("birthday");
String address = resultSet.getString("address");
user.setId(id);
user.setUserName(userName);
user.setSex(sex);
user.setBirthday(birthday);
user.setAddress(address);
list.add((T)user);
}
return list;
}
}
步骤3:使用工厂模式进行SqlSession的实例化
到这里通过对外暴露SqlSession接口就完了吗?答案是否定的。使用框架的时候需要new这个接口的实现类SqlSessionImpl,但是直接new接口的实现类不便于后期维护(如果实现类名字发生变化或者变更实现类,那么系统中众多调用的地方都需要改),因此使用工厂设计模式去帮我们返回接口的实现类对象,这样如果实现类发生变化,我们只需要修改工厂类中一个地方代码即可,便于维护。
工厂 SqlSessionFactory
/**
* 创建工厂类 用于产生SqlSession实现类对象
* @Author: CYL
* @Date: 2021/4/19 14:17
*/
public class SqlSessionFactory {
public SqlSession openSqlSession() {
SqlSessionImpl sqlSession = new SqlSessionImpl();
return sqlSession;
}
}
测试代码(使用工厂)
@Test
public void testFactory() throws Exception {
// 建造工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactory();
// 使用工厂生成SqlSession
SqlSession sqlSession = sqlSessionFactory.openSqlSession();
List<Object> list = sqlSession.selestLsit();
list.forEach(System.out::println);
}
步骤4:分析 SqlSession 中的需要优化问题
我们观察上面SqlSession的这个实现类会发现有很大问题。
数据源连接信息、sql语句不能写死,写到配置文件中更灵活;操作对象不能写死,通过全限定类名获取对象的字节码文件,通过反射把解析到的元数据set到对象中。
优化点一共有四处,这里记为待办事项(TODO):
- 待办事项01 数据库的连接信息应该放在配置文件中;
- 待办事项02 SQL语句不能写死,应写到配置文件中;
- 待办事项03 pojo对象不单单是User一种,也许是学生对象、也许是商品对象...... 这里也不能写死,想到获取pojo对象的class文件,利用Class.forName(权限定类名)获取,且权限定类名也应该从配置文件中读取;
- 待办事项04 SQL查询出的结果封装不能写死,也要优化(通过反射);
/**
* 需要优化的四个地方
* 数据源连接信息、sql语句不能写死,写到配置文件中更灵活
* 操作对象不能写死,通过全限定类名获取对象的字节码文件,通过反射 把解析到的元数据set到对象中
*
* @Author: CYL
* @Date: 2021/4/19 15:20
*/
public class SqlSessionImpl implements SqlSession{
private String driverClass = "com.mysql.jdbc.Driver";
private String url = "jdbc:mysql://127.0.0.1:3306/mybatis?characterEncoding=utf-8";
private String username = "root";
private String password = "root";
@Override
public <T> List<T> selestLsit() throws Exception {
List<T> list = new ArrayList<>();
// TODO 待办事项01 数据库的连接信息应该放在配置文件中
Class.forName(driverClass);
Connection connection = DriverManager.getConnection(url, username, password);
// TODO 待办事项02 sql语句不能写死(写到配置文件中)
String sql = "select * from User";
PreparedStatement statement = connection.prepareStatement(sql);
ResultSet resultSet = statement.executeQuery();
// TODO 待办事项03 pojo对象不单单是User一种, 利用Class.forName(权限定类名)获取, 且权限定类名应该从配置文件中读取
User user = null;
// TODO 待办事项04 完成 封装对象
while (resultSet.next()) {
user = new User();
int id = resultSet.getInt("id");
String userName = resultSet.getString("username");
String sex = resultSet.getString("sex");
Date birthday = resultSet.getDate("birthday");
String address = resultSet.getString("address");
user.setId(id);
user.setUserName(userName);
user.setSex(sex);
user.setBirthday(birthday);
user.setAddress(address);
list.add((T)user);
}
return list;
}
}
步骤5:解决第一个TODO待办事项 (数据库信息的获取)
定义xml,约定其格式(规范)
SqlMapConfig.xml
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?characterEncoding=utf8" />
<property name="username" value="root" />
<property name="password" value="root" />
</dataSource>
</environment>
</environments>
</configuration>
定义xml对应的pojo类Configuration,并修改SqlSessionImpl中的部分代码
Configuration.java
public class Configuration {
private String driver;
private String url;
private String username;
private String password;
public String getDriver() {
return driver;
}
public void setDriver(String driver) {
this.driver = driver;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "Configuration{" +
"driver='" + driver + '\'' +
", url='" + url + '\'' +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
修改SqlSessionImpl
读取xml然后封装pojo对象供项目使用,考虑放在SqlSessionImpl和SqlSessionFactory中都不合适因为会被读取多次,所以我们再抽象一层SqlSessionFactoryBuilder类去构建工厂,构建工厂的同时,准备工厂内部所需要的材料:读取一次xml文件,然后一直使用。
工厂建造者 SqlSessionFactoryBuilder.java
/**
* 工厂建造者
* 在建造工厂的同时 加载这个配置文件
*
* @Author: CYL
* @Date: 2021/4/20 11:02
*/
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream inputStream) {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactory();
// 解读配置文件
Configuration configuration = loadXmlConfig(inputStream);
sqlSessionFactory.setConfiguration(configuration);
return sqlSessionFactory;
}
/**
* 解析 xml文件 返回一个 Configuration
*
* @param inputStream
* @return
*/
private Configuration loadXmlConfig(InputStream inputStream){
Configuration configuration = new Configuration();
try {
//1: 创建一个SAXReader对象
SAXReader saxReader = new SAXReader();
//2: 调用read方法 关联上 要读取的文件,然后返回一个Document对象
Document document = saxReader.read(inputStream);
inputStream.close();
//3: document调用 getRootElement方法 得到一个根节点(元素)
Element rootElement = document.getRootElement();
//4: 使用xpath表达式 寻找根节点下 所有的property标签
List<Element> propElements = rootElement.selectNodes("//property");
//5: 遍历 得到每个property标签
if(propElements!=null &&propElements.size()>0){
for (Element element : propElements) {
//element就是每个 property标签
//6: 解析 每个propery标签中 name属性 与 value属性
String name = element.attributeValue("name");
String value = element.attributeValue("value");
// System.out.println(name+" "+value);
// 解析的 数据 封装到 Configuration对象中
//根据 name的值 判断 往哪个属性中 设置值
if("driver".equalsIgnoreCase(name)){
configuration.setDriver(value);
}
if("url".equalsIgnoreCase(name)){
configuration.setUrl(value);
}
if("username".equalsIgnoreCase(name)){
configuration.setUsername(value);
}
if("password".equalsIgnoreCase(name)){
configuration.setPassword(value);
}
}
}
} catch (DocumentException | IOException e) {
e.printStackTrace();
}
return configuration;
}
}
步骤6:处理第二个和第三个待办事项(sql和resultType的获取)
定义xml,约定其格式(规范)
UserMapper.xml
<?xml version="1.0" encoding="utf-8" ?>
<mapper namespace="user">
<select id="queryUserList" resultType="com.mybatis.pojo.User">
select * from user
</select>
</mapper>
- namespace:分类管理sql的作用,类似于java中的包名
- id:标识sql语句
- resultType:封装结果的全限定类名
定义对应的pojo类Mapper,并且修改Configuration类
Mapper.java
/**
* 用来表示 sql 与resultType的映射关系
*
* @Author: CYL
* @Date: 2021/4/20 11:27
*/
public class Mapper {
private String sql;
private String resultType;
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
}
public String getResultType() {
return resultType;
}
public void setResultType(String resultType) {
this.resultType = resultType;
}
}
修改 Configuration 类
读取配置sql语句的xml文件,在数据源xml中配置
SqlMapConfig.xml
修改 SqlSessionFactoryBuilder 类中读取mapper.xml文件的部分
修改 SqlSessionFactoryBuilder
/**
* 工厂建造者
* 在建造工厂的同时 加载这个配置文件
*
* @Author: CYL
* @Date: 2021/4/20 11:02
*/
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream inputStream) {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactory();
// 解读配置文件
Configuration configuration = loadXmlConfig(inputStream);
sqlSessionFactory.setConfiguration(configuration);
return sqlSessionFactory;
}
/**
* 解析 xml文件 返回一个 Configuration
*
* @param inputStream
* @return
*/
private Configuration loadXmlConfig(InputStream inputStream){
Configuration configuration = new Configuration();
try {
//1: 创建一个SAXReader对象
SAXReader saxReader = new SAXReader();
//2: 调用read方法 关联上 要读取的文件,然后返回一个Document对象
Document document = saxReader.read(inputStream);
inputStream.close();
//3: document调用 getRootElement方法 得到一个根节点(元素)
Element rootElement = document.getRootElement();
//4: 使用xpath表达式 寻找根节点下 所有的property标签
List<Element> propElements = rootElement.selectNodes("//property");
//5: 遍历 得到每个property标签
if(propElements!=null &&propElements.size()>0){
for (Element element : propElements) {
//element就是每个 property标签
//6: 解析 每个propery标签中 name属性 与 value属性
String name = element.attributeValue("name");
String value = element.attributeValue("value");
// System.out.println(name+" "+value);
// 解析的 数据 封装到 Configuration对象中
//根据 name的值 判断 往哪个属性中 设置值
if("driver".equalsIgnoreCase(name)){
configuration.setDriver(value);
}
if("url".equalsIgnoreCase(name)){
configuration.setUrl(value);
}
if("username".equalsIgnoreCase(name)){
configuration.setUsername(value);
}
if("password".equalsIgnoreCase(name)){
configuration.setPassword(value);
}
}
}
//解析 mapper标签
List<Element> mapperEles = rootElement.selectNodes("//mapper");
//遍历得到每个mapper标签
for (Element mapperEle : mapperEles) {
//mapperEle <mapper resource="UserMapper.xml">
//解析 文件的地址
String mapperPath = mapperEle.attributeValue("resource");
// 根据 mapper的路径 解析文件 并把解析好的数据 封装到confiruation中
parseMapper(mapperPath,configuration);
}
} catch (DocumentException | IOException e) {
e.printStackTrace();
}
return configuration;
}
/**
* 根据mapper路径解析 mapper里面的数据 将数据封装到 configuration中
* @param mapperPath
* @param configuration
*/
private void parseMapper(String mapperPath, Configuration configuration) throws DocumentException,IOException{
// 创建 SAXReader对象
SAXReader saxReader = new SAXReader();
//根据 读取文件
InputStream inputStream = XMLConfigBuilder.class.getClassLoader().getResourceAsStream(mapperPath);
Document document = saxReader.read(inputStream);
//获取根节点
Element rootElement = document.getRootElement();
//解析 namespace
String namespace = rootElement.attributeValue("namespace");
//获取所有的 select标签
List<Element> selectEles = rootElement.selectNodes("//select");
//遍历得到每一个
for (Element selectEle : selectEles) {
// selectEle
/*
<select id="queryUserList" resultType="com.itheima.pojo.User">
select * from user
</select>
*/
String id = selectEle.attributeValue("id");
String resultType = selectEle.attributeValue("resultType");
String sql = selectEle.getText();
// 封装一个Mapper对象
Mapper mapper = new Mapper();
mapper.setSql(sql);
mapper.setResultType(resultType);
String selectId = namespace+"."+id;
configuration.getMapperMap().put(selectId,mapper);
}
}
}
步骤7:修改SqlSessionImpl中第二和第三个待办事项
SqlSession接口添加参数
修改SqlSessionImpl
步骤8:将SqlSessionFactoryBuilder中读取xml构建Configuration对象的代码抽取为XmlConfigBuilder类
将SqlSessionFactoryBuilder中读取xml构建Configuration对象的代码抽取为XmlConfigBuilder类,便于代码重用(不同功能的代码放到不同的java类中,各司其职)。
构建Configuration复杂对象的过程也是设计模式中构建者模式的一种体现。
XmlConfigBuilder.java
public class XMLConfigBuilder {
/**
* 解析 xml文件 返回一个 Configuration
* @param inputStream 读取xml的那个流
* @return 返回一个 配置 对象
*/
public static Configuration loadXmlConfig(InputStream inputStream){
Configuration configuration = new Configuration();
try {
//1: 创建一个SAXReader对象
SAXReader saxReader = new SAXReader();
//2: 调用read方法 关联上 要读取的文件,然后返回一个Document对象
Document document = saxReader.read(inputStream);
inputStream.close();
//3: document调用 getRootElement方法 得到一个根节点(元素)
Element rootElement = document.getRootElement();
//4: 使用xpath表达式 寻找根节点下 所有的property标签
List<Element> propElements = rootElement.selectNodes("//property");
//5: 遍历 得到每个property标签
if(propElements!=null &&propElements.size()>0){
for (Element element : propElements) {
//element就是每个 property标签
//6: 解析 每个propery标签中 name属性 与 value属性
String name = element.attributeValue("name");
String value = element.attributeValue("value");
// System.out.println(name+" "+value);
// 解析的 数据 封装到 Configuration对象中
//根据 name的值 判断 往哪个属性中 设置值
if("driver".equalsIgnoreCase(name)){
configuration.setDriver(value);
}
if("url".equalsIgnoreCase(name)){
configuration.setUrl(value);
}
if("username".equalsIgnoreCase(name)){
configuration.setUsername(value);
}
if("password".equalsIgnoreCase(name)){
configuration.setPassword(value);
}
}
}
//解析 mapper标签
List<Element> mapperEles = rootElement.selectNodes("//mapper");
//遍历得到每个mapper标签
for (Element mapperEle : mapperEles) {
//mapperEle <mapper resource="UserMapper.xml">
//解析 文件的地址
String mapperPath = mapperEle.attributeValue("resource");
// 根据 mapper的路径 解析文件 并把解析好的数据 封装到confiruation中
parseMapper(mapperPath,configuration);
}
} catch (DocumentException |IOException e) {
e.printStackTrace();
}
return configuration;
}
/**
* 根据mapper路径解析 mapper里面的数据 将数据封装到 configuration中
* @param mapperPath
* @param configuration
*/
public static void parseMapper(String mapperPath, Configuration configuration) throws DocumentException,IOException{
// 创建 SAXReader对象
SAXReader saxReader = new SAXReader();
//根据 读取文件
InputStream inputStream = XMLConfigBuilder.class.getClassLoader().getResourceAsStream(mapperPath);
Document document = saxReader.read(inputStream);
//获取根节点
Element rootElement = document.getRootElement();
//解析 namespace
String namespace = rootElement.attributeValue("namespace");
//获取所有的 select标签
List<Element> selectEles = rootElement.selectNodes("//select");
//遍历得到每一个
for (Element selectEle : selectEles) {
// selectEle
/*
<select id="queryUserList" resultType="com.itheima.pojo.User">
select * from user
</select>
*/
String id = selectEle.attributeValue("id");
String resultType = selectEle.attributeValue("resultType");
String sql = selectEle.getText();
// 封装一个Mapper对象
Mapper mapper = new Mapper();
mapper.setSql(sql);
mapper.setResultType(resultType);
String selectId = namespace+"."+id;
configuration.getMapperMap().put(selectId,mapper);
}
}
}
修改SqlSessionFactoryBuilder
/**
* 工厂建造者
* 在建造工厂的同时 加载这个配置文件
*
* @Author: CYL
* @Date: 2021/4/20 11:02
*/
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream inputStream) {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactory();
// 解读配置文件,调用封装好的XMLConfigBuilder.loadXmlConfig()
Configuration configuration = XMLConfigBuilder.loadXmlConfig(inputStream);
sqlSessionFactory.setConfiguration(configuration);
return sqlSessionFactory;
}
}
步骤9:解决第四个待办事项 封装SQL查询结果(通过反射)
最终的SqlSessionImpl
/**
* 实现SqlSession接口
*
* @Author: CYL
* @Date: 2021/4/19 13:57
*/
public class SqlSessionImpl implements SqlSession {
private Configuration configuration;
public void setConfiguration(Configuration configuration) {
this.configuration = configuration;
}
@Override
public <T> List<T> selestLsit(String selectId) throws Exception {
List<T> list = new ArrayList<>();
// TODO 待办事项01 数据库的连接信息应该放在配置文件中
Class.forName(configuration.getDriver());
Connection connection = DriverManager.getConnection(configuration.getUrl(),
configuration.getUsername(), configuration.getPassword());
// TODO 待办事项02 sql语句不能写死(写到配置文件中)
String sql = configuration.getMapperMap().get(selectId).getSql();
PreparedStatement statement = connection.prepareStatement(sql);
// 执行sql
ResultSet resultSet = statement.executeQuery();
Object obj = null;
// TODO 待办事项03 权限定类名应该从配置文件中读取出来
String resultType = configuration.getMapperMap().get(selectId).getResultType(); // com.mybatis.pojo.User
Class clazz = Class.forName(resultType);
// TODO 待办事项04 完成 封装对象
// 获取元数据
ResultSetMetaData metaData = resultSet.getMetaData();
// 定义一个集合用于保存解析到的 字段名
List<String> fieldList = new ArrayList<>();
// 获取字段个数
int columnCount = metaData.getColumnCount();
// 根据序号获取指定的字段名
for (int i = 1; i <= columnCount; i++) {
String columnName = metaData.getColumnName(i);
fieldList.add(columnName);
}
// 获取所有方法
Method[] methods = clazz.getMethods();
while (resultSet.next()) {
// 反射创建对象 字节码文件对象.newInstance()
obj = clazz.newInstance();
// 遍历得到每个属性名字
for (String fieldName : fieldList) { // fieldName 字段的名字
// 获取字段的值
Object fieldValue = resultSet.getObject(fieldName);
// 遍历所有方法
for (Method method : methods) {
// 获取方法的名字
String methodName = method.getName();
// 寻找 当前fieldName对应的set方法
if (methodName.equalsIgnoreCase("set"+fieldName)) {
// 如果是true 就找到了setXxx方法
// 执行
method.invoke(obj, fieldValue);
}
}
}
list.add((T)obj);
}
return list;
}
}
代码测试
public class MainTest {
@Test
public void selectList() throws Exception {
// 创建工厂建造者对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("SqlMapConfig.xml");
// 工厂对象 由建造者建造
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
// 创建一个暴露接口的实现类对象
SqlSession sqlSession = sqlSessionFactory.openSqlSession();
String selectId = "user.queryUserList";
List<Object> list = sqlSession.selestLsit(selectId);
list.forEach(System.out::println);
inputStream.close();
}
}
测试结果
2.2.4 工程目录结构
2.3 打包测试
至此自定义mybatis的demo开发完毕。但框架是要能被其他项目引用的,所以我们把开发好的小框架进行打包然后测试。
2.3.1 打包之前删掉多余文件
2.3.2 首先clear一下,之后双击install,安装到本地仓库
2.3.3 在本地Maven仓库查看是否打包成功
2.3.4 测试
新建一个工程进行测试,否则在同一个工程中的话引用的是自定义Mybatis框架的源码工程,而非打好的jar包。
2.3.5 自定义Mybatis框架小结
- 自定义Mybatis结构图
- 框架的开发使用流程
编写源代码—>打jar包—>框架使用者引入jar包—>按照框架的规范进行XML配置—>Java程序中调用框架API实现功能 - 引入框架坐标即可,它所依赖的dom4j和jaxen都不需要再次引入,这是Maven的依赖传递特性
- 本案例中整合了jdbc、dom4j、xpath、反射、数据库元数据等基础知识)
- 框架往往都会用到设计模式(本案例用到了工厂设计模式和构建者设计模式)
- 框架展示了代码的拆分思想,各司其职,便于维护