手写一个简单的MyBatis框架

本文转载于:https://www.qfcwx.top/2019/04/23/shou-xie-yi-ge-jian-dan-de-mybatis/
作者:清风

本文代码参考了:"享学课堂——手写MyBatis"的课程。如有侵权请立即与本人联系,本人将及时删除相关的代码和文献。

MyBatis核心流程的三大阶段

一、初始化阶段:读取xml配置文件和注解中的配置信息,创建配置对象,并完成各个模块的初始化工作。
二、代理阶段:封装iBatis的编程模型,使用mapper接口开发的初始化工作。
三、数据读写阶段:通过SqlSession完成SQL解析,参数的映射,SQL的执行,结果的反射解析过程。

SqlSession

  1. 意味着创建会话,代表一次与数据库的连接。
  2. 是MyBatis对外提供数据访问的主要API(iBatis的编程方式)
  3. 实际上SqlSession的功能是基于Executor来实现的。

实际操作

首先创建连接数据库的配置文件db.properties

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=UTC&useSSL=false&nullNamePatternMatchesAll=true
username=root
password=root

pom中导入相关的约束,最重要的就是dom4j,用来解析mapper.xml文件。还有MySQL的驱动。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.qfcwx</groupId>
    <artifactId>simple-mybatis</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- mybatis的依赖 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.6</version>
        </dependency>

        <!-- 依赖dom4j来解析mapper.xml -->
        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.15</version>
        </dependency>
    </dependencies>
</project>

新建一个实体类。

package com.qfcwx.pojo;

import java.io.Serializable;

/**
 * @ClassName: User
 * @Description: TODO
 * @Date: 2019/4/18 17:36
 * @Version 1.0
 **/
public class User implements Serializable {

    private Long id;
    private String username;
    private String password;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    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 "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

有三个属性,分别与数据库对应,然后创建mapper接口和mapper.xml文件。

public interface UserMapper {

    /**
     * //TODO 根据id查询user对象
     *
     * @param id 用户id
     * @return com.qfcwx.pojo.User
     */
    User selectUserById(Long id);

    /**
     * //TODO 查询所有用户
     *
     * @return java.util.List<com.qfcwx.pojo.User>
     */
    List<User> selectList();
}

然后在resource文件夹新建一个mapper文件夹,建立UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.qfcwx.mapper.UserMapper">

    <select id="selectUserById" resultType="com.qfcwx.pojo.User">
    SELECT
        id,username,password
    FROM 
    	user
    WHERE 
    	id = ?
  </select>
    
    <select id="selectList" resultType="com.qfcwx.pojo.User">
    SELECT
        id,username,password
    FROM 
    	user
  </select>

</mapper>

准备工作完成,接下来就是重头戏了。
需要创建一个与mapper.xml中标签和属性对应的实体类。其中包含(namespace、id、resultType、sql…)等。

package com.qfcwx.config;

/**
 * @ClassName: MappedStatement
 * @Description: TODO 映射mapper.xml的实体类namespace,id,resultType,sql...等,对应了一条sql语句
 * @Date: 2019/4/18 11:14
 * @Version 1.0
 **/
public class MappedStatement {

    private String namespace;
    private String sourceId;
    private String resultType;
    private String sql;

    public String getNamespace() {
        return namespace;
    }

    public void setNamespace(String namespace) {
        this.namespace = namespace;
    }

    public String getSourceId() {
        return sourceId;
    }

    public void setSourceId(String sourceId) {
        this.sourceId = sourceId;
    }

    public String getResultType() {
        return resultType;
    }

    public void setResultType(String resultType) {
        this.resultType = resultType;
    }

    public String getSql() {
        return sql;
    }

    public void setSql(String sql) {
        this.sql = sql;
    }
}

一个MappedStatement对象对应一条sql语句。接下来。新建一个配置类。读取所有配置文件(db.properties,mapper.xml)

package com.qfcwx.config;

import java.util.HashMap;
import java.util.Map;

/**
 * @ClassName: Configuration
 * @Description: TODO 对应mybatis-config.xml。读取所有配置文件,包括db.properties,mapper.xml
 * @Date: 2019/4/18 11:20
 * @Version 1.0
 **/
public class Configuration {

    private String driver;
    private String url;
    private String userName;
    private String passWord;

    private Map<String, MappedStatement> statementMap = new HashMap<String, MappedStatement>();

    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;
    }

    public Map<String, MappedStatement> getStatementMap() {
        return statementMap;
    }

    public void setStatementMap(Map<String, MappedStatement> statementMap) {
        this.statementMap = statementMap;
    }
}

