自己实现一个 MyBatis 框架
   软件工程   0 评论   1157 浏览

自己实现一个 MyBatis 框架

   软件工程   0 评论   1157 浏览

前言

MyBatis 是一个非常优秀的持久层应用框架,目前几乎已经一统天下。既然是持久层框架,那么一定是对于数据库的操作,Java 中谈到数据库操作,一定少不了 JDBC。那么 MyBatis 比传统的 JDBC 好在哪那?MyBatis 又在哪方面做了优化那?

JDBC

如果我们需要查询所有用户,传统的 JDBC 会这样写。

public static void main(String[] args) {
    //声明Connection对象
    Connection con = null;
    try {
        //加载驱动程序
        Class.forName("com.mysql.jdbc.Driver");
        //创建 connection 对象
        con = DriverManager.getConnection("jdbc:mysql://localhost:3306/db","username","password");

        //使用 connection 对象创建statement 或者 PreparedStatement 类对象,用来执行SQL语句
        Statement statement = con.createStatement();
        //要执行的SQL语句
        String sql = "select * from user";
        //3.ResultSet类,用来存放获取的结果集!!
        ResultSet rs = statement.executeQuery(sql);

        String job = "";
        String id = "";
        while(rs.next()){
            //获取job这列数据
            job = rs.getString("job");
            //获取userId这列数据
            id = rs.getString("userId");

            //输出结果
            System.out.println(id + "\t" + job);
        }
    } catch(ClassNotFoundException e) {
        e.printStackTrace();
    } catch(SQLException e) {
        //数据库连接失败异常处理
        e.printStackTrace();
    }catch (Exception e) {
        e.printStackTrace();
    }finally{
        rs.close();
        con.close();
    }
}

通过上面的代码,我们可以将 JDBC 对于数据库的操作总结为以下几个步骤:

  1. 加载驱动
  2. 创建连接,Connection 对象
  3. 根据 Connection 创建 Statement 或者 PreparedStatement 来执行 SQL 语句
  4. 返回结果集到 ResultSet 中
  5. 手动将 ResultSet 映射到 JavaBean 中

传统的 JDBC 操作的问题也一目了然,整体非常繁琐,也不够灵活,执行一个 SQL 查询就要写一堆代码。

MyBatis

来看看 MyBatis 代码如何查询数据库。几行代码就完成了数据库查询操作,并且将数据库查询出来的结果映射到了 JavaBean 中了。我们的代码没有加入 Spring Mybatis,加入 Spring 后整体流程会复杂很多,不方便我们理解。

//获取 sqlSession,sqlSession 相当于传统 JDBC 的 Conection
public static SqlSession getSqlSession(){
    InputStream configFile = new FileInputStream(filePath);
      SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder.build(configFile);
      return sqlSessionFactory.openSession();
}

//使用 sqlSession 获得对应的 mapper,mapper 用来执行 sql 语句。
public static User get(SqlSession sqlSession, int id){
      UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
      return userMapper.selectByPrimaryKey(id);
}

我们来对 MyBatis 操作数据库做一个总结:

  1. 使用配置文件构建 SqlSessionFactory
  2. 使用 SqlSessionFactory 获得 SqlSession,SqlSession 相当于传统 JDBC 的 Conection
  3. 使用 SqlSession 得到 Mapper
  4. 用 Mapper 来执行 SQL 语句,并返回结果直接封装到 JavaBean 中

源码分析

大家平时应该经常使用 MyBatis 框架,对于 SqlSessionFactory、SqlSession、Mapper 等也有一些概念。下面我们从源码来分析怎么实现这些概念。

前置知识

先给出一个大部分框架的代码流程,方便大家理解框架。下面的图片就说明了接口、抽象类和实现类的关系,我们自己写代码时也要多学习这种思想。

