01-MyBatis入门到手写以及设计模式说明

79 阅读23分钟

01-MyBatis入门到手写以及设计模式说明

框架概述

1.1 什么是框架

框架(Framework)是整个或部分系统的可重用设计,表现为一组抽象构件及构件实例间交互的方法;另一种 定义认为,框架是可被应用开发者定制的应用骨架。前者是从应用方面而后者是从目的方面给出的定义。 简而言之,框架其实就是某种应用的半成品,就是一组组件,供你选用完成你自己的系统。简单说就是使用别人搭好的舞台,你来做表演。而且框架一般是成熟的,不断升级的软件。

它是我们软件开发中的一套解决方案,不同的框架解决的是不同的问题。 使用框架的好处:框架封装了很多的细节,使开发者可以使用极简的方式实现功能。大大提高开发效率。

例如:外卖平台就属于一个半成品,使用外卖平台就可以帮助我们省去很多事情。

1.2 框架要解决的问题

框架要解决的最重要的一个问题是技术整合的问题,在 J2EE 的 框架中,有着各种各样的技术,不同的软件企业需要从 J2EE 中选择不同的技术,这就使得软件企业最终的应用依赖于这些技术,技术自身的复杂性和技术的风险性将会直接对应用造成冲击。而应用是软件企业的核心,是竞争力的关键所在,因此应该将应用自身的设计和具体的实现技术解耦。这样,软件企业的研发将集中在应用的设计上,而不是具体的技术实现,技术实现是应用的底层支撑,它不应该直接对应用产生影响。

1.3 软件开发的分层重要性

框架的重要性在于它实现了部分功能,并且能够很好的将低层应用平台和高层业务逻辑进行了缓和。为了实现 软件工程中的“高内聚、低耦合”。把问题划分开来各个解决,易于控制,易于延展,易于分配资源。我们常见的 MVC 软件设计思想就是很好的分层思想。

image.png

1.4 分层开发下的常见框架

常见的 JavaEE 开发框架:

1 、解决数据库持久化问题的框架

Mybatis

mybatis是一个优秀的基于java的持久层框架,它内部封装了jdbc, 使开发者只需要关注sql语句本身,而不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。

SpringMVC

在web模型中,MVC是一种很流行的框架,通过把Model,View,Controller分离,把较为复杂的web应用分成逻辑清晰的几部分,是为了简化开发,减少出错。还是为了组内开发人员之间的配合。总之就是一种分层工作的办法。

SpringMVC,是spring的一个子框架,当然拥有spring的特性,如依赖注入。

Spring下的子项目:Spring Web MVC是一种基于Java的实现了Web MVC设计模式的请求驱动类型的轻量级Web框架,即使用了MVC架构模式的思想,将web层进行职责解耦,基于请求驱动指的就是使用请求-响应模型,框架的目的就是帮助我们简化开发,Spring Web MVC也是要简化我们日常Web开发的。

Spring

spring是开源的轻量级框架 spring核心主要两部分:

(1)aop:面向切面编程,扩展功能不是修改源代码实现

(2)ioc:控制反转,比如有一个类,在类里面有方法(不是静态的方法),调用类里面的方法,创建类的对象,使用对象调用方法,创建类对象的过程,需要new出来对象,ioc可以帮你实现创建对象等操作。

1.5 JDBC回顾

