手写 Mybatis,“整整” Mybatis源码

2023年6月17日08:08:55

前言

前两天写了一个 手写Spring ioc 框架,“撸撸”Spring 源码我们今天来整整Mybatis。

mybaits 在 ORM 框架中,可算是半壁江山了,由于它是轻量级,半自动加载,灵活性和易拓展性。深受广大公司的喜爱,所以我们程序开发也离不开 mybatis 。

很多朋友对 mybatis 源码没什么了解,或者想看但是不知道怎么看的苦恼吗?

归根结底,我们还是需要知道为什么会有 mybatis ,mybatis 解决了什么问题? 想要知道 mybatis 解决了什么问题,就要知道传统的 JDBC 操作存在哪些痛点才促使 mybatis 的诞生。 我们带着这些疑问,再来一步步学习吧。

内容稍微有点长!耐心阅读!

另外本人整理收藏了20年多家公司面试知识点整理 ,以及各种Java核心知识点免费分享给大家,下方只是部分截图
想要资料的话也可以点击直接进入:暗号:csdn,免费获取。

手写 Mybatis,“整整” Mybatis源码

传统JDBC的弊端

所以我们先来来看下原始 JDBC 的操作

我们知道最原始的数据库操作。分为以下几步

  1. 获取 connection 连接
  2. 获取 preparedStatement
  3. 参数替代占位符
  4. 获取执行结果 resultSet
  5. 解析封装 resultSet 到对象中返回。

如下是原始 JDBC 的查询代码,存在哪些问题?