接下来就进入第一个阶段,读取xml配置文件和注解中的配置信息,创建配置对象,并完成各个模块的初始化工作。
SqlSession是MyBatis的关键对象,通过java操作MyBatis时,可看到,它是由SqlSessionFactory这个工厂来创建的。所以需要先完成SqlSessionFactory的相关代码。

public class SqlSessionFactory {

    private final Configuration configuration = new Configuration();

    /**
     * 记录mapper.xml存放的位置
     **/
    private static final String MAPPER_CONFIG_LOCATION = "mapper";
    /**
     * 记录数据库连接信息存放的文件
     **/
    private static final String DB_CONFIG_FILE = "db.properties";

    public SqlSessionFactory() {
        loadDBInfo();
        loadMappersInfo();
    }

    /**
     * //TODO 读取数据库配置文件信息
     *
     * @param
     * @return void
     **/
    private void loadDBInfo() {
        //加载数据库信息配置文件
        InputStream stream = SqlSessionFactory.class.getClassLoader().getResourceAsStream(DB_CONFIG_FILE);
        Properties properties = new Properties();
        try {
            properties.load(stream);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //将数据库配置信息写入configuration对象中
        configuration.setDriver(properties.get("driver").toString());
        configuration.setUrl(properties.get("url").toString());
        configuration.setUserName(properties.get("username").toString());
        configuration.setPassWord(properties.get("password").toString());
    }

    /**
     * //TODO 获取指定文件下的所有mapper.xml文件
     *
     * @param
     * @return void
     **/
    private void loadMappersInfo() {
        URL resource = null;
        resource = SqlSessionFactory.class.getClassLoader().getResource(MAPPER_CONFIG_LOCATION);
        //获取指定文件夹信息
        File file = new File(resource.getFile());
        if (file.isDirectory()) {
            File[] mappers = file.listFiles();
            //遍历文件夹下所有的mapper.xml文件,解析后,注册到configuration中
            for (File mapper : mappers) {
                loadMapper(mapper);
            }
        }
    }

    /**
     * //TODO 对mapper.xml文件解析
     *
     * @param mapper
     * @return void
     **/
    private void loadMapper(File mapper) {
        //创建SAXReader对象
        SAXReader saxReader = new SAXReader();
        //通过read方法读取一个文件,转换成Document对象
        Document document = null;
        try {
            document = saxReader.read(mapper);
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        //获取根节点元素对象<mapper>
        Element rootElement = document.getRootElement();
        //获取命名空间
        String namespace = rootElement.attribute("namespace").getData().toString();
        //获取子节点<select>标签
        List<Element> selects = rootElement.elements("select");
        //遍历select节点,将信息记录到MappedStatement对象,并登记到Configuration对象中
        for (Element element : selects) {
            MappedStatement statement = new MappedStatement();
            String id = element.attribute("id").getData().toString();
            String resultType = element.attribute("resultType").getData().toString();
            //读取sql语句信息
            String sql = element.getData().toString();

            String sourceId = namespace + "." + id;
            //给MappedStatement对象赋值
            statement.setSourceId(sourceId);
            statement.setNamespace(namespace);
            statement.setResultType(resultType);
            statement.setSql(sql);
            configuration.getStatementMap().put(sourceId, statement);
        }

    }

    public SqlSession openSession() {
        return new DefaultSqlSession(configuration);
    }

}

以上代码都有相应的注释,同时,也很基础,很容易理解。这里就不过多的进行解释了。
SqlSessionFactory在这里主要有两个功能:
①. 读取配置文件,解析信息,填充到configuration中。
②. 生产SqlSession
MyBatis的源码中,SqlSessionFactory是一个接口。同时,我们也可以定义为接口。由子类来实现。但是,这里为了方便,就省去了定义。
Configuration这个对象在全局中,只能存在一个,所以将其定义为final。
openSession()这个方法获得一个SqlSession对象。
定义一个SqlSession的接口。

public interface SqlSession {

    /**
     * //TODO 根据传入的条件查询单一结果
     *
     * @param statement 方法对应的sql语句,namespace + id
     * @param parameter 要传入到sql语句中的查询参数
     * @return T
     */
    <T> T selectOne(String statement, Object parameter);

    /**
     * //TODO 查询集合
     *
     * @param statement 方法对应的sql语句,namespace + id
     * @param parameter 要传入到sql语句中的查询参数
     * @return java.util.List<E>
     */
    <E> List<E> selectList(String statement, Object parameter);


    /**
     * //TODO 获取mapper对象
     *
     * @param type 对象的类型
     * @return T
     */
    <T> T getMapper(Class<T> type);


}

由其子类来实现SqlSession,并重写其中的方法。

public class DefaultSqlSession implements SqlSession {

    private Configuration configuration;

    private final Executor executor;

    public DefaultSqlSession(Configuration configuration) {
        super();
        this.configuration = configuration;
        this.executor = new DefaultExecutor(configuration);
    }


    @Override
    public <T> T selectOne(String statement, Object parameter) {
        List<T> list = this.selectList(statement, parameter);
        if (list == null || list.size() == 0) {
            return null;
        }
        if (list.size() == 1) {
            return list.get(0);
        } else {
            throw new RuntimeException("too man result");
        }
    }

    @Override
    public <E> List<E> selectList(String statement, Object parameter) {
        MappedStatement smt = this.configuration.getStatementMap().get(statement);
        return executor.query(smt, parameter);
    }

    @Override
    public <T> T getMapper(Class<T> type) {
        MappedProxy mappedProxy = new MappedProxy(this);
        return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[]{type}, mappedProxy);
    }

}

在这里会有很多疑惑的地方。
①. Executor这个对象是干什么的?
②. MappedProxy这个对象的干什么的?
③. selectOne为什么调用selectList的方法?
下面我们一一解答。

问题一

上面说到SqlSession的功能是基于Executor来实现的。其实,MyBatis中,SqlSession对数据库的操作,是委托给执行器Executor来完成的。并且每一个SqlSession都拥有一个新的Executor对象。

MyBatis源码中Executor的方法:

public interface Executor {