public static void main(String[] args) {
      Connection connection = null;
      PreparedStatement preparedStatement = null;
      ResultSet resultSet = null;
        try {
            //加载数据库驱动
            Class.forName("com.mysql.jdbc.Driver");
            //通过驱动管理类获取数据库链接
            connection = DriverManager
            .getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf8","root", "root");
            //定义 sql 语句 ?表示占位符
            String sql = "select * from user where username = ?";
            //获取预处理 statement
	    preparedStatement = connection.prepareStatement(sql);
            //设置参数,第一个参数为 sql 语句中参数的序号(从 1 开始),第二个参数为设置的参数值
            preparedStatement.setString(1, "王五");
            //向数据库发出 sql 执行查询,查询出结果集
            resultSet = preparedStatement.executeQuery();
            //遍历查询结果集
            while(resultSet.next()){
           		 	System.out.println(resultSet.getString("id")+"
            		"+resultSet.getString("username"));
            }
            } catch (Exception e) {
            	e.printStackTrace();
            }finally{
                //释放资源
                if(resultSet!=null){
                    try {
                    	resultSet.close();
                    } catch (SQLException e) {
                    	e.printStackTrace();
                    }
				}
                if(preparedStatement!=null){
                    try {
                    	preparedStatement.close();
                    } catch (SQLException e) {
                    	e.printStackTrace();
                    }
                }
                if(connection!=null){
               	 try {
                	connection.close();
               	 } catch (SQLException e) {
                	// TODO Auto-generated catch block
               	 	e.printStackTrace();
                }
			}
		}
	}
上边使用 jdbc 的原始方法(未经封装)实现了查询数据库表记录的操作。

1.6 问题分析

image.png 1、数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库链接池可解决此问题。

2、Sql 语句在代码中硬编码,造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。

3、使用 preparedStatement 向占有位符号传参数存在硬编码,因为 sql 语句的 where 条件不一定,可能多也可能少,修改 sql 还要修改代码,系统不易维护。

4、对结果集解析存在硬编码(查询列名),sql 变化导致解析代码变化,系统不易维护,如果能将数据库记 录封装成 pojo 对象解析比较方便。

Mybatis 框架快速入门

2.1 mybatis概述

Mybatis是一个持久层框架,用java编写的。

​ 它封装了jdbc操作的很多细节,使开发者只需要关注sql语句本身,而无需关注注册驱动,创建连接等繁杂过程

​ 它使用了ORM思想实现了结果集的封装。

ORM: Object Relational Mappging 对象关系映射,简单的说: 就是把数据库表和实体类及实体类的属性对应起来让我们可以操作实体类就实现操作数据库表。

		user			User
		id			userId
		user_name		userName

2.2 搭建 Mybatis 开发环境

2.2.1 创建 maven 工程

创建 mybatis01 的工程,工程信息如下:
Groupid:com.qadl
ArtifactId:day01_01-mybatis
Packing:jar

2.2.2 添加数据库表

CREATE TABLE `user` (
  `id` int(11) NOT NULL auto_increment,
  `username` varchar(32) NOT NULL COMMENT '用户名称',
  `birthday` datetime default NULL COMMENT '生日',
  `sex` char(1) default NULL COMMENT '性别',
  `address` varchar(256) default NULL COMMENT '地址',
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
​
​
​
insert  into `user`(`id`,`username`,`birthday`,`sex`,`address`) values 
(41,'老王','2020-02-27 17:47:08','男','北京'),
(42,'小二王','2020-03-02 15:09:37','女','北京金燕龙'),
(43,'小二王','2020-03-04 11:34:34','女','北京金燕龙'),
(45,'亲爱的李','2020-03-04 12:04:06','男','北京金燕龙'),
(46,'老王','2020-03-07 17:37:26','男','北京'),
(48,'小马宝莉','2020-03-08 11:44:00','女','北京修正');

2.2.3 添加Mybatis的坐标

在 pom.xml 文件中添加 Mybatis3.4.5 的坐标,如下:

<dependencies>
        <!--mybatis依赖jar包-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.5</version>
        </dependency>
        <!--单元测试-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.10</version>
            <scope>test</scope>
        </dependency>
        <!--MySQL数据库-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.19</version>
            <scope>runtime</scope>
        </dependency>
        <!--日志信息-->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.12</version>
        </dependency>
    </dependencies>

2.2.4 编写 User 实体类

package com.sixstar.domain;
​
import java.io.Serializable;
import java.util.Date;
​
public class User implements Serializable {
​
    private Integer id;
    private String username;
    private Date birthday;
    private String sex;
    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 Date getBirthday() {
        return birthday;
    }
    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }
    public String getSex() {
        return sex;
    }
    public void setSex(String sex) {
        this.sex = sex;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
    @Override
    public String toString() {
        return "User [id=" + id + ", username=" + username + ", birthday=" + birthday
                + ", sex=" + sex + ", address="
                + address + "]";
    }
}
​

2.2.5 编写 持久层口 接口 IUserDao

IUserDao 接口就是我们的持久层接口(也可以写成 UserDao 或者 UserMapper),具体代码如下:

package com.sixstar.dao;
​
import com.sixstar.domain.User;
​
import java.util.List;
​
public interface IUserDao {
​
    /**
     * 查询所有用户
     * @return
     */
    List<User> findAll();
    
}
​

2.2.6 编写 SqlMapConfig.xml 配置文件

1、mybatis添加约束

<?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">
<!-- mybatis的主配置文件 -->
<configuration>
    <!-- 配置环境 -->
    <environments default="mysql">
        <!-- 配置mysql的环境-->
        <environment id="mysql">
            <!-- 配置事务的类型-->
            <transactionManager type="JDBC"></transactionManager>
            <!-- 配置数据源(连接池) -->
            <dataSource type="POOLED">
                <!-- 配置连接数据库的4个基本信息 -->
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC"/>
                <property name="username" value="root"/>
                <property name="password" value="123"/>
            </dataSource>
        </environment>
    </environments>
​
    <!-- 指定映射配置文件的位置,映射配置文件指的是每个dao独立的配置文件 -->
    <mappers>
        <mapper resource="com/qadl/dao/IUserDao.xml"/>
    </mappers>
</configuration>

2.2.7 编写 持久层接口的件映射文件 IUserDao.xml

  • 要求:
    • 创建位置:必须和持久层接口在相同的包中。
    • 名称:必须以持久层接口名称命名文件名,扩展名是.xml

image.png

<?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">
<!--
    namespace属性是名称空间的意思。
    功能相当于 给配置文件定义一个包名。
    一般情况下。可以写两种值,一种是对应类的全类名
    一种情况是。对应类的处理接口全类名(后面讲)。
 --><mapper namespace="com.sixstar.dao.IUserDao">
    <!--
    select 标签用于定义一个select查询语句
    resultType 属性定义返回值的数据类型
    配置查询所有
    -->
    <select id="findAll" resultType="com.sixstar.domain.User">
        select * from user
    </select>
</mapper>
环境搭建的注意事项:
第一个:创建IUserDao.xml 和 IUserDao.java时名称是为了和我们之前的知识保持一致。
​ 在Mybatis中它把持久层的操作接口名称和映射文件也叫做:Mapper 
​ 所以:IUserDao 和 IUserMapper是一样的
第二个:在idea中创建目录的时候,它和包是不一样的 
​ 包在创建时:com.sixstar.dao它是三级结构 
​ 目录在创建时:com.sixstar.dao是一级目录 
第三个:mybatis的映射配置文件位置必须和dao接口的包结构相同
第四个:映射配置文件的mapper标签namespace属性的取值必须是dao接口的全限定类名
第五个:映射配置文件的操作配置(select),id属性的取值必须是dao接口的方法名

当我们遵从了第三,四,五点之后,我们在开发中就无须再写dao的实现类。

2.2.8 配置日志log4j.properties

# Set root category priority to INFO and its only appender to CONSOLE.
#log4j.rootCategory=INFO, CONSOLE            debug   info   warn error fatal
log4j.rootCategory=debug, CONSOLE, LOGFILE
​
# Set the enterprise logger category to FATAL and its only appender to CONSOLE.
log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE
​
# CONSOLE is set to be a ConsoleAppender using a PatternLayout.
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\n
​
# LOGFILE is set to be a File appender using a PatternLayout.
log4j.appender.LOGFILE=org.apache.log4j.FileAppender
log4j.appender.LOGFILE.File=d:\axis.log
log4j.appender.LOGFILE.Append=true
log4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayout
log4j.appender.LOGFILE.layout.ConversionPattern=%d{ISO8601} %-6r [%15.15t] %-5p %30.30c %x - %m\n

2.2.9 编写测试类

package com.sixstar.test;
​
import com.sixstar.dao.IUserDao;
import com.sixstar.domain.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
​
import java.io.InputStream;
import java.util.List;
​
public class MyBatisTest {
    public static void main(String[] args) throws Exception {
        //1.读取配置文件
        InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
        //2.创建 SqlSessionFactory 的构建者对象
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        //3.使用构建者创建工厂对象 SqlSessionFactory
        SqlSessionFactory factory = builder.build(in);
        //4.使用 SqlSessionFactory 生产 SqlSession 对象
        SqlSession session = factory.openSession();
        //5.使用 SqlSession 创建 dao 接口的代理对象
        IUserDao userDao = session.getMapper(IUserDao.class);
        //6.使用代理对象执行查询所有方法
        List<User> users = userDao.findAll();
        for(User user : users) {
            System.out.println(user);
        }
        //7.释放资源
        session.close();
        in.close();
    }
}
​

2.2.10 使用注解方式

1、 在持久层接口中添加注解
package com.sixstar.dao;
​
import com.sixstar.domain.User;
import org.apache.ibatis.annotations.Select;
​
import java.util.List;
​
public interface IUserDao {
​
    /**
     * 查询所有用户
     * @return
     */
    @Select("select * from user")
    List<User> findAll();
​
}
2、修改 SqlMapConfig.xml
<!-- 告知 mybatis 映射配置的位置 -->
<mappers>
    <mapper class="com.sixstar.dao.IUserDao"/>
</mappers>
  • 注意事项:
    • 不要忘记在映射配置中告知mybatis要封装到哪个实体类中
    • 配置的方式:指定实体类的全限定类名
    mybatis基于注解的入门案例:
        把IUserDao.xml移除,在dao接口的方法上使用@Select注解,并且指定SQL语句
        同时需要在SqlMapConfig.xml中的mapper配置时,使用class属性指定dao接口的全限定类名。
    明确:
    我们在实际开发中,都是越简便越好,所以都是采用不写dao实现类的方式。
    不管使用XML还是注解配置。
    但是Mybatis它是支持写dao实现类的。

2.2.11 小结

通过快速入门示例,我们发现使用 mybatis 是非常容易的一件事情,因为只需要编写 Dao 接口并且按照 mybatis 要求编写两个配置文件,就可以实现功能。远比我们之前的 jdbc 方便多了。(我们使用注解之后,将变得更为简单,只需要编写一个 mybatis 配置文件就够了。)

但是,这里面包含了许多细节,比如为什么会有工厂对象(SqlSessionFactory),为什么有了工厂之后还 要有构建者对象(SqlSessionFactoryBuilder),为什么 IUserDao.xml 在创建时有位置和文件名的要求等等。

这些问题我们在手写 mybatis 框架的章节,通过层层剥离的方式,给大家讲解。

请注意:我们讲解自定义 Mybatis 框架,不是让大家回去自己去写个 mybatis,而是让我们能更好了了解 mybatis 内部是怎么执行的,在以后的开发中能更好的使用 mybatis 框架,同时对它的设计理念(设计模式)有一个认识。

2.2.12 入门案例分析

image.png

1、读取配置文件

Java 类路径告诉 java 解释器和 javac 编译器去哪里找它们要执行或导入的类。。

2、创建 SqlSessionFactory 的构建者对象

1-在Mybatis环境的初始化过程中,SqlSessionFactoryBuilder会调用XMLConfigBuilder读取所有的MybatisMapConfig.xml和所有的*Mapper.xml文件,构建Mybatis运行的核心对象Configuration对象,然后将该Configuration对象作为参数构建一个SqlSessionFactory对象。

其中XMLConfigBuilder 在构建Configuration对象时,也会调用XMLMapperBuilder用于读取*.Mapper文件,而XMLMapperBuilder会使用XMLStatementBuilder来读取和build 所有的SQL语句。

2-举例:我们去创建一个真实的工厂的时候,那么我们应该从哪里出发,首先我们工厂创建在什么地方选场地,我们在选完之后还要去买建筑材料,去请人工等等一些操作,这些操作可能就又费时又费力,在这个过程中我们还不一定最终能够把工厂建好。

所以我们选择的办法可能是什么,最直接的找个包工队,有了包工队我们是不是就不用管了,剩下的事情就都是他的了,我们需要干嘛,我们只需要告诉他我们的需要然后给钱。

SqlSessionFactoryBuilder构建者-包工队

他就负责给我们创建工厂,里面那些繁琐的事情我们就不用管了,我们只需要做一件事,给钱。in就是钱

3、使用工厂生产SqlSession对象

1-简单工厂模式 SqlSession可以认为是一个Mybatis工作的核心的接口,通过这个接口可以执行执行SQL语句、获取Mappers、管理事务。类似于连接MySQL的Connection对象。

举例-比如如果我们在这里new了一个实现类,这个时候我们就只能使用这个1实现了,那天我想使用2实现的时候,那么就需要打开代码自己去改成2实现,我们的应用都是web开发部署在服务器上的,如果你每次修改都需要改源码的话,那么带给我们的就是重新部署重新编译重新启动服务器,所以我们把new这个关键字屏蔽了,使用工厂给我们去生产一个Session对象,这个时候就解决了我们类之间的依赖关系。

4、使用 SqlSession 创建 dao 接口的代理对象

代理模式可以认为是Mybatis的核心使用的模式,正是由于这个模式,我们只需要编写Mapper.java接口,不需要实现,由Mybatis后台帮我们完成具体SQL的执行

手写Mybatis框架

3.1 涉及知识点介绍

本章我们将使用前面所学的基础知识来构建一个属于自己的持久层框架,将会涉及到的一些知识点:工厂模式 (Factory 工厂模式)、构造者模式(Builder 模式)、代理模式,反射,自定义注解,注解的反射,xml 解析, 数据库元数据,元数据的反射等。

3.2 流程分析

image.png

创建 Maven 工程

创建 mybatis01 的工程,工程信息如下:
Groupid:com.sixstar
ArtifactId:day01_02-mybatis
Packing:jar

3.4 引入相关坐标

<dependencies>
        <!--单元测试-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.10</version>
            <scope>test</scope>
        </dependency>
        <!--MySQL数据库-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.19</version>
            <scope>runtime</scope>
        </dependency>
        <!--日志信息-->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.12</version>
        </dependency>
</dependencies>

3.5 编写Resources类

package com.sixstar.mybatis.io;
​
import java.io.InputStream;
​
/**
 * 使用类加载器读取配置文件的类
 */
public class Resources {
​
    public static InputStream getResourceAsStream(String filePath){
        /*
        Resources.class拿到当前类的字节码
        getClassLoader()获取字节码的类加载器
        getResourceAsStream(filePath)根据类加载器读取配置加载到内存
         */
        return Resources.class.getClassLoader().getResourceAsStream(filePath);
    }
}
​

3.6 编写SqlSessionFactoryBuilder

package com.sixstar.mybatis.sqlsession;
​
​
import java.io.InputStream;
​
/**
 * 用于创建一个SqlSessionFactory对象
 */
public class SqlSessionFactoryBuilder {
​
    /**
    * 根据传入的流,实现对 SqlSessionFactory 的创建
    * @param config 它就是 SqlMapConfig.xml 的配置以及里面包含的 IUserDao.xml 的配置
    * @return
    */
    public SqlSessionFactory build(InputStream config){
        return null;
    }
}
​

3.7 编写SqlSessionFactory

package com.sixstar.mybatis.sqlsession;
​
​
public interface SqlSessionFactory {
​
    /**
     * 用于打开一个新的SqlSession对象
     * @return
     */
    SqlSession openSession();
}
​

3.8 编写SqlSession

package com.sixstar.mybatis.sqlsession;
​
/**
 * 手写Mybatis中和数据库交互的核心类
 *  它里面可以创建dao接口的代理对象
 */
public interface SqlSession {
​
    /**
     * 根据参数创建一个代理对象
     * @param daoInterfaceClass dao的接口字节码
     * @param <T>
     * @return
     */
    <T> T getMapper(Class<T> daoInterfaceClass);
​
    /**
     * 释放资源
     */
    void close();
}
​

3.9 导入工具类

package com.sixstar.mybatis.utils;
​
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
​
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
​
/**
 *  用于解析配置文件
 */
public class XMLConfigBuilder {
​
​
​
    /**
     * 解析主配置文件,把里面的内容填充到DefaultSqlSession所需要的地方
     * 使用的技术:
     *      dom4j+xpath
     */
    public static Configuration loadConfiguration(InputStream config){
        try{
            //定义封装连接信息的配置对象(mybatis的配置对象)
            Configuration cfg = new Configuration();
​
            //1.获取SAXReader对象
            SAXReader reader = new SAXReader();
            //2.根据字节输入流获取Document对象
            Document document = reader.read(config);
            //3.获取根节点
            Element root = document.getRootElement();
            //4.使用xpath中选择指定节点的方式,获取所有property节点
            List<Element> propertyElements = root.selectNodes("//property");
            //5.遍历节点
            for(Element propertyElement : propertyElements){
                //判断节点是连接数据库的哪部分信息
                //取出name属性的值
                String name = propertyElement.attributeValue("name");
                if("driver".equals(name)){
                    //表示驱动
                    //获取property标签value属性的值
                    String driver = propertyElement.attributeValue("value");
                    cfg.setDriver(driver);
                }
                if("url".equals(name)){
                    //表示连接字符串
                    //获取property标签value属性的值
                    String url = propertyElement.attributeValue("value");
                    cfg.setUrl(url);
                }
                if("username".equals(name)){
                    //表示用户名
                    //获取property标签value属性的值
                    String username = propertyElement.attributeValue("value");
                    cfg.setUsername(username);
                }
                if("password".equals(name)){
                    //表示密码
                    //获取property标签value属性的值
                    String password = propertyElement.attributeValue("value");
                    cfg.setPassword(password);
                }
            }
            //取出mappers中的所有mapper标签,判断他们使用了resource还是class属性
            List<Element> mapperElements = root.selectNodes("//mappers/mapper");
            //遍历集合
            for(Element mapperElement : mapperElements){
                //判断mapperElement使用的是哪个属性
                Attribute attribute = mapperElement.attribute("resource");
                if(attribute != null){
                    System.out.println("使用的是XML");
                    //表示有resource属性,用的是XML
                    //取出属性的值
                    String mapperPath = attribute.getValue();//获取属性的值"com/sixstar/dao/IUserDao.xml"
                    //把映射配置文件的内容获取出来,封装成一个map
                    Map<String,Mapper> mappers = loadMapperConfiguration(mapperPath);
                    //给configuration中的mappers赋值
                    cfg.setMappers(mappers);
                }else{
                    System.out.println("使用的是注解");
                    //表示没有resource属性,用的是注解
                    //获取class属性的值
                    String daoClassPath = mapperElement.attributeValue("class");
                    //根据daoClassPath获取封装的必要信息
                    Map<String,Mapper> mappers = loadMapperAnnotation(daoClassPath);
                    //给configuration中的mappers赋值
                    cfg.setMappers(mappers);
                }
            }
            //返回Configuration
            return cfg;
        }catch(Exception e){
            throw new RuntimeException(e);
        }finally{
            try {
                config.close();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
​
    }
    /**
     * 根据传入的参数,解析XML,并且封装到Map中
     * @param mapperPath    映射配置文件的位置
     * @return  map中包含了获取的唯一标识(key是由dao的全限定类名和方法名组成)
     *          以及执行所需的必要信息(value是一个Mapper对象,里面存放的是执行的SQL语句和要封装的实体类全限定类名)
     */
    private static Map<String,Mapper> loadMapperConfiguration(String mapperPath)throws IOException {
        InputStream in = null;
        try{
            //定义返回值对象
            Map<String,Mapper> mappers = new HashMap<String,Mapper>();
            //1.根据路径获取字节输入流
            in = Resources.getResourceAsStream(mapperPath);
            //2.根据字节输入流获取Document对象
            SAXReader reader = new SAXReader();
            Document document = reader.read(in);
            //3.获取根节点
            Element root = document.getRootElement();
            //4.获取根节点的namespace属性取值
            String namespace = root.attributeValue("namespace");//是组成map中key的部分
            //5.获取所有的select节点
            List<Element> selectElements = root.selectNodes("//select");
            //6.遍历select节点集合
            for(Element selectElement : selectElements){
                //取出id属性的值      组成map中key的部分
                String id = selectElement.attributeValue("id");
                //取出resultType属性的值  组成map中value的部分
                String resultType = selectElement.attributeValue("resultType");
                //取出文本内容            组成map中value的部分
                String queryString = selectElement.getText();
                //创建Key
                String key = namespace+"."+id;
                //创建Value
                Mapper mapper = new Mapper();
                mapper.setQueryString(queryString);
                mapper.setResultType(resultType);
                //把key和value存入mappers中
                mappers.put(key,mapper);
            }
            return mappers;
        }catch(Exception e){
            throw new RuntimeException(e);
        }finally{
            in.close();
        }
    }
​
    /**
     * 根据传入的参数,得到dao中所有被select注解标注的方法。
     * 根据方法名称和类名,以及方法上注解value属性的值,组成Mapper的必要信息
     * @param daoClassPath
     * @return
     */
    private static Map<String,Mapper> loadMapperAnnotation(String daoClassPath)throws Exception{
        //定义返回值对象
        Map<String,Mapper> mappers = new HashMap<String, Mapper>();
​
        //1.得到dao接口的字节码对象
        Class daoClass = Class.forName(daoClassPath);
        //2.得到dao接口中的方法数组
        Method[] methods = daoClass.getMethods();
        //3.遍历Method数组
        for(Method method : methods){
            //取出每一个方法,判断是否有select注解
            boolean isAnnotated = method.isAnnotationPresent(Select.class);
            if(isAnnotated){
                //创建Mapper对象
                Mapper mapper = new Mapper();
                //取出注解的value属性值
                Select selectAnno = method.getAnnotation(Select.class);
                String queryString = selectAnno.value();
                mapper.setQueryString(queryString);
                //获取当前方法的返回值,还要求必须带有泛型信息
                Type type = method.getGenericReturnType();//List<User>
                //判断type是不是参数化的类型
                if(type instanceof ParameterizedType){
                    //强转
                    ParameterizedType ptype = (ParameterizedType)type;
                    //得到参数化类型中的实际类型参数
                    Type[] types = ptype.getActualTypeArguments();
                    //取出第一个
                    Class domainClass = (Class)types[0];
                    //获取domainClass的类名
                    String resultType = domainClass.getName();
                    //给Mapper赋值
                    mapper.setResultType(resultType);
                }
                //组装key的信息
                //获取方法的名称
                String methodName = method.getName();
                String className = method.getDeclaringClass().getName();
                String key = className+"."+methodName;
                //给map赋值
                mappers.put(key,mapper);
            }
        }
        return mappers;
    }
​
}
​

3.10 导入相关jar包

<!-- 解析 xml 的 dom4j -->
<dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>1.6.1</version>
</dependency>
<!-- dom4j 的依赖包 jaxen -->
<dependency>
    <groupId>jaxen</groupId>
    <artifactId>jaxen</artifactId>
    <version>1.1.6</version>
</dependency>

3.11 编写Configuration

package com.sixstar.mybatis.cfg;
​
import java.util.HashMap;
import java.util.Map;
​
/**
 * 自定义mybatis的配置类
 */
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;
    }
}
​

3.12 编写Mapper

package com.sixstar.mybatis.cfg;
​
/**
 * 用于封装执行的SQL语句和结果类型的全限定类名
 */
public class Mapper {
​
    private String queryString;//SQL
    private String resultType;//实体类的全限定类名
​
    public String getQueryString() {
        return queryString;
    }
​
    public void setQueryString(String queryString) {
        this.queryString = queryString;
    }
​
    public String getResultType() {
        return resultType;
    }
​
    public void setResultType(String resultType) {
        this.resultType = resultType;
    }
}
​

3.13 解析XML工具类介绍

1、不看注解的方式,因为还没有设计到

2、XML配置方式

2.1 首先取出mappers中的所有mapper标签,因为我们可以有很多mapper标签,所以先遍历集合,然后判断有没有resource,如果有那么就是XML配置的方式,那如果没有就是注解方式,如果是XML配置的首先我打印一句话使用的是XML配置,然后获取它的值它就是com/sixstar/dao/IUserDao.xml,接下来就把这个值作为一个参数传到下面这个方法里面,这个方法做了什么事呢。

2.2 它首先定义了一个Map,为什么要定义Map,其实刚刚我们已经分析过了,因为我们可能会有很多这样的信息需要存起来,所以我们需要一个集合,那为什么是Map不是List,原因就是我们有获取查找需求的时候,肯定Map查找的快一些,所以这个时候我们在这里面用一个Map,然后用到我们Resources.getResourceAsStream(mapperPath),再往后又是XML解析找到我们的select,找到之后取出id属性的值,再取出resultType的值,最后取出组成map中value的部分,也就是SQL语句,把这些都取出来之后,创建key,key是namespace+"."+id,前面我们也取了namespace,把这些都取出来之后创建了一个Mapper对象并且把它填充进去,最后把这个Mapper对象还有我们拼接好的key存入Mapper当中,这个时候你有一个,那么就会从Map集合里面添加一个key还有value,两个就添加两个,这些都执行完了之后我们都这个返回回去。

3.14 编写Configuration类的setMappers方法

这里不能用赋值的方式,不然我遍历一次它就会赋值一次值,如果我有两个Mapper它就会循环两次,第二次再调用赋值的时候,在这个setMappers方法中毫无疑问就把第一次的覆盖掉了,这个时候就会变成我们住配置文件里面的Mapper只能有一个了,显然这就是不对的,这里就不能使用直接赋值的方式。

    private Map<String,Mapper> mappers = new HashMap<String,Mapper>();
​
    public Map<String, Mapper> getMappers() {
        return mappers;
    }
​
    public void setMappers(Map<String, Mapper> mappers) {
        this.mappers.putAll(mappers);//此处需要使用追加的方式
    }

3.15编程SqlSessionFactoryBuilder

package com.sixstar.mybatis.sqlsession;
​
​
import com.sixstar.mybatis.cfg.Configuration;
import com.sixstar.mybatis.sqlsession.defaults.DefaultSqlSessionFactory;
import com.sixstar.mybatis.utils.XMLConfigBuilder;
​
import java.io.InputStream;
​
/**
 * 用于创建一个SqlSessionFactory对象
 */
public class SqlSessionFactoryBuilder {
​
    /**
     * 根据参数的字节输入流来构建一个SqlSessionFactory工厂
     * @param config
     * @return
     */
    public SqlSessionFactory build(InputStream config){
        //把解析得到的配置赋值给Configuration
        Configuration cfg = XMLConfigBuilder.loadConfiguration(config);
        //把解析的配置返回给实现类DefaultSqlSessionFactory
        return  new DefaultSqlSessionFactory(cfg);
    }
}
​

现在我们写完了SqlSessionFactoryBuilder还是没有工厂,我们现在的这个工厂是个接口,那么现在我们需要创建一个它的实现类。

3.16编写DefaultSqlSessionFactory

package com.sixstar.mybatis.sqlsession.defaults;
​
import com.sixstar.mybatis.cfg.Configuration;
import com.sixstar.mybatis.sqlsession.SqlSession;
import com.sixstar.mybatis.sqlsession.SqlSessionFactory;
​
​
/**
 * SqlSessionFactory接口的实现类
 */
public class DefaultSqlSessionFactory implements SqlSessionFactory{
​
    private Configuration cfg;
​
    public DefaultSqlSessionFactory(Configuration cfg){
        this.cfg = cfg;
    }
​
    /**
     * 用于创建一个新的操作数据库对象
     * @return
     */
    @Override
    public SqlSession openSession() {
        return new DefaultSqlSession(cfg);
    }
}
​

现在我们就拿到了Configuration里面的信息,里面包含了数据库连接的信息,以及我们需要执行的语句和封装的结果类型,此时我们的工厂就有了,那么就代表每个Session都有了,接下来实现SqlSession的实现类。

3.17编写DefaultSqlSession

package com.sixstar.mybatis.sqlsession.defaults;
​
import com.sixstar.mybatis.cfg.Configuration;
import com.sixstar.mybatis.sqlsession.SqlSession;
import com.sixstar.mybatis.sqlsession.proxy.MapperProxy;
import com.sixstar.mybatis.utils.DataSourceUtil;
​
import java.lang.reflect.Proxy;
import java.sql.Connection;
​
/**
 * SqlSession接口的实现类
 */
public class DefaultSqlSession implements SqlSession {
​
    private Configuration cfg;
​
    public DefaultSqlSession(Configuration cfg){
        this.cfg = cfg;
    }
​
    /**
     * 用于创建代理对象
     * @param daoInterfaceClass dao的接口字节码
     * @param <T>
     * @return
     */
    @Override
    public <T> T getMapper(Class<T> daoInterfaceClass) {
        return null;
    }
​
    /**
     * 用于释放资源
     */
    @Override
    public void close() {}
}
​

现在我们这些类和接口就都有了关系,但是还有很多事情需要去做,先说一下什么关系,读取配置文件用到了Resources,读出来的这个流,也就是我们找到的这些信息,交给了构建者,构建者使用工具类给我们构建了一个工厂对象,工厂里面的openSession给我们提供了一个Session方法,接下来我们就要在这个Session方法里面去实现代理对象,和查询所有的操作。

3.18编写DefaultSqlSession

package com.sixstar.mybatis.sqlsession.defaults;
​
import com.sixstar.mybatis.cfg.Configuration;
import com.sixstar.mybatis.sqlsession.SqlSession;
import com.sixstar.mybatis.sqlsession.proxy.MapperProxy;
import com.sixstar.mybatis.utils.DataSourceUtil;
​
import java.lang.reflect.Proxy;
import java.sql.Connection;
​
/**
​
 * SqlSession接口的实现类
 */
public class DefaultSqlSession implements SqlSession {
​
    private Configuration cfg;
​
    public DefaultSqlSession(Configuration cfg){
        this.cfg = cfg;
    }
​
    /**
     * 用于创建代理对象
     * @param daoInterfaceClass dao的接口字节码
     * @param <T>
     * @return
     */
    @Override
    public <T> T getMapper(Class<T> daoInterfaceClass) {
           Proxy.newProxyInstance(daoInterfaceClass.getClassLoader(),
           new Class[]{daoInterfaceClass},new MapperProxy(cfg.getMappers());
        return null;
    }
​
    /**
     * 用于释放资源
     */
    @Override
    public void close() {}
}
​

3.19编写MapperProxy

package com.sixstar.mybatis.sqlsession.proxy;
​
import com.sixstar.mybatis.cfg.Mapper;
import com.sixstar.mybatis.utils.Executor;
​
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.Map;
​
​
public class MapperProxy implements InvocationHandler {
​
    //map的key是全限定类名+方法名
    //value是SQL语句
    private Map<String,Mapper> mappers;
    //用于工具类
    private Connection conn;
​
    public MapperProxy(Map<String,Mapper> mappers,Connection conn){
        this.mappers = mappers;
        this.conn = conn;
    }
​
    /**
     * 用于对方法进行增强的,我们的增强其实就是调用selectList方法
     * @param proxy
     * @param method
     * @param args
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //1.获取方法名
        String methodName = method.getName();
        //2.获取方法所在类的名称
        String className = method.getDeclaringClass().getName();
        //3.组合key
        String key = className+"."+methodName;
        //4.获取mappers中的Mapper对象
        Mapper mapper = mappers.get(key);
        //5.判断是否有mapper
        if(mapper == null){
            throw new IllegalArgumentException("传入的参数有误");
        }
        //6.调用工具类执行查询所有
        return new Executor().selectList(mapper,conn);
    }
}
​

3.20 导入工具类

这个方法是可以直接用的,但是要传入一个Connection进来,如果没有Connection就不能执行,于是就可以在invoke方法中返回 new一个Executor,因为这个selectList返回的本身就是一个List就没有关系了

package com.sixstar.mybatis.utils;
​
import com.sixstar.mybatis.cfg.Mapper;
​
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.List;
​
/**
 * 负责执行SQL语句,并且封装结果集
 */
public class Executor {
​
    public <E> List<E> selectList(Mapper mapper, Connection conn) {
        PreparedStatement pstm = null;
        ResultSet rs = null;
        try {
            //1.取出mapper中的数据
            String queryString = mapper.getQueryString();//select * from user
            String resultType = mapper.getResultType();//com.sixstar.domain.User
            Class domainClass = Class.forName(resultType);
            //2.获取PreparedStatement对象
            pstm = conn.prepareStatement(queryString);
            //3.执行SQL语句,获取结果集
            rs = pstm.executeQuery();
            //4.封装结果集
            List<E> list = new ArrayList<E>();//定义返回值
            while(rs.next()) {
                //实例化要封装的实体类对象
                E obj = (E)domainClass.newInstance();
​
                //取出结果集的元信息:ResultSetMetaData
                ResultSetMetaData rsmd = rs.getMetaData();
                //取出总列数
                int columnCount = rsmd.getColumnCount();
                //遍历总列数
                for (int i = 1; i <= columnCount; i++) {
                    //获取每列的名称,列名的序号是从1开始的
                    String columnName = rsmd.getColumnName(i);
                    //根据得到列名,获取每列的值
                    Object columnValue = rs.getObject(columnName);
                    //给obj赋值:使用Java内省机制(借助PropertyDescriptor实现属性的封装)
                    PropertyDescriptor pd = new PropertyDescriptor(columnName,domainClass);//要求:实体类的属性和数据库表的列名保持一种
                    //获取它的写入方法
                    Method writeMethod = pd.getWriteMethod();
                    //把获取的列的值,给对象赋值
                    writeMethod.invoke(obj,columnValue);
                }
                //把赋好值的对象加入到集合中
                list.add(obj);
            }
            return list;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            release(pstm,rs);
        }
    }
​
    //释放资源
    private void release(PreparedStatement pstm,ResultSet rs){
        if(rs != null){
            try {
                rs.close();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
​
        if(pstm != null){
            try {
                pstm.close();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
}
​

3.21编写DefaultSqlSession

package com.sixstar.mybatis.sqlsession.defaults;
​
import com.sixstar.mybatis.cfg.Configuration;
import com.sixstar.mybatis.sqlsession.SqlSession;
import com.sixstar.mybatis.sqlsession.proxy.MapperProxy;
import com.sixstar.mybatis.utils.DataSourceUtil;
​
import java.lang.reflect.Proxy;
import java.sql.Connection;
​
/**
 * SqlSession接口的实现类
 */
public class DefaultSqlSession implements SqlSession {
​
    private Configuration cfg;
    private Connection connection;
​
    public DefaultSqlSession(Configuration cfg){
        this.cfg = cfg;
        connection = DataSourceUtil.getConnection(cfg);
    }
​
    /**
     * 用于创建代理对象
     * @param daoInterfaceClass dao的接口字节码
     * @param <T>
     * @return
     *这个方法返回的是Obj所以需要强转一下
     */
    @Override
    public <T> T getMapper(Class<T> daoInterfaceClass) {
        return (T) Proxy.newProxyInstance(daoInterfaceClass.getClassLoader(),
                new Class[]{daoInterfaceClass},new MapperProxy(cfg.getMappers(),connection));
    }
​
    /**
     * 用于释放资源
     */
    @Override
    public void close() {
        if(connection != null) {
            try {
                connection.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
​

3.22编写DataSourceUtil

package com.sixstar.mybatis.utils;
​
import com.sixstar.mybatis.cfg.Configuration;
​
import java.sql.Connection;
import java.sql.DriverManager;
​
/**
 * 用于创建数据源的工具类
 */
public class DataSourceUtil {
​
    /**
     * 用于获取一个连接
     * @param cfg
     * @return
     */
    public static Connection getConnection(Configuration cfg){
        try {
            //加载驱动
            Class.forName(cfg.getDriver());
            //返回连接信息
            return DriverManager.getConnection(cfg.getUrl(), cfg.getUsername(), cfg.getPassword());
        }catch(Exception e){
            throw new RuntimeException(e);
        }
    }
}
​

3.23使用注解

1、先改配置

<mapper class="com.sixstar.dao.IUserDao"/>

2、定义注解

package com.sixstar.mybatis.annotations;
​
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
​
/**
 * 查询的注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Select {
​
    /**
     * 配置SQL语句的
     * @return
     */
    String value();
}
​

3、添加注解标签

package com.sixstar.dao;
​
import com.sixstar.domain.User;
import com.sixstar.mybatis.annotations.Select;
​
import java.util.List;
​
public interface IUserDao {
​
    /**
     * 查询所有用户
     * @return
     */
    @Select("select * from user")
    List<User> findAll();
​
}
​

自定义 Mybatis 的设计模式说明

4.1 工厂模式( SqlSessionFactory )

image.png

4.2 代理模式(MapperProxyFactory)

image.png

4.3 构建者模式(SqlSessionFactoryBuilder)

image.png