public static void main(String[] args) {
        String dirver="com.mysql.jdbc.Driver";
        String url="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8";
        String userName="root";
        String password="123456";

        Connection connection=null;
        List<User> userList=new ArrayList<>();
        try {
            Class.forName(dirver);
            connection= DriverManager.getConnection(url,userName,password);

            String sql="select * from user where username=?";
            PreparedStatement preparedStatement=connection.prepareStatement(sql);
            preparedStatement.setString(1,"张三");
            System.out.println(sql);
            ResultSet resultSet=preparedStatement.executeQuery();

            User user=null;
            while(resultSet.next()){
                user=new User();
                user.setId(resultSet.getInt("id"));
                user.setUsername(resultSet.getString("username"));
                user.setPassword(resultSet.getString("password"));
                userList.add(user);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

        if (!userList.isEmpty()) {
            for (User user : userList) {
                System.out.println(user.toString());
            }
        }

    }

大家是不是发现了上面有哪些不友好的地方?

我这里总结了以下几点:

  1. 数据库的连接信息存在硬编码,即是写死在代码中的。
  2. 每次操作都会建立和释放 connection 连接,操作资源的不必要的浪费。
  3. sql 和参数存在硬编码。
  4. 将返回结果集封装成实体类麻烦,要创建不同的实体类,并通过 set 方法一个个的注入。

存在上面的问题,所以 mybatis 就对上述问题进行了改进。 对于硬编码,我们很容易就想到配置文件来解决。mybatis 也是这么解决的。 对于资源浪费,我们想到使用连接池,mybatis 也是这个解决的。 对于封装结果集麻烦,我们想到是用 JDK 的反射机制,好巧,mybatis 也是这么解决的。

设计思路

既然如此,我们就来写一个自定义持久层框架,来解决上述问题,当然是参照 mybatis 的设计思路,这样我们在写完之后,再来看 mybatis 的源码就恍然大悟,这个地方这样配置原来是因为这样啊。

我们分为使用端和框架端两部分。

学海无涯,我们一起勉力前行!

Ps:有需要的小伙伴可以点击直接进入:暗号:csdn,免费获取。

手写 Mybatis,“整整” Mybatis源码

使用端

我们在使用 mybatis 的时候是不是需要使用 SqlMapConfig.xml 配置文件,用来存放数据库的连接信息,以及 mapper.xml 的指向信息。mapper.xml 配置文件用来存放 sql 信息。

所以我们在使用端来创建两个文件 SqlMapConfig.xml 和 mapper.xml。

框架端

框架端要做哪些事情呢?如下:

  1. 获取配置文件。也就是获取到使用端的 SqlMapConfig.xml 以及 mapper.xml 的文件
  2. 解析配置文件。对获取到的文件进行解析,获取到连接信息,sql,参数,返回类型等等。这些信息都会保存在 configuration 这个对象中。
  3. 创建 SqlSessionFactory,目的是创建 SqlSession 的一个实例。
  4. 创建 SqlSession ,用来完成上面原始 JDBC 的那些操作。

那在 SqlSession 中 进行了哪些操作呢?

  1. 获取数据库连接
  2. 获取 sql ,并对 sql 进行解析
  3. 通过内省,将参数注入到 preparedStatement 中
  4. 执行 sql
  5. 通过反射将结果集封装成对象

使用端实现

好了,上面说了一下,大概的设计思路,主要也是仿照 mybatis 主要的类实现的,保证类名一致,方便我们后面阅读源码。我们先来配置好使用端吧,我们创建一个 maven 项目。

在项目中,我们创建一个 User 实体类

public class User {
    private Integer id;
    private String username;
    private String password;
    private String birthday;
    //getter()和 setter()方法
}

创建 SqlMapConfig.xml 和 Mapper.xml SqlMapConfig.xml

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
    <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&amp;characterEncoding=utf8&amp;useUnicode=true&amp;useSSL=false"></property>
    <property name="userName" value="root"></property>
    <property name="password" value="123456"></property>

    <mapper resource="UserMapper.xml">
    </mapper>
</configuration>

可以看到我们 xml 中就配置了数据库的连接信息,以及 mapper 一个索引。mybatis 中的 SqlMapConfig.xml 中还包含其他的标签,只是丰富了功能而已,所以我们只用最主要的。

mapper.xml 是每个类的 sql 都会生成一个对应的 mapper.xml 。我们这里就用 User 类来说吧,所以我们就创建一个 UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="cn.quellanan.dao.UserDao">
    <select id="selectAll" resultType="cn.quellanan.pojo.User">
        select * from user
    </select>
    <select id="selectByName" resultType="cn.quellanan.pojo.User" paramType="cn.quellanan.pojo.User">
        select * from user where username=#{username}
    </select>
</mapper>

可以看到有点 mybatis 里面文件的味道,有 namespace 表示命名空间,id 唯一标识,resultType 返回结果集的类型,paramType 参数的类型。 我们使用端先创建到这,主要是两个配置文件。

我们接下来看看框架端是怎么实现的。

框架端实现

架端,我们按照上面的设计思路一步一步来。

获取配置

怎么样获取配置文件呢?我们可以使用 JDK 自带自带的类 Resources 加载器来获取文件。我们创建一个自定义 Resource 类来封装一下:

import java.io.InputStream;
public class Resources {
    public  static InputStream getResources(String path){
        //使用系统自带的类 Resources 加载器来获取文件。
        return Resources.class.getClassLoader().getResourceAsStream(path);
    }
}

这样通过传入路径,就可以获取到对应的文件流啦。

解析配置文件

上面获取到了 SqlMapConfig.xml 配置文件,我们现在来解析它。 不过在此之前,我们需要做一点准备工作,就是解析的内存放到什么地方?

所以我们来创建两个实体类 Mapper 和 Configuration 。

Mapper Mapper 实体类用来存放使用端写的 mapper.xml 文件的内容,我们前面说了里面有 id、sql、resultType 和 paramType .所以我们创建的 Mapper 实体如下:

public class Mapper {
    private String id;
    private Class<?> resultType;
    private Class<?> parmType;
    private String sql;
    //getter()和 setter()方法
}

这里我们为什么不添加 namespace 的值呢? 聪明的你肯定发现了,因为 mapper 里面这些属性表明每个 sql 都对应一个 mapper , 而 namespace 是一个命名空间,算是 sql 的上一层,所以在 mapper 中暂时使用不到,就没有添加了。

Configuration Configuration 实体用来保存 SqlMapConfig 中的信息。所以需要保存数据库连接,我们这里直接用 JDK 提供的 DataSource 。还有一个就是 mapper 的信息。每个 mapper 有自己的标识,所以这里采用 hashMap 来存储。如下:

public class Configuration {

    private DataSource dataSource;
    HashMap <String,Mapper> mapperMap=new HashMap<>();
    //getter()和 setter 方法
    }

XmlMapperBuilder

做好了上面的准备工作,我们先来解析 mapper 吧。我们创建一个 XmlMapperBuilder 类来解析。通过 dom4j 的工具类来解析 XML 文件。我这里用的 dom4j 依赖为:

         <dependency>
            <groupId>org.dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>2.1.3</version>
        </dependency>

思路:

  1. 获取文件流,转成 document。
  2. 获取根节点,也就是 mapper。获取根节点的 namespace 属性值
  3. 获取 select 节点,获取其 id,sql , resultType ,paramType
  4. 将 select 节点的属性封装到 Mapper 实体类中。
  5. 同理获取 update/insert/delete 节点的属性值封装到 Mapper 中
  6. 通过 namespace.id 生成 key 值将 mapper 对象保存到 Configuration 实体中的 HashMap 中。
  7. 返回 Configuration 实体
    代码如下:
public class XmlMapperBuilder {
    private Configuration configuration;
    public XmlMapperBuilder(Configuration configuration){
        this.configuration=configuration;
    }

    public Configuration loadXmlMapper(InputStream in) throws DocumentException, ClassNotFoundException {
        Document document=new SAXReader().read(in);

        Element rootElement=document.getRootElement();
        String namespace=rootElement.attributeValue("namespace");

        List<Node> list=rootElement.selectNodes("//select");

        for (int i = 0; i < list.size(); i++) {
            Mapper mapper=new Mapper();
            Element element= (Element) list.get(i);
            String id=element.attributeValue("id");
            mapper.setId(id);
            String paramType = element.attributeValue("paramType");
            if(paramType!=null && !paramType.isEmpty()){
                mapper.setParmType(Class.forName(paramType));
            }
            String resultType = element.attributeValue("resultType");
            if (resultType != null && !resultType.isEmpty()) {
                mapper.setResultType(Class.forName(resultType));
            }
            mapper.setSql(element.getTextTrim());
            String key=namespace+"."+id;
            configuration.getMapperMap().put(key,mapper);
        }
        return configuration;
    }

}

上面我只解析了 select 标签。大家可以解析对应 insert/delete/uupdate 标签,操作都是一样的。

XmlConfigBuilder

我们再来解析一下 SqlMapConfig.xml 配置信息思路是一样的。

1、获取文件流,转成 document。
2、获取根节点,也就是 configuration。
3、获取根节点中所有的 property 节点,并获取值,也就是获取数据库连接信息
4、创建一个 dataSource 连接池
5、将连接池信息保存到 Configuration 实体中
6、获取根节点的所有 mapper 节点
7、调用 XmlMapperBuilder 类解析对应 mapper 并封装到 Configuration 实体中
8、完
代码如下:

public class XmlConfigBuilder {
    private Configuration configuration;
    public XmlConfigBuilder(Configuration configuration){
        this.configuration=configuration;
    }

    public Configuration loadXmlConfig(InputStream in) throws DocumentException, PropertyVetoException, ClassNotFoundException {

        Document document=new SAXReader().read(in);

        Element rootElement=document.getRootElement();

        //获取连接信息
        List<Node> propertyList=rootElement.selectNodes("//property");
        Properties properties=new Properties();

        for (int i = 0; i < propertyList.size(); i++) {
            Element element = (Element) propertyList.get(i);
            properties.setProperty(element.attributeValue("name"),element.attributeValue("value"));
        }
        //是用连接池
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        dataSource.setDriverClass(properties.getProperty("driverClass"));
        dataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
        dataSource.setUser(properties.getProperty("userName"));
        dataSource.setPassword(properties.getProperty("password"));
        configuration.setDataSource(dataSource);

        //获取 mapper 信息
        List<Node> mapperList=rootElement.selectNodes("//mapper");
        for (int i = 0; i < mapperList.size(); i++) {
            Element element= (Element) mapperList.get(i);
            String mapperPath=element.attributeValue("resource");
            XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration);
            configuration=xmlMapperBuilder.loadXmlMapper(Resources.getResources(mapperPath));
        }
        return configuration;
    }
}

创建 SqlSessionFactory

完成解析后我们创建 SqlSessionFactory 用来创建 Sqlseesion 的实体,这里为了尽量还原 mybatis 设计思路,也也采用的工厂设计模式。 SqlSessionFactory 是一个接口,里面就一个用来创建 SqlSessionf 的方法。 如下:

public interface SqlSessionFactory {
    public SqlSession openSqlSession();
}

单单这个接口是不够的,我们还得写一个接口的实现类,所以我们创建一个 DefaultSqlSessionFactory。 如下:

public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private Configuration configuration;

    public DefaultSqlSessionFactory(Configuration configuration) {
        this.configuration = configuration;
    }
    public SqlSession openSqlSession() {
        return new DefaultSqlSeeion(configuration);
    }
}