Mybatis中至少用到以下设计模式,建造者模式例如SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder;工厂模式例如SqlSessionFactory、ObjectFactory、MapperProxyFactory;单例模式例如ErrorContext和LogFactory;代理模式Mybatis实现的核心,比如MapperProxy、ConnectionLogger,用的jdk的动态代理;还有executor.loader包使用了cglib或者javassist达到延迟加载的效果;还有其它装饰者、适配器、模板方法模式等。

带着结果看过程

看源码对于很多人来说都是一个比较枯燥和乏味的过程,如果不做抽象和总结,会觉得非常乱。另外,看源码不要去扣某个细节,尽量从宏观上理解它。这样带着结果看过程你就会知道设计者为什么这么做。

先给出整个 MyBatis 框架的架构图,大家先有一个印象:

原理分析

说明,我们讲解的是原生的 MyBatis 框架,并不是与 Spring 结合的 MyBatis 框架。

还是把上面 MyBatis 操作数据库的代码拿过来,方便我们与源码对照。

//获取 sqlSession,sqlSession 相当于传统 JDBC 的 Conection
public static SqlSession getSqlSession(){
    //步骤一
    InputStream configFile = new FileInputStream(filePath);
    //步骤二
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder.build(configFile);
    return sqlSessionFactory.openSession();
}
    
//使用 sqlSession 获得对应的 mapper,mapper 用来执行 sql 语句。
public static User get(SqlSession sqlSession, int id){
    //步骤三
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    return userMapper.selectByPrimaryKey(id);
}

MyBatis 框架的第一步就是加载我们数据库的相关信息,比如用户名、密码等。以及我们在 XML 文件中写的 SQL 语句。

//配置文件中指定了数据库相关的信息和写 sql 语句的 mapper 相关信息,稍后我们需要读取并加载到我们的配置类中。
<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://localhost:3306/db"/>
                    <property name="username" value="root"/>
                    <property name="password" value="123456"/>
                </dataSource>
            </transactionManager>
        </environment>
    </environments>
</configuration>
    
<mappers>
    <mapper resource="xml/UserMapper.xml"/>
</mappers>

第二步就是通过读取到的配置文件信息,构建一个 SqlSessionFactory。

通过 openSession 方法返回了一个 sqlSession,我们来看看 openSession 方法做了什么。

//我们来重点看看 openSession 做了什么操作, DefaultSqlSessionFactory.java
@Override
public SqlSession openSession() {
    return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);
}
public Configuration getConfiguration() {
    return this.configuration;
}
//这个函数里面有着事务控制相关的代码。
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    DefaultSqlSession var8;
    try {
        Environment environment = this.configuration.getEnvironment();
        TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
        //根据上面的参数得到 TransactionFactory,通过 TransactionFactory 生成一个 Transaction,可以理解为这个 SqlSession 的事务控制器
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        // 将这个事务控制器封装在 Executor 里
        Executor executor = this.configuration.newExecutor(tx, execType);
        // 使用 configuration 配置类,Executor,和 configuration(是否自动提交) 来构建一个 DefaultSqlSession。
        var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
    } catch (Exception var12) {
        this.closeTransaction(tx);
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + var12, var12);
    } finally {
        ErrorContext.instance().reset();
    }
    return var8;
}

看了上面的一大段代码你可能会觉得蒙,没关系,我们来划重点,最终结果返回了一个 DefaultSqlsession。

// 使用 configuration 配置类(我们上面读取的配置文件就需要加载到这个类中),Executor(包含了数据事务控制相关信息),和 autoCommit(是否自动提交) 来构建一个 DefaultSqlSession。
var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);

有了这个 sqlSession 之后,我们就可以实现所有对数据库的操作了,因为我们已经把所有的信息加载到这里面了。数据库信息、SQL 信息、SQL 语句执行器等。当然我们一般使用这个 sqlSession 获得对应的 mapper 接口类,然后用这个接口类查询数据库。

既然所有东西都封装在 sqlSession 中,先来看看 sqlSession 的组成部分。

SqlSession 的接口定义:里面定义了增删改查和提交回滚等方法。