  ResultHandler NO_RESULT_HANDLER = null;

  int update(MappedStatement ms, Object parameter) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

  <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;

  List<BatchResult> flushStatements() throws SQLException;

  void commit(boolean required) throws SQLException;

  void rollback(boolean required) throws SQLException;

  CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

  boolean isCached(MappedStatement ms, CacheKey key);

  void clearLocalCache();

  void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);

  Transaction getTransaction();

  void close(boolean forceRollback);

  boolean isClosed();

  void setExecutorWrapper(Executor executor);

}

可以看到的是。Executor中定义了查询及更新的方法。还有事务提交和回滚。其实现类有五个:

  1. SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。
  2. ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map<String, Statement>内,供下一次使用。
  3. BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理的;BatchExecutor相当于维护了多个桶,每个桶里都装了很多属于自己的SQL,就像苹果蓝里装了很多苹果,番茄蓝里装了很多番茄,最后,再统一倒进仓库。
  4. CachingExecutor:装饰设计模式典范,先从缓存中获取查询结果,存在就返回,不存在,再委托给Executor delegate去数据库取,delegate可以是上面任一的SimpleExecutor、ReuseExecutor、BatchExecutor。
  5. ClosedExecutor:仅作为一种标识,和Serializable标记接口作用相当。

基本上Executor就是这些了。详细的资料,读者可自行查看其源码。

问题二

这是一个代理类。因为Mapper接口没有具体的实现类。所以,通过代理的方式,创建Mapper的代理对象。让其执行相关的数据库操作。

问题三

因为查询单个对象还是集合。都进行了查询操作。
selectOne查询一个对象,对应的List集合中只能有一个值,所以直接使用list.get(0)则可以取出。若是出现了多值的情况,则程序抛出异常。
selectList则查询所有结果。

上面的问题都回答完了,若是读者还有不懂得地方,请给博主留言或者自查搜索引擎解决。
回到程序中。使用动态代理创建一个Mapper接口的实现类。

public class MappedProxy implements InvocationHandler {

    private SqlSession sqlSession;

    public MappedProxy(SqlSession sqlSession) {
        super();
        this.sqlSession = sqlSession;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Class<?> returnType = method.getReturnType();
        //调用这个方法的class或接口与参数cls表示的类或接口相同,或者是参数cls表示的类或接口的父类,则返回tr。
        //判断返回值是否为Collection的子类
        if (Collection.class.isAssignableFrom(returnType)) {
            return sqlSession.selectList(method.getDeclaringClass().getName() + "." + method.getName(), args == null ? null : args[0]);
        } else {
            return sqlSession.selectOne(method.getDeclaringClass().getName() + "." + method.getName(), args == null ? null : args[0]);
        }
    }
}

因为MappedStatement对象中封装了对应的sql语句。可以根据其返回值,判断代理对象调用哪个方法。
最后的操作就交给Executor对象了。由它来操作数据库。

package com.qfcwx.executor;

import com.qfcwx.config.MappedStatement;

import java.util.List;

/**
 * @ClassName: Executor
 * @Description: TODO Mybatis的核心接口之一,定义了数据库操作最基本的方法,SqlSession的功能都基于它来实现
 * @Date: 2019/4/19 10:22
 * @Version 1.0
 **/
public interface Executor {

    /**
     * //TODO
     *
     * @param statement
     * @param parameter
     * @return java.util.List<E>
     **/
    <E> List<E> query(MappedStatement statement, Object parameter);
}