可以看到就是创建一个 DefaultSqlSeeion 并将包含配置信息的 configuration 传递下去。DefaultSqlSeeion 就是 SqlSession 的一个实现类。

创建 SqlSession
在 SqlSession 中我们就要来处理各种操作了,比如 selectList,selectOne,insert, update , delete 等等。 如下:

public interface SqlSession {

    /**
     * 条件查找
     * @param statementid  唯一标识,namespace.selectid
     * @param parm  传参,可以不传也可以一个,也可以多个
     * @param <E>
     * @return
     */
    public <E> List<E> selectList(String statementid,Object...parm) throws Exception;

    public <T> T selectOne(String statementid, Object...parm) throws Exception;

    public int insert(String statementid, Object...parm) throws Exception;
    public int update(String statementid, Object...parm) throws Exception;
    public int delete(String statementid, Object...parm) throws Exception;
    public void commit() throws Exception;


    /**
     * 使用代理模式来创建接口的代理对象
     * @param mapperClass
     * @param <T>
     * @return
     */
    public <T> T getMapper(Class<T> mapperClass);

然后我们创建 DefaultSqlSeeion 来实现 SqlSeesion 。

public class DefaultSqlSeeion implements SqlSession {

private Configuration configuration;

private Executer executer=new SimpleExecuter();

public DefaultSqlSeeion(Configuration configuration) {
this.configuration = configuration;
}

@Override
public <E> List<E> selectList(String statementid, Object... parm) throws Exception {
Mapper mapper=configuration.getMapperMap().get(statementid);
List<E> query = executer.query(configuration, mapper, parm);
return query;
}

@Override
public <T> T selectOne(String statementid, Object... parm) throws Exception {
List<Object> list =selectList(statementid, parm);
if(list.size()==1){
return (T) list.get(0);
}else{
throw new RuntimeException("返回结果过多");
}
}

@Override
public int insert(String statementid, Object... parm) throws Exception {
return update(statementid,parm);
}

@Override
public int update(String statementid, Object... parm) throws Exception {
Mapper mapper=configuration.getMapperMap().get(statementid);
int update = executer.update(configuration, mapper, parm);
return update;
}

@Override
public int delete(String statementid, Object... parm) throws Exception {
return update(statementid,parm);
}

@Override
public void commit() throws Exception {
executer.commit();
}

@Override
public <T> T getMapper(Class<T> mapperClass) {

Object proxyInstance

  • 作者:花名提莫
  • 原文链接:https://blog.csdn.net/qq_41770757/article/details/109385242
    更新时间:2023年6月17日08:08:55 ,共 10294 字。