public interface SqlSession extends Closeable {
    <T> T selectOne(String var1);

    <T> T selectOne(String var1, Object var2);

    <E> List<E> selectList(String var1);

    <E> List<E> selectList(String var1, Object var2);

    <E> List<E> selectList(String var1, Object var2, RowBounds var3);

    <K, V> Map<K, V> selectMap(String var1, String var2);

    <K, V> Map<K, V> selectMap(String var1, Object var2, String var3);

    <K, V> Map<K, V> selectMap(String var1, Object var2, String var3, RowBounds var4);

    <T> Cursor<T> selectCursor(String var1);

    <T> Cursor<T> selectCursor(String var1, Object var2);

    <T> Cursor<T> selectCursor(String var1, Object var2, RowBounds var3);

    void select(String var1, Object var2, ResultHandler var3);

    void select(String var1, ResultHandler var2);

    void select(String var1, Object var2, RowBounds var3, ResultHandler var4);

    int insert(String var1);

    int insert(String var1, Object var2);

    int update(String var1);

    int update(String var1, Object var2);

    int delete(String var1);

    int delete(String var1, Object var2);

    void commit();

    void commit(boolean var1);

    void rollback();

    void rollback(boolean var1);

    List<BatchResult> flushStatements();

    void close();

    void clearCache();

    Configuration getConfiguration();

    <T> T getMapper(Class<T> var1);

    Connection getConnection();
}

接下来用 sqlSession 获取对应的 Mapper。

DefaultSqlSession 的 getMapper 实现:

public <T> T getMapper(Class<T> type) {
    return this.configuration.getMapper(type, this);
}
    
//从 configuration 里面 getMapper,Mapper 就在 Configuration 里
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return this.mapperRegistry.getMapper(type, sqlSession);
}

MapperRegistry 里 getMapper 的最终实现,同时我们需要思考一个问题,我们的 sqlSession 接口里面只定义了抽象的增删改查,而这个接口并没有任何实现类,那么这个 XML 到底是如何与接口关联起来并生成实现类那?通过 MapperRegistry 可以得出答案,那就是动态代理。

public class MapperRegistry {
    private final Configuration config;
    // 用一个 Map 来存储接口和 xml 文件之间的映射关系,key 应该是接口,但是 value 是 MapperProxyFactory
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap();

    public MapperRegistry(Configuration config) {
        this.config = config;
    }

    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        //获取到这个接口对应的 MapperProxyFactory。
        MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
        if (mapperProxyFactory == null) {
            throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        } else {
            try {
                //用上一步获取的 MapperProxyFactory 和 sqlSession 构建对应的 Class
                return mapperProxyFactory.newInstance(sqlSession);
            } catch (Exception var5) {
                throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
            }
        }
    }
}

最终的结果是生成一个 mapper 接口的动态代理类,通过这个类,我们实现对数据库的增删改查。

接下来我们看看 newInstance 的具体实现:

public T newInstance(SqlSession sqlSession) {
    // mapperInterface 就是接口
    MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
    return this.newInstance(mapperProxy);
}
    
protected T newInstance(MapperProxy<T> mapperProxy) {
    //动态代理,这里的动态代理有一些不一样
    return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}

为什么说这里的动态代理有一些不一样那?我们先看看正常流程的动态代理,接口,和接口实现类是必须的。而我们的 Mapper 接口只有充满了 SQL 语句的 XML 文件,没有具体实现类。

与传统的动态代理相比,MyBatis 的 Mapper 接口是没有实现类的,那么它又是怎么实现动态代理的那?

我们来看一下 MapperProxy 的源码:

public class MapperProxy<T> implements InvocationHandler, Serializable {
    private static final long serialVersionUID = -6424540398559729838L;
    private final SqlSession sqlSession;
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache;