上面定义一个接口。写一个查询的方法。相信大家在写原生JDBC操作数据库的时候,都用过Dbutils吧。里面的方法,只有Query和Update。
这是因为不管查询一个,还是查询多个,都是查询的方法。而增删改都只是数据的更新。
MyBatis中使用了连接池,而博主使用了最原生的JDBC来操作数据库。

public class Connections {

    public static Connection getConnection(Configuration configuration) {
        Connection connection = null;
        try {
            //加载驱动
            Class.forName(configuration.getDriver());
            connection = DriverManager.getConnection(configuration.getUrl(), configuration.getUserName(), configuration.getPassWord());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return connection;

    }
}

上面提取了一个获取数据库连接的对象,这里就以MySQL为例。最后一步就是让Executor的子类来重写query方法,进行数据库的操作。

package com.qfcwx.executor.impl;

import com.qfcwx.config.Configuration;
import com.qfcwx.config.MappedStatement;
import com.qfcwx.executor.Executor;
import com.qfcwx.jdbc.Connections;
import com.qfcwx.pojo.User;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

/**
 * @ClassName: DefaultExecutor
 * @Description: TODO  操作数据库的操作,实则mybatis底层封装的连接池
 * @Date: 2019/4/19 11:10
 * @Version 1.0
 **/
public class DefaultExecutor implements Executor {

    private Configuration configuration;

    public DefaultExecutor(Configuration configuration) {
        super();
        this.configuration = configuration;
    }

    @Override
    public <E> List<E> query(MappedStatement statement, Object parameter) {
        List<E> list = new ArrayList<E>();
        //获取连接
        Connection connection = Connections.getConnection(configuration);
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
            //创建预编译PreparedStatement对象,从MappedStatement获取sql语句
            preparedStatement = connection.prepareStatement(statement.getSql());
            //处理sql中的占位符
            parameterSize(preparedStatement, parameter);
            //执行查询操作获取resultSet
            resultSet = preparedStatement.executeQuery();
            //将结果集通过反射技术,填充到list中
            handleResult(resultSet, list, statement.getResultType());
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                if (resultSet != null) {
                    resultSet.close();
                }
                if (preparedStatement != null) {
                    preparedStatement.close();
                }
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return list;
    }

    /**
     * //TODO 对PreparedStatement中的占位符进行处理
     *
     * @param statement
     * @param parameter
     * @return void
     */
    private void parameterSize(PreparedStatement statement, Object parameter) throws SQLException {
        if (parameter instanceof Integer) {
            statement.setInt(1, (Integer) parameter);
        } else if (parameter instanceof Long) {
            statement.setLong(1, (Long) parameter);
        } else if (parameter instanceof String) {
            statement.setString(1, (String) parameter);
        }
    }

    /**
     * //TODO 读取ResultSet中的数据,并转换成目标对象
     *
     * @param resultSet
     * @param ret
     * @param className
     * @return void
     */
    private <E> void handleResult(ResultSet resultSet, List<E> ret, String className) {
        Class<E> clazz = null;
        //通过反射获取类的对象
        try {
            clazz = (Class<E>) Class.forName(className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        try {
            while (resultSet.next()) {
                //通过反射实例化对象
                Object model = clazz.newInstance();
                //使用反射工具将ResultSet中的数据填充到entity中
                long id = resultSet.getLong("id");
                String username = resultSet.getString("username");
                String password = resultSet.getString("password");
                User user = (User) model;
                user.setId(id);
                user.setUsername(username);
                user.setPassword(password);
                ret.add((E) user);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

基本上就是JDBC操作数据库的代码,也没什么好说的。通过反射来进行结果的反向解析。
最后就是测试的代码了。

public class Test {

    public static void main(String[] args) {

        SqlSessionFactory sqlSessionFactory = new SqlSessionFactory();

        SqlSession sqlSession = sqlSessionFactory.openSession();
        System.out.println(sqlSession);

        UserMapper mapper = sqlSession.getMapper(UserMapper.class);

        User user = mapper.selectUserById(10L);
        System.out.println(user);

        List<User> userList = mapper.selectList();
        System.out.println(userList);


    }
}

测试结果,读者可以根据自己的实践去检验。

想要通过一篇技术文就了解MyBatis的底层原理,这是不现实的。但是,通过慢慢的积累,每一个猿类都能成为开源框架的贡献者之一。
感兴趣的读者,可以使用编辑器一行一行debug来看。上面的代码还是比较基础的,也许是博主没理解那么深刻,讲的云里雾里。但是,相信各位开发人员都能通过慢慢揣摩,弄懂真正的奥义。

下面也会给出项目的源码的地址。供大家下载学习。

GitHub地址: https://github.com/Qingfengchuiwoxin/Mybatis

相信奇迹的人,本身就和奇迹一样了不起。

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值