可怕!你没看错,这次确实是纯手工实现一个MyBatis框架

目录

  • 前言

  • JDBC

  • MyBatis

  • 源码分析

  • 前置知识

  • 原理分析

  • 自己实现一个 MyBatis 框架

# 前言

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框架_第1张图片

可怕!你没看错,这次确实是纯手工实现一个MyBatis框架

带着结果看过程

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

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

可怕!你没看错,这次确实是纯手工实现一个MyBatis框架_第2张图片

可怕!你没看错,这次确实是纯手工实现一个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 相关信息,稍后我们需要读取并加载到我们的配置类中。
  
    
      
        
          
            
            
            
            
          
        
      
    
  
  
    
  

第二步就是通过读取到的配置文件信息,构建一个 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 selectOne(String var1);
  
       T selectOne(String var1, Object var2);
  
       List selectList(String var1);
  
       List selectList(String var1, Object var2);
  
       List selectList(String var1, Object var2, RowBounds var3);
  
       Map selectMap(String var1, String var2);
  
       Map selectMap(String var1, Object var2, String var3);
  
       Map selectMap(String var1, Object var2, String var3, RowBounds var4);
  
       Cursor selectCursor(String var1);
  
       Cursor selectCursor(String var1, Object var2);
  
       Cursor 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 flushStatements();
  
      void close();
  
      void clearCache();
  
      Configuration getConfiguration();
  
       T getMapper(Class var1);
  
      Connection getConnection();
  }

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