    public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
    }
    // 正常的动态代理中 Object proxy 这个参数应该是接口的实现类
    // com.yoyling.pkg.UserMapper@5a123uf
    // 现在里面是 org.apache.ibatis.binding.MapperProxy@6y213kn, 这俩面
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, args);
            }

            if (this.isDefaultMethod(method)) {
                return this.invokeDefaultMethod(proxy, method, args);
            }
        } catch (Throwable var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }
        // Mapper 走这个流程,先尝试在缓存里获取 method
        MapperMethod mapperMethod = this.cachedMapperMethod(method);
        return mapperMethod.execute(this.sqlSession, args);
    }

    private MapperMethod cachedMapperMethod(Method method) {
        MapperMethod mapperMethod = (MapperMethod)this.methodCache.get(method);
        if (mapperMethod == null) {
            // mapperMethod 的构建,通过接口名,方法,和 xml 配置(通过 sqlSession 的 Configuration 获得)
            mapperMethod = new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration());
            //通过 execute 执行方法,因为 sqlSession 封装了 Executor,所以还要传进来,execute 方法使用
            //sqlSession 里面的方法。
            this.methodCache.put(method, mapperMethod);
        }

        return mapperMethod;
    }

}

来看 MapperMethod 的定义:

// command 里面包含了方法名,比如 com.yoyling.pkg.selectByPrimaryKey
// type, 表示是 SELECT,UPDATE,INSERT,或者 DELETE
// method 是方法的签名
public class MapperMethod {
    private final MapperMethod.SqlCommand command;
    private final MapperMethod.MethodSignature method;

    public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
        this.command = new MapperMethod.SqlCommand(config, mapperInterface, method);
        this.method = new MapperMethod.MethodSignature(config, mapperInterface, method);
    }
}

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;//返回结果
        //INSERT操作
    if (SqlCommandType.INSERT == command.getType()) {
        //处理参数
        Object param = method.convertArgsToSqlCommandParam(args);
        //调用sqlSession的insert方法
        result = rowCountResult(sqlSession.insert(command.getName(), param));
    }
}

通过 sqlSession 来执行我们的 SQL 语句,返回结果,动态代理的方法调用结束。

进入 DefaultSqlSession 执行对应的 SQL 语句。

public <T> T selectOne(String statement, Object parameter) {
    List<T> list = this.selectList(statement, parameter);
    if (list.size() == 1) {
        return list.get(0);
    } else if (list.size() > 1) {
        throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
        return null;
    }
}

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    List var5;
    try {
        // 这里又需要 configuration 来获取对应的 statement
        // MappedStatement 里面有 xml 文件,和要执行的方法,就是 xml 里面的 id,statementType,以及 sql 语句。
        MappedStatement ms = this.configuration.getMappedStatement(statement);
        // 用 executor 执行 query,executor 里面应该是包装了 JDBC。
        var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception var9) {
        throw ExceptionFactory.wrapException("Error querying database.  Cause: " + var9, var9);
    } finally {
        ErrorContext.instance().reset();
    }
    return var5;
}

Executor 的实现类里面执行 query 方法。

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    Cache cache = ms.getCache();
    if (cache != null) {
        this.flushCacheIfRequired(ms);
        if (ms.isUseCache() && resultHandler == null) {
            this.ensureNoOutParams(ms, boundSql);
            List<E> list = (List)this.tcm.getObject(cache, key);
            if (list == null) {
                list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                this.tcm.putObject(cache, key, list);
            }
            return list;
        }
    }
    // 使用 delegate 去 query,delegate 是 SimpleExecutor。里面使用 JDBC 进行数据库操作。
    return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

手动实现MyBatis框架

整体流程

  1. 首先创建 SqlSessionFactory 实例,SqlSessionFactory 就是创建 SqlSession 的工厂类。
  2. 加载配置文件创建 Configuration 对象,配置文件包括数据库相关配置文件以及我们在 XML 文件中写的 SQL。
  3. 通过 SqlSessionFactory 创建 SqlSession。
  4. 通过 SqlSession 获取 mapper 接口动态代理。
  5. 动态代理回调 SqlSession 中某查询方法。
  6. SqlSession 将查询方法转发给 Executor。
  7. Executor 基于 JDBC 访问数据库获取数据,最后还是通过 JDBC 操作数据库。
  8. Executor 通过反射将数据转换成 POJO 并返回给 SqlSession。
  9. 将数据返回给调用者。

项目整体使用 Maven 构建,01_01mybatis01_02mybatis_annotation 是脱离 Spring 的 MyBatis 使用的例子,大家可以先熟悉以下 Mybatis 框架如何使用,代码就不在讲解了。

首先按照我们以前的使用 MyBatis 代码时的流程,创建 Mapper 接口、XML 文件,和 POJO 以及集一些配置文件,这几个文件我们和 mybatis-demo 创建一样的即可,方便我们比较结果。

Mapper 接口,这里面定义两个抽象方法,根据主键查找用户和查找所有用户:

package com.yoyling.mybatis.mapper;
import com.yoyling.mybatis.entity.User;
import java.util.List;
    
public interface UserMapper {
    User selectByPrimaryKey(long userId);
    List<User> selectAll();
}

XML 文件,里面是上面两个抽象方法的具体 SQL 实现,完全消防官方 XML 文件的写法,需要注意 namespace、id、resultType、SQL 语句这几个点,都是我们后面代码需要处理的。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yoyling.mybatis.mapper.UserMapper">
    <select id="selectByPrimaryKey" resultType="com.yoyling.mybatis.entity.User">
        select *
        from user
        where id = #{id}
    </select>
    
    <select id="selectAll" resultType="com.yoyling.mybatis.entity.User">
        select *
        from user
    </select>
</mapper>

最后是我们的实体类,它的属性与数据库的表相对应:

package com.yoyling.mybatis.entity;

import java.util.Date;

public class User {

    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 + '\'' +
                '}';
    }
}

最后一个配置文件,数据库连接配置文件 db.propreties:

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatistest
jdbc.username=root
jdbc.password=root

配置文件和一些测试的必须类已经写完了,首先我们需要把这些配置信息加载到 Configuration 配置类中。

先定义一个类来加载写 SQL 语句的 XML 文件,上面我们说过要注意四个点,namespace、id、resultType、SQL 语句,我们写对应的属性来保存它,代码很简单,就不多讲了。

package com.yoyling.mybatis.confiuration;

/**
 * XML 中的 sql 配置信息加载到这个类中
 */
public class MappedStatement {

    private String namespace;

    private String id;

    private String resultType;

    private String sql;

    public String getNamespace() {
        return namespace;
    }

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

    public String getId() {
        return id;
    }

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

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

接下来我们定义一个 Configuration 总配置类,来保存 db.propeties 里面的属性和 XML 文件的 SQL 信息,Configuration 类里面的文件对应我们配置文件中的属性。

package com.yoyling.mybatis.confiuration;

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

/**
 * 所有的配置信息
 */
public class Configuration {

    private String jdbcDriver;

    private String jdbcUrl;

    private String jdbcPassword;

    private String jdbcUsername;

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

    public Map<String, MappedStatement> getMappedStatement() {
        return mappedStatement;
    }

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

    public String getJdbcDriver() {
        return jdbcDriver;
    }

    public void setJdbcDriver(String jdbcDriver) {
        this.jdbcDriver = jdbcDriver;
    }

    public String getJdbcUrl() {
        return jdbcUrl;
    }

    public void setJdbcUrl(String jdbcUrl) {
        this.jdbcUrl = jdbcUrl;
    }

    public String getJdbcPassword() {
        return jdbcPassword;
    }

    public void setJdbcPassword(String jdbcPassword) {
        this.jdbcPassword = jdbcPassword;
    }

    public String getJdbcUsername() {
        return jdbcUsername;
    }

