手写一个简单的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中配置关联sql配置文件的路径,这样的话通过一个文件流将所有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、反射、数据库元数据等基础知识)
  • 框架往往都会用到设计模式(本案例用到了工厂设计模式和构建者设计模式)
  • 框架展示了代码的拆分思想,各司其职,便于维护
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
很抱歉,作为AI语言模型,我无法手写代码,但我可以简单地介绍一下MyBatis框架的基本原理和核心组件。 MyBatis是一款Java持久层框架,它的主要作用是将Java对象和关系型数据库之间的操作映射起来,从而实现数据的持久化。MyBatis的核心组件包括: 1. SqlSessionFactoryBuilder:用于创建SqlSessionFactory。通过读取MyBatis的配置文件和Mapper文件,生成SqlSessionFactory,从而获取SqlSession。 2. SqlSessionFactory:是MyBatis框架的核心组件,用于创建SqlSession。通过SqlSessionFactory,我们可以获取SqlSession对象,从而进行数据库的操作。 3. SqlSession:是MyBatis框架中的一个关键组件,它表示和数据库之间的会话。SqlSession提供了一系列方法,用于执行SQL语句、提交事务、关闭连接等操作。 4. Mapper:是MyBatis框架中的另一个重要组件,它用于定义数据库操作的接口。Mapper接口中定义了一系列方法,每个方法对应一个SQL语句。在Mapper接口中,我们可以使用注解或XML文件来配置SQL语句。 基于以上核心组件,我们可以通过编写Mapper接口和对应的SQL语句,使用MyBatis框架来进行数据库的操作。下面是一个简单的Mapper接口示例: ``` public interface UserMapper { @Select("SELECT * FROM user WHERE id = #{id}") User getUserById(int id); @Insert("INSERT INTO user(name, age) VALUES (#{name}, #{age})") void addUser(User user); @Update("UPDATE user SET name = #{name}, age = #{age} WHERE id = #{id}") void updateUser(User user); @Delete("DELETE FROM user WHERE id = #{id}") void deleteUser(int id); } ``` 在这个示例中,我们定义了一个UserMapper接口,其中包含了4个方法,分别对应了查询、插入、更新和删除操作。每个方法都使用@Select、@Insert、@Update、@Delete注解来配置对应的SQL语句。使用MyBatis框架时,我们只需要通过SqlSession.getMapper(UserMapper.class)方法获取UserMapper接口的实例,然后调用其中的方法即可完成对数据库的操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值