DefaultSqlSession 的 getMapper 实现:

 

 public  T getMapper(Class type) {
    return this.configuration.getMapper(type, this);
  }
  
  //从 configuration 里面 getMapper,Mapper 就在 Configuration 里
  public  T getMapper(Class 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, MapperProxyFactory> knownMappers = new HashMap();
  
      public MapperRegistry(Configuration config) {
          this.config = config;
      }
  
      public  T getMapper(Class type, SqlSession sqlSession) {
        //获取到这个接口对应的 MapperProxyFactory。
          MapperProxyFactory 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 mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
    return this.newInstance(mapperProxy);
  }
  
  protected T newInstance(MapperProxy mapperProxy) {
    //动态代理,这里的动态代理有一些不一样
    return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
  }

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

可怕!你没看错,这次确实是纯手工实现一个MyBatis框架_第3张图片

可怕!你没看错,这次确实是纯手工实现一个MyBatis框架

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

我们来看一下 MapperProxy 的源码:

 

 public class MapperProxy implements InvocationHandler, Serializable {
      private static final long serialVersionUID = -6424540398559729838L;
      private final SqlSession sqlSession;
      private final Class mapperInterface;
      private final Map methodCache;
  
      public MapperProxy(SqlSession sqlSession, Class mapperInterface, Map methodCache) {
          this.sqlSession = sqlSession;
          this.mapperInterface = mapperInterface;
          this.methodCache = methodCache;
      }
      // 正常的动态代理中 Object proxy 这个参数应该是接口的实现类
      // com.paul.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.paul.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 selectOne(String statement, Object parameter) {
    List 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  List 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  List 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  List 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 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 框架

整体流程

可怕!你没看错,这次确实是纯手工实现一个MyBatis框架_第4张图片

可怕!你没看错,这次确实是纯手工实现一个MyBatis框架

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

项目整体使用 Maven 构建,mybatis-demo 是脱离 Spring 的 MyBatis 使用的例子,大家可以先熟悉一下 Mybatis 框架如何使用,代码就不再讲解了。paul-mybatis 是我们自己实现的 MyBatis 框架。

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

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

 

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

 


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

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

 

package com.paul.mybatis.entity;
  
  public class User {
  
      private long userId;
      private String userName;
      private int sex;
      private String role;
  
      public long getUserId() {
          return userId;
      }
      public void setUserId(long userId) {
          this.userId = userId;
      }
      public String getUserName() {
          return userName;
      }
      public void setUserName(String userName) {
          this.userName = userName;
      }
      public int getSex() {
          return sex;
      }
      public void setSex(int sex) {
          this.sex = sex;
      }
      public String getRole() {
          return role;
      }
      public void setRole(String role) {
          this.role = role;
      }
  }

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

 

 jdbc.driver=com.mysql.jdbc.Driver
  jdbc.url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf8
  jdbc.username=root
  jdbc.password=root

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

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

 

package com.paul.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.paul.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 mappedStatement = new HashMap<>();
  
      public Map getMappedStatement() {
          return mappedStatement;
      }
  
      public void setMappedStatement(Map 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.paul.mybatis.factory;
  
  import com.paul.mybatis.sqlsession.SqlSession;
  
  public interface SqlSessionFactory {
      SqlSession openSession();
  }

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

 

 package com.paul.mybatis.factory;
  
  import com.paul.mybatis.confiuration.Configuration;
  import com.paul.mybatis.confiuration.MappedStatement;
  import com.paul.mybatis.sqlsession.DefaultSqlSession;
  import com.paul.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();
          }
          //获取根结点元素对象
          Element e = document.getRootElement();
          //获取命名空间namespace
          String namespace = e.attribute("namespace").getData().toString();
          //获取select,insert,update,delete子节点列表
          List selects = e.elements("select");
          List inserts = e.elements("insert");
          List updates = e.elements("update");
          List deletes = e.elements("delete");
  
          List 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.paul.mybatis.sqlsession;
  
  import java.util.List;
  
  /**
   *
   * 封装了所有数据库的操作
   * 所有功能都是基于 Excutor 来实现的,Executor 封装了 JDBC 操作
   *
   *
   */
  public interface SqlSession {
      /**
       * 根据传入的条件查询单一结果
       * @param statement  namespace+id,可以用做 key,去 configuration 里面获取 sql 语句,resultType
       * @param parameter  要传入 sql 语句中的查询参数
       * @param  返回指定的结果对象
       * @return
       */
       T selectOne(String statement, Object parameter);
       List selectList(String statement, Object parameter);
       T getMapper(Class type);
  }  

Default 的 SqlSession 实现类。里面需要传入 Executor,这个 Executor 里面封装了 JDBC 操作数据库的流程。我们重点关注 getMapper 方法,使用动态代理生成一个加强类。

数据库的相关操作转给 SqlSession,使用 Mapper 能使编程更加优雅。

 

   package com.paul.mybatis.sqlsession;
  
  import com.paul.mybatis.bind.MapperProxy;
  import com.paul.mybatis.confiuration.Configuration;
  import com.paul.mybatis.confiuration.MappedStatement;
  import com.paul.mybatis.executor.Executor;
  import com.paul.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 selectOne(String statement, Object parameter) {
          List 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  List selectList(String statement, Object parameter) {
          MappedStatement ms = configuration.getMappedStatement().get(statement);
          // 我们的查询方法最终还是交给了 Executor 去执行,Executor 里面封装了 JDBC 操作。传入参数包含了 sql 语句和 sql 语句需要的参数。
          return executor.query(ms,parameter);
      }
  
      @Override
      public  T getMapper(Class type) {
        //通过动态代理生成了一个实现类,我们重点关注,动态代理的实现,它是一个 InvocationHandler,传入参数是 this,就是 sqlSession 的一个实例。
          MapperProxy mp = new MapperProxy(this);
          //给我一个接口,还你一个实现类
          return (T)Proxy.newProxyInstance(type.getClassLoader(),new Class[]{type},mp);
      }
  }

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

 

package com.paul.mybatis.bind;

  import com.paul.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.paul.mybatis.executor;
  
  import com.paul.mybatis.confiuration.MappedStatement;
  
  import java.util.List;

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

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

 

package com.paul.mybatis.executor;


import com.paul.mybatis.confiuration.Configuration;
import com.paul.mybatis.confiuration.MappedStatement;
import com.paul.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  List query(MappedStatement ms, Object parameter) {
        System.out.println(ms.getSql().toString());

        List 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  void handlerResultSet(ResultSet resultSet, List ret,String className){
        Class clazz = null;
        //通过反射获取类对象
        try {
            clazz = (Class)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.paul.mybatis;
  
  import com.paul.mybatis.entity.User;
  import com.paul.mybatis.factory.DefaultSqlSessionFactory;
  import com.paul.mybatis.factory.SqlSessionFactory;
  import com.paul.mybatis.mapper.UserMapper;
  import com.paul.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 的具体实现要点。

你可能感兴趣的:(Java,Spring,数据库,数据库,mybatis,mysql,java,spring)