    public void setJdbcUsername(String jdbcUsername) {
        this.jdbcUsername = jdbcUsername;
    }
}

按照上面的流程图,我们来创建一个 SqlSessionFactory 工厂类,这个类有两个功能,一个是加载配置文件信息到 Configuration 类中,另一个是创建 SqlSession。

SqlSessionFactory 抽象模版:

package com.yoyling.mybatis.factory;
    
import com.yoyling.mybatis.sqlsession.SqlSession;
    
public interface SqlSessionFactory {
    SqlSession openSession();
}

创建 SqlSessionFactory 的 Default 实现类,Default 实现类主要完成了两个功能,加载配置信息到 Configuration 对象里,实现创建 SqlSession 的功能。

package com.yoyling.mybatis.factory;

import com.yoyling.mybatis.confiuration.Configuration;
import com.yoyling.mybatis.confiuration.MappedStatement;
import com.yoyling.mybatis.sqlsession.DefaultSqlSession;
import com.yoyling.mybatis.sqlsession.SqlSession;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

/**
 * 1.初始化时就完成了 configuration 的实例化
 * 2.工厂类,生成 sqlSession
 */
public class DefaultSqlSessionFactory implements SqlSessionFactory{

    private final Configuration configuration = new Configuration();

    // xml 文件存放的位置
    private static final String MAPPER_CONFIG_LOCATION = "mappers";

    // 数据库信息存放的位置
    private static final String DB_CONFIG_FILE = "db.properties";


    public DefaultSqlSessionFactory() {
        loadDBInfo();
        loadMapperInfo();
    }

    private void loadDBInfo() {
        InputStream db = this.getClass().getClassLoader().getResourceAsStream(DB_CONFIG_FILE);
        Properties p = new Properties();

        try {
            p.load(db);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //将配置信息写入Configuration 对象
        configuration.setJdbcDriver(p.get("jdbc.driver").toString());
        configuration.setJdbcUrl(p.get("jdbc.url").toString());
        configuration.setJdbcUsername(p.get("jdbc.username").toString());
        configuration.setJdbcPassword(p.get("jdbc.password").toString());

    }

    //解析并加载xml文件
    private void loadMapperInfo(){
        URL resources = null;
        resources = this.getClass().getClassLoader().getResource(MAPPER_CONFIG_LOCATION);
        File mappers = new File(resources.getFile());
        //读取文件夹下面的文件信息
        if(mappers.isDirectory()){
            File[] files = mappers.listFiles();
            for(File file:files){
                loadMapperInfo(file);
            }
        }
    }

    private void loadMapperInfo(File file){
        SAXReader reader = new SAXReader();
        //通过read方法读取一个文件转换成Document 对象
        Document document = null;
        try {
            document = reader.read(file);
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        //获取根结点元素对象<mapper>
        Element e = document.getRootElement();
        //获取命名空间namespace
        String namespace = e.attribute("namespace").getData().toString();
        //获取select,insert,update,delete子节点列表
        List<Element> selects = e.elements("select");
        List<Element> inserts = e.elements("insert");
        List<Element> updates = e.elements("update");
        List<Element> deletes = e.elements("delete");

        List<Element> all = new ArrayList<>();
        all.addAll(selects);
        all.addAll(inserts);
        all.addAll(updates);
        all.addAll(deletes);

        //遍历节点,组装成 MappedStatement 然后放入到configuration 对象中
        for(Element ele:all){
            MappedStatement mappedStatement = new MappedStatement();
            String id = ele.attribute("id").getData().toString();
            String resultType = ele.attribute("resultType").getData().toString();
            String sql = ele.getData().toString();

            mappedStatement.setId(namespace+"."+id);
            mappedStatement.setResultType(resultType);
            mappedStatement.setNamespace(namespace);
            mappedStatement.setSql(sql);
            // xml 文件中的每个 sql 方法都组装成 mappedStatement 对象,以 namespace+"."+id 为 key, 放入
            // configuration 配置类中。
            configuration.getMappedStatement().put(namespace+"."+id,mappedStatement);
        }
    }

    @Override
    public SqlSession openSession() {
        // openSession 方法创建一个 DefaultSqlSession,configuration 配置类作为 构造函数参数传入
        return new DefaultSqlSession(configuration);
    }
}

在 SqlSessionFactory 里创建了 DefaultSqlSession,我们看看它的具体实现。SqlSession 里面应该封装了所有数据库的具体操作和一些获取 mapper 实现类的方法。

SqlSession 接口,定义模版方法

package com.yoyling.mybatis.sqlsession;

import java.util.List;

/**
 * 封装了所有数据库的操作
 * 所有功能都是基于 Excutor 来实现的,Executor 封装了 JDBC 操作
 */
public interface SqlSession {
    /**
     * 根据传入的条件查询单一结果
     * @param statement  namespace+id,可以用做 key,去 configuration 里面获取 sql 语句,resultType
     * @param parameter  要传入 sql 语句中的查询参数
     * @param <T> 返回指定的结果对象
     * @return
     */
    <T> T selectOne(String statement, Object parameter);
    <T> List<T> selectList(String statement, Object parameter);
    <T> T getMapper(Class<T> type);
}  

Default 的 SqlSession 实现类。里面需要传入 Executor,这个 Executor 里面封装了 JDBC 操作数据库的流程。我们重点关注 getMapper 方法,使用动态代理生成一个加强类。这里面最终还是把数据库的相关操作转给 SqlSession,使用 Mapper 能使编程更加优雅。

package com.yoyling.mybatis.sqlsession;

import com.yoyling.mybatis.bind.MapperProxy;
import com.yoyling.mybatis.confiuration.Configuration;
import com.yoyling.mybatis.confiuration.MappedStatement;
import com.yoyling.mybatis.executor.Executor;
import com.yoyling.mybatis.executor.SimpleExecutor;

import java.lang.reflect.Proxy;
import java.util.List;

public class DefaultSqlSession implements  SqlSession {

    private final Configuration configuration;

    private Executor executor;

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

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

    @Override
    public <T> List<T> selectList(String statement, Object parameter) {
        MappedStatement ms = configuration.getMappedStatement().get(statement);
        // 我们的查询方法最终还是交给了 Executor 去执行,Executor 里面封装了 JDBC 操作。传入参数包含了 sql 语句和 sql 语句需要的参数。
        return executor.query(ms,parameter);
    }

    @Override
    public <T> T getMapper(Class<T> type) {
        //通过动态代理生成了一个实现类,我们重点关注,动态代理的实现,它是一个 InvocationHandler,传入参数是 this,就是 sqlSession 的一个实例。
        MapperProxy mp = new MapperProxy(this);
        //给我一个接口,还你一个实现类
        return (T)Proxy.newProxyInstance(type.getClassLoader(),new Class[]{type},mp);
    }
}

来看看我们的 InvocationHandler 如何实现 invoke 方法:

package com.yoyling.mybatis.bind;

import com.yoyling.mybatis.sqlsession.SqlSession;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;

/**
 * 将请求转发给 sqlSession
 */
public class MapperProxy implements InvocationHandler {

    private SqlSession sqlSession;

    public MapperProxy(SqlSession sqlSession) {
        this.sqlSession = sqlSession;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(method.getDeclaringClass().getName()+"."+method.getName());
        //最终还是将执行方法转给 sqlSession,因为 sqlSession 里面封装了 Executor
        //根据调用方法的类名和方法名以及参数,传给 sqlSession 对应的方法
        if(Collection.class.isAssignableFrom(method.getReturnType())){
            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]);
        }
    }
}

获取 Mapper 接口的实现类我们已经实现了,通过动态代理调用 sqlSession 的方法。那么就剩最后一个重要的工作了,那就是实现 Exectuor 类去操作数据库,封装 JDBC。

Executor 抽象模版,我们只实现了 query、update 等操作慢慢增加。

package com.yoyling.mybatis.executor;

import com.yoyling.mybatis.confiuration.MappedStatement;

import java.util.List;

/**
 * mybatis 核心接口之一,定义了数据库操作的最基本的方法,JDBC,sqlSession的所有功能都是基于它来实现的
 */
public interface Executor {
    /**
     * 查询接口
     * @param ms 封装sql 语句的 mappedStatemnet 对象,里面包含了 sql 语句,resultType 等。
     * @param parameter 传入sql 参数
     * @param <E> 将数据对象转换成指定对象结果集返回
     * @return
     */
    <E> List<E> query(MappedStatement ms, Object parameter);
}

Executor 接口的实现类,主要是对 JDBC 的封装,和利用反射方法将结果映射到 resultType 对应的实体类中

package com.yoyling.mybatis.executor;

import com.yoyling.mybatis.confiuration.Configuration;
import com.yoyling.mybatis.confiuration.MappedStatement;
import com.yoyling.mybatis.util.ReflectionUtil;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class SimpleExecutor implements Executor {

    private final Configuration configuration;

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

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter) {
        System.out.println(ms.getSql().toString());

        List<E> ret = new ArrayList<>(); //返回结果集
        try {
            Class.forName(configuration.getJdbcDriver());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
            connection = DriverManager.getConnection(configuration.getJdbcUrl(), configuration.getJdbcUsername(), configuration.getJdbcPassword());
            String regex = "#\\{([^}])*\\}";
            // 将 sql 语句中的 #{userId} 替换为 ?
            String  sql = ms.getSql().replaceAll(regex,"");
            preparedStatement = connection.prepareStatement(sql);
            //处理占位符,把占位符用传入的参数替换
            parametersize(preparedStatement, parameter);
            resultSet = preparedStatement.executeQuery();
            handlerResultSet(resultSet, ret,ms.getResultType());
        }catch (SQLException e){
            e.printStackTrace();
        }finally {
            try {
                resultSet.close();
                preparedStatement.close();
                connection.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }

        return ret;
    }


    private void parametersize(PreparedStatement preparedStatement,Object parameter) throws SQLException{
        if(parameter instanceof Integer){
            preparedStatement.setInt(1,(int)parameter);
        }else if(parameter instanceof  Long){
            preparedStatement.setLong(1,(Long)parameter);
        }else if(parameter instanceof  String){
            preparedStatement.setString(1,(String)parameter);
        }
    }

    private <E> void handlerResultSet(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 entity = clazz.newInstance();
                //通过反射工具 将 resultset 中的数据填充到 entity 中
                ReflectionUtil.setPropToBeanFromResultSet(entity, resultSet);
                ret.add((E) entity);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

到目前未知,我们简单版的 MyBatis 框架已经实现了,我们来写一个测试类测试一下。

package com.yoyling.mybatis;

import com.yoyling.mybatis.entity.User;
import com.yoyling.mybatis.factory.DefaultSqlSessionFactory;
import com.yoyling.mybatis.factory.SqlSessionFactory;
import com.yoyling.mybatis.mapper.UserMapper;
import com.yoyling.mybatis.sqlsession.SqlSession;

public class TestDemo {

    public static void main(String[] args) {
        SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory();

        SqlSession sqlSession = sqlSessionFactory.openSession();

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

        User user = mapper.selectByPrimaryKey(121312312313L);

        System.out.println(user.toString());
    }
}

看一下测试的结果,整个 MyBatis 框架已经实现完成了,当然有很多地方需要完善,比如 XML 中的 SQL 语句处处理还缺很多功能,目前只支持 select 等,希望大家能通过源码解读和自己写的过程明白 MyBatis 的具体实现要点。

最后给出源码地址:https://github.com/rawchen/MyBatisFrame,如果您觉得学到了东西请在 GitHub 点赞。

本文由 RawChen 发表, 最后编辑时间为:2021-11-13 13:23
如果你觉得我的文章不错,不妨鼓励我继续写作。

发表评论
选择表情
Top