• 欢迎访问开心洋葱网站,在线教程,推荐使用最新版火狐浏览器和Chrome浏览器访问本网站,欢迎加入开心洋葱 QQ群
  • 为方便开心洋葱网用户,开心洋葱官网已经开启复制功能!
  • 欢迎访问开心洋葱网站,手机也能访问哦~欢迎加入开心洋葱多维思维学习平台 QQ群
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏开心洋葱吧~~~~~~~~~~~~~!
  • 由于近期流量激增,小站的ECS没能经的起亲们的访问,本站依然没有盈利,如果各位看如果觉着文字不错,还请看官给小站打个赏~~~~~~~~~~~~~!

Mybatis执行流程学习之手写mybatis雏形

其他 theMine 1428次浏览 0个评论

Mybatis是目前开发中最常用的一款基于ORM思想的半自动持久层框架,平时我们都仅仅停留在使用阶段,对mybatis是怎样运行的并不清楚,今天抽空找到一些资料自学了一波,自己写了一个mybatis的雏形,在此对学习过程做一个记录
首先,我们新建一个提供mybatis框架功能的工程IMybatis,这个工程中主要完成mybatis整个初始化和执行过程的功能开发。

 Mybatis执行流程学习之手写mybatis雏形

该工程中用到的依赖

<?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.my</groupId>
    <artifactId>IMybatis</artifactId>
    <version>1.0-SNAPSHOT</version>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>6</source>
                    <target>6</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compile.encoding>UTF-8</maven.compile.encoding>
        <java.version>1.8</java.version>
        <maven.compile.source>1.8</maven.compile.source>
        <maven.compile.target>1.8</maven.compile.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.19</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.12</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.10</version>
        </dependency>
        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>
        <dependency>
            <groupId>jaxen</groupId>
            <artifactId>jaxen</artifactId>
            <version>1.1.6</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.26</version>
        </dependency>
    </dependencies>

</project>

 

我们在完成上面第一步中框架的编写后会进行打包发布到本地仓库,再新建一个测试工程IMybatis-test,这个工程的pom文件中会引入IMybatis工程的依赖,完成测试

Mybatis执行流程学习之手写mybatis雏形

 

 

 该工程的依赖

<?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>org.my</groupId>
    <artifactId>IMybatis-test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>com.my</groupId>
            <artifactId>IMybatis</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

 

mybatis要完成对数据库的连接,增删改查功能,需要有两个配置文件(这里先不管以注解的形式在mapper接口中编写的sql),一个是配置的数据库的连接信息,我这里是datasourceConfig.xml,

<configuration>
    <!-- 数据库配置信息 -->
    <dataSource>
        <property name="driverClass" value="com.mysql.cj.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql:///test?serverTimezone=Asia/Shanghai"></property>
        <property name="username" value="root"></property>
        <property name="password" value="123456"></property>
    </dataSource>

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

 

另一个是提供sql的mapper文件,这里是UserMapper.xml,这两个文件都在IMybatis-test工程中提供

<mapper namespace="com.my.dao.UserMapper">

    <!-- sql的唯一表示由 namespace.id 来组成statementId -->
    <select id="findAll" resultType="com.my.pojo.User">
      select * from user
    </select>

    <select id="findOne" parameterType="com.my.pojo.User" resultType="com.my.pojo.User">
        select * from user where id = #{id}
    </select>

    <select id="findById" parameterType="java.lang.Long" resultType="com.my.pojo.User">
        select * from user where id = #{id}
    </select>

    <delete id="delete" parameterType="com.my.pojo.User">
        delete from user where id = #{id}
    </delete>

    <delete id="deleteById" parameterType="java.lang.Long">
        delete from user where id = #{id}
    </delete>

    <update id="update" parameterType="com.my.pojo.User">
        update user set name = #{name} where id = #{id}
    </update>

    <insert id="insert" parameterType="com.my.pojo.User">
        insert into user(id, name) VALUES(#{id}, #{name})
    </insert>
</mapper>

 

下面就要完成IMybatis的功能开发。

一、新建Resource类完成对datasourceConfig.xml文件的加载,将其以流的形式加载到内存中

package com.my.io;

import java.io.InputStream;

/**
 * @Description: 配置文件读取
 * @Author lzh
 * @Date 2020/12/6 16:01
 */
public class Resource {

  /**
   * 根据传递的路径path去读取到该路径下的配置文件datasourceConfig.xml,并将其读成字节流返回
   * @param path
   * @return InputStream
   */
  public static InputStream getResourceAsStream(String path){
    InputStream resourceAsStream = Resource.class.getClassLoader().getResourceAsStream(path);
    return resourceAsStream;
  }
}

二、新建SqlSessionFactoryBuilder类,编写build()方法,一步一步构建SqlSessionFactory对象

package com.my.sqlSession;

import com.my.config.XMLConfigBuilder;
import com.my.pojo.Configuration;

import java.io.InputStream;

/**
 * @Description: 解析配置文件
 * @Author lzh
 * @Date 2020/12/6 16:23
 */
public class SqlSessionFactoryBuilder {

  /**
   * 根据字节流解析出配置文件中各个标签的值,并封装到Configuration中,创建DefaultSqlSessionFactory对象
   * @param in
   * @return
   * @throws Exception
   */
  public SqlSessionFactory build(InputStream in) throws Exception {

    //创建一个XMLConfigBuilder对象
    XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();

    //对配置文件进行解析
    Configuration configuration = xmlConfigBuilder.parseConfig(in);

    //创建DefaultSqlSessionFactory对象
    DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);

    return defaultSqlSessionFactory;
  }
}

三、在build方法中可以看到首先要创建一个XMLConfigBuilder 对象,在该对象中编写了一个parseConfig()方法完成对配置文件的解析,并完成对Configuration 对象的封装,Configuration 是我们这个工程中的一个非常核心的对象,里面存储了对配置文件解析后的结果,同样在真正的Mybatis框架中也有该对象,当然功能比我这里的更强大。

package com.my.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.my.io.Resource;
import com.my.pojo.Configuration;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.InputStream;
import java.util.List;
import java.util.Properties;

/**
 * @Description:
 * @Author lzh
 * @Date 2020/12/6 16:26
 */
public class XMLConfigBuilder {

  private Configuration configuration;

  public XMLConfigBuilder() {
    this.configuration = new Configuration();
  }

  /**
   * 解析dataSourceConfig.xml
   * @param in
   * @return
   * @throws Exception
   */
  public Configuration parseConfig(InputStream in) throws Exception {

    //利用dom4j技术对配置文件进行解析
    Document document = new SAXReader().read(in);
    Element rootElement = document.getRootElement();
    //查找dataSourceConfig.xml中的property标签
    List<Element> list = rootElement.selectNodes("//property");
    Properties properties = new Properties();
    for (Element element : list) {
      //取出每个property标签中的值存到Properties对象中
      String name = element.attributeValue("name");
      String value = element.attributeValue("value");
      properties.setProperty(name, value);
    }
    //从Properties中取出各个属性构建一个连接池,来提供对数据库连接的管理,避免资源浪费,提高性能
    DruidDataSource druidDataSource = new DruidDataSource();
    druidDataSource.setDriverClassName(properties.getProperty("driverClass"));
    druidDataSource.setUrl(properties.getProperty("url"));
    druidDataSource.setUsername(properties.getProperty("username"));
    druidDataSource.setPassword(properties.getProperty("password"));
    //将连接池对象放入Configuration对象中
    configuration.setDataSource(druidDataSource);

    //解析dataSourceConfig.xml中的mapper标签,mapper标签中的resource属性值存放的就是UserMapper.xml的文件位置
    List<Element> mapperList = rootElement.selectNodes("//mapper");
    for (Element element : mapperList) {
      String mapperPath = element.attributeValue("resource");
      InputStream resourceAsStream = Resource.getResourceAsStream(mapperPath);
      //解析UserMapper.xml文件,进一步封装Configuration对象
      XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration); xmlMapperBuilder.parse(resourceAsStream);
    }
    return configuration;
  }
}

上图红色的地方创建了一个XMLMapperBuilder对象,该对象提供了一个parse()方法,就是完成对UserMapper.xml文件的解析,并完成对Configuration封装

package com.my.config;

import com.my.config.eunm.SqlCommandType;
import com.my.pojo.Configuration;
import com.my.pojo.MappedStatement;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.InputStream;
import java.util.List;

/**
 * @Description:
 * @Author lzh
 * @Date 2020/12/6 17:03
 */
public class XMLMapperBuilder {

  private Configuration configuration;

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

  /**
   * 解析UserMapper.xml配置文件中得内容,将每一个标签构建成一个MappedStatement,并赋值到Configuration中
   * @param in
   * @throws DocumentException
   */
  public void parse(InputStream in) throws DocumentException {
    Document document = new SAXReader().read(in);
    Element rootElement = document.getRootElement();
    String namespace = rootElement.attributeValue("namespace");
    //解析select标签
    List<Element> selectList = rootElement.selectNodes("//select");
    this.parseElement(selectList, namespace, SqlCommandType.SELECT);

    //解析insert标签
    List<Element> insertList = rootElement.selectNodes("//insert");
    this.parseElement(insertList, namespace, SqlCommandType.INSERT);

    //解析update标签
    List<Element> updateList = rootElement.selectNodes("//update");
    this.parseElement(updateList, namespace, SqlCommandType.UPDATE);

    //解析delete标签
    List<Element> deleteList = rootElement.selectNodes("//delete");
    this.parseElement(deleteList, namespace, SqlCommandType.DELETE);
  }

  /**
   * 解析mapper.xml文件中增删改查标签
   * @param elementList
   * @param namespace
   * @param sqlCommandType
   */
  private void parseElement(List<Element> elementList, String namespace, SqlCommandType sqlCommandType) {
    for (Element element : elementList) {
      String id = element.attributeValue("id");
      String resultType = element.attributeValue("resultType");
      String parameterType = element.attributeValue("parameterType");
      String sql = element.getTextTrim();

      MappedStatement mappedStatement = new MappedStatement();
      mappedStatement.setSqlCommandType(sqlCommandType);
      mappedStatement.setId(id);
      mappedStatement.setParameterType(parameterType);
      mappedStatement.setResultType(resultType);
      mappedStatement.setSql(sql);

      configuration.getMappedStatementMap().put(namespace + "." + id, mappedStatement);
    }
  }
}

该类中用到的SqlCommandType是一个枚举类,就是列举的UserMapper.xml中的几个主要的sql标签类型增删改查,也是借鉴的原Mybatis框架中的写法

package com.my.config.eunm;

public enum SqlCommandType {
  INSERT,
  UPDATE,
  DELETE,
  SELECT;

  private SqlCommandType(){

  }
}

还有一个MappedStatement对象,这个对象中就是封装的每一个insert、update、delete、select标签中的信息(包括每个标签中的id、parameterType、resutType、sql语句等等),每个标签就是一个MappedStatement对象

package com.my.pojo;

import com.my.config.eunm.SqlCommandType;

/**
 * @Description:
 * @Author lzh
 * @Date 2020/12/6 16:17
 */
public class MappedStatement {

  private SqlCommandType sqlCommandType;

  private String id;

  private String resultType;

  private String parameterType;

  private String sql;

  public SqlCommandType getSqlCommandType() {
    return sqlCommandType;
  }

  public void setSqlCommandType(SqlCommandType sqlCommandType) {
    this.sqlCommandType = sqlCommandType;
  }

  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 getParameterType() {
    return parameterType;
  }

  public void setParameterType(String parameterType) {
    this.parameterType = parameterType;
  }

  public String getSql() {
    return sql;
  }

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

封装好MappedStatement对象后,再将其放入Configuration对象的mappedStatementMap属性中,该属性就是一个Map集合,key就是UserMapper.xml文件中的namespace的值+ “.” +每一个标签的id值(例如我们这里的com.my.dao.UserMapper.findAll),因为一个Mapper接口对应一个Mapper.xml文件,而每个Mapper.xml文件中的namespace的值就是Mapper接口的全限定类名,每个标签的id值就是Mapper接口中对应的方法名,所以通过这个组合key就能和Mapper接口产生关联,当我们在调用Mapper接口中的方法时,就可以通过Mapper接口的全限定类名和调用的方法名在Configuration中的Map集合中找到对应的MappedStatement对象,也就是能拿到需要执行的sql、参数类型、返回值类型等等。

package com.my.pojo;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/** 核心对象
 * @Description:
 * @Author lzh
 * @Date 2020/12/6 16:18
 */
public class Configuration {

  /**
   * 数据源
   */
  private DataSource dataSource;

  /**
   * key:statementId vlaue:封装好的MappedStatement
   */
  private Map<String,MappedStatement> mappedStatementMap = new HashMap<String, MappedStatement>();

  public DataSource getDataSource() {
    return dataSource;
  }

  public void setDataSource(DataSource dataSource) {
    this.dataSource = dataSource;
  }

  public Map<String, MappedStatement> getMappedStatementMap() {
    return mappedStatementMap;
  }

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

到这里我们的Configuration对象就封装完毕。

四、然后我们可以在第二步中的SqlSessionFactoryBuilder类的build()方法中看到,根据Configuration对象构造出了DefaultSqlSessionFactory工厂对象,整个构建DefaultSqlSessionFactory的过程就是一个构建者模式的体现(通过多个小的对象构建出一个大的对象)

package com.my.sqlSession;

public interface SqlSessionFactory {

  SqlSession createSqlSession();
}

 

package com.my.sqlSession;

import com.my.pojo.Configuration;

/**
 * @Description: SqlSession的工厂对象,用于生产SqlSession
 * @Author lzh
 * @Date 2020/12/6 17:17
 */
public class DefaultSqlSessionFactory implements SqlSessionFactory {

  private Configuration configuration;

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

  /**
   * 创建SqlSession会话
   * @return
   */
  public SqlSession createSqlSession() {
    return new DefaultSqlSession(configuration);
  }
}

五、利用DefaultSqlSessionFactory工厂对象 的createSqlSession()方法来获取一个SqlSession对象,就是一个我们所说的一个会话对象,该对象也是一个非常重要的对象

package com.my.sqlSession;

import java.util.List;

/**
 * @Description: SqlSession
 * @Author lzh
 * @Date 2020/12/6 17:18
 */
public interface SqlSession {

  <E> List<E> selectList(String statementId, Class<?> methodParameterType, Object... param) throws Exception;

  <T> T selectOne(String statementId, Class<?> methodParameterType, Object... param) throws Exception;

  <T> T getMapper(Class<?> mapperClass);
}
package com.my.sqlSession;

import com.my.config.eunm.SqlCommandType;
import com.my.pojo.Configuration;
import com.my.pojo.MappedStatement;

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

/**
 * @Description: SqlSession会话的实现
 * @Author lzh
 * @Date 2020/12/6 17:21
 */
public class DefaultSqlSession implements SqlSession, InvocationHandler {

  private Configuration configuration;

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

  /**
   * 多条查询
   * @param statementId
   * @param param
   * @param <E>
   * @return
   * @throws Exception
   */
  public <E> List<E> selectList(String statementId, Class<?> methodParameterType, Object... param) throws Exception {

    SimpleExecutor simpleExecutor = new SimpleExecutor();
    List<Object> query = simpleExecutor.query(configuration, configuration.getMappedStatementMap().get(statementId), methodParameterType, param);
    return (List<E>) query;
  }

  /**
   * 单条查询
   * @param statementId
   * @param param
   * @param <T>
   * @return
   * @throws Exception
   */
  public <T> T selectOne(String statementId, Class<?> methodParameterType, Object... param) throws Exception {
    List<Object> objects = selectList(statementId, methodParameterType, param);
    if (objects.size() == 1){
      return (T) objects.get(0);
    }else if (objects.size() <= 0){
      return null;
    }else {
      throw new RuntimeException("Result more than one");
    }
  }

  /**
   * 新增
   * @param statementId
   * @param param
   * @return
   */
  public int insert(String statementId, Class<?> methodParameterType, Object... param) throws Exception {
    return update(statementId, methodParameterType, param);
  }

  /**
   * 修改
   * @param statementId
   * @param param
   * @return
   */
  public int update(String statementId, Class<?> methodParameterType, Object... param) throws Exception {
    SimpleExecutor simpleExecutor = new SimpleExecutor();
    return simpleExecutor.update(configuration, configuration.getMappedStatementMap().get(statementId), methodParameterType, param);
  }

  /**
   * 删除
   * @param statementId
   * @param param
   * @return
   */
  public int delete(String statementId, Class<?> methodParameterType, Object... param) throws Exception {
    return update(statementId, methodParameterType, param);
  }

  /**
   * 创建代理对象
   * @param mapperClass
   * @param <T>
   * @return
   */
  @Override
  public <T> T getMapper(Class<?> mapperClass) {
    Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, this);
    return (T) proxyInstance;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String methodName = method.getName();
    Class<?> methodParameterType = null;
    if (null != method.getParameterTypes() && 0 < method.getParameterTypes().length){
      methodParameterType = method.getParameterTypes()[0];
    }
    String className = method.getDeclaringClass().getName();
    String statementId = className + "." + methodName;
    MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
    SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
    if (SqlCommandType.SELECT == sqlCommandType){
      Type genericReturnType = method.getGenericReturnType();
      if (genericReturnType instanceof ParameterizedType){
        return selectList(statementId, methodParameterType, args);
      }
      return selectOne(statementId, methodParameterType, args);
    }else if (SqlCommandType.INSERT == sqlCommandType){
      return insert(statementId, methodParameterType, args);
    }else if (SqlCommandType.UPDATE == sqlCommandType){
      return update(statementId, methodParameterType, args);
    }else if (SqlCommandType.DELETE == sqlCommandType){
      return delete(statementId, methodParameterType, args);
    }else {
      throw new RuntimeException("Unknown SqlCommandType For: " + sqlCommandType);
    }
  }
}

六、在SqlSession中,就提供了增删改查方法,用于操作数据库,我们另外还可以看到一个getMapper()方法,该方法需要传入一个Class参数,那么这个方法是干什么的呢?我们有过开发经验的朋友都知道很早以前在用spring+Mybatis框架开发的时候,对每一个Dao(也就是这里我们说的Mapper层)层的接口都会去写一个实现类DaoImpl,在实现类中通过JDBC来完成对数据库的操作,这样的编码方式会存在很多问题,比如:

  1. 每次执行一个方法都会区获取一个Connection对象,也就是创建一个数据库连接
  2. sql语句和业务代码融合在一起,增加代码耦合度,也不便于维护
  3. 封装返回结果麻烦,不够智能

所以针对第一个问题我们引入了连接池来管理数据库连接,每次都是从池子里面去获取,减少了资源消耗,提高了效率,针对后面两个问题,首先Mybatis去调了DaoImpl实现类,其次,通过Java反射技术完成对参数的赋值和对返回结果的动态封装(这一步后面代码中会有体现)。那么去掉了DaoImpl实现类,Dao接口中需要做的事总是需要有人来做的,否则无法完成对数据库的操作,因此Mybatis中会为每个Dao接口(也就是这里我们说的Mapper接口)生成一个代理对象,去完成之前DaoImpl做的事。这里的getMapper()方法就是去获取传入参数对象的代理对象,我们这里就是获取UserMapper接口的代理对象,创建代理对象时我们可以看到在getMapper()方法中的Proxy.newProxyInstance(),需要传递三个参数,第一个参数就是一个类加载器,第二参数就是我们需要为哪个对象产生代理对象,也就是getMapper()方法的参数,重点是第三个参数,需要传入一个InvocationHandler对象,而InvocationHandler是一个接口,我们这里的DefaultSqlSession实现了这个接口,所以第三个参数传的就是this,该类本身。实现了InvocationHandler接口就需要重写invoke()方法,而我们知道调用代理对象的方法,都会走到该invoke()方法中,所以我们这里调用UserMapper接口中的方法时,同样会执行这里的invoke方法,这样在invoke()方法中就可以完成我们以前在DaoImpl中需要完成的事。

七、下面我们具体来看下invoke()中做了什么,首先看下三个参数,第一个就是一个代理对象,第二个就是我们调用的方法Method,第三个就是调用方法时传入的参数args,那么我们根据Method对象就可以获取到该方法的全限定类名和该方法的名称,从而组合一个statemenId,而我们在上面第五步中通过createSqlSession()方法创建SqlSession对象时,是将我们封装的Configuration对象传入了,所有这里我们可以通过statementId在Configuration对象的mappedStatementMap这个Map集合中找到我们封装的MappedStatement对象,通过MappedStatement对象中的SqlCommandType的值我们可以判断出我们需要执行增删改查中的哪个方法,从而去调用该类具体的增删改查方法,在执行具体方法时,我们这里并没有在SqlSession对象中直接去操作数据库,而是将这些crud操作交给了一个SimpleExecutor执行器去完成真正对数据库的操作。

package com.my.sqlSession;

import com.my.pojo.Configuration;
import com.my.pojo.MappedStatement;

import java.util.List;

public interface Executor {

  <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Class<?> methodParameterType, Object... param) throws Exception;

  int update(Configuration configuration, MappedStatement mappedStatement, Class<?> methodParameterType, Object[] param) throws Exception;

}
package com.my.sqlSession;

import com.my.config.BoundSql;
import com.my.pojo.Configuration;
import com.my.pojo.MappedStatement;
import com.my.utils.GenericTokenParser;
import com.my.utils.ParameterMapping;
import com.my.utils.ParameterMappingTokenHandler;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * @Description: Executor执行器
 * @Author lzh
 * @Date 2020/12/6 17:32
 */
public class SimpleExecutor implements Executor {

  /**
   * 真正的查询方法,负责完成JDBC的操作
   * @param configuration
   * @param mappedStatement
   * @param param
   * @param <E>
   * @return
   * @throws Exception
   */
  public <E> List<E>  query(Configuration configuration, MappedStatement mappedStatement,Class<?> methodParameterType, Object... param) throws Exception {
    PreparedStatement preparedStatement = this.createPreparedStatement(configuration, mappedStatement, methodParameterType, param);
    //执行sql,返回结果集ResultSet
    ResultSet resultSet = preparedStatement.executeQuery();
    //对结果封装,映射出对应得返回类型
    String resultType = mappedStatement.getResultType();
    Class<?> resultClass = getClassType(resultType);
    List<Object> result = new ArrayList<Object>();
    while (resultSet.next()){
      Object o = resultClass.newInstance();
      ResultSetMetaData metaData = resultSet.getMetaData();
      for (int i = 1; i <= metaData.getColumnCount(); i++) {
        String columnName = metaData.getColumnName(i);
        Object object = resultSet.getObject(columnName);
        PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultClass);
        Method writeMethod = propertyDescriptor.getWriteMethod();
        writeMethod.invoke(o, object);
      }
      result.add(o);
    }
    return (List<E>) result;
  }

  @Override
  public int update(Configuration configuration, MappedStatement mappedStatement, Class<?> methodParameterType, Object[] param) throws Exception {
    PreparedStatement preparedStatement = this.createPreparedStatement(configuration, mappedStatement, methodParameterType, param);
    preparedStatement.execute();
    int row = preparedStatement.getUpdateCount();
    return row;
  }

  /**
   * 获取PreparedStatement对象
   * @param configuration
   * @param mappedStatement
   * @param param
   * @return
   * @throws Exception
   */
  private PreparedStatement createPreparedStatement(Configuration configuration, MappedStatement mappedStatement, Class<?> methodParameterType, Object[] param) throws Exception {

    //获取数据库连接
    Connection connection = configuration.getDataSource().getConnection();
    //从MappedStatement中取出sql,现在的sql就是userMapper.xml中我们编写的带有#{}的sql语句
    String sql = mappedStatement.getSql();
    //处理sql语句,解析出sql语句中#{}中的属性值,并将#{}替换为?,封装到BoundSql对象中
    BoundSql boundSql = getBoundSql(sql);

    //获取PreparedStatement对象
    PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());
    //如果有参数,给参数赋值
    String parameterType = mappedStatement.getParameterType();
    if (null != parameterType){
      Class<?> parameterClass = getClassType(parameterType);

      List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
      for (int i = 0; i < parameterMappingList.size(); i++) {
        if (isObject(methodParameterType)){
          preparedStatement.setObject(i + 1, param[0]);
        }else {
          ParameterMapping parameterMapping = parameterMappingList.get(i);
          //该content就是我们sql中#{id}中的id
          String content = parameterMapping.getContent();
          //利用反射在parameterClass中取出content这个属性的值,并完成sql的赋值
          Field declaredField = parameterClass.getDeclaredField(content);
          declaredField.setAccessible(true);
          Object o = declaredField.get(param[0]);
          preparedStatement.setObject(i + 1, o);
        }
      }
    }
    return preparedStatement;
  }

  /**
   * 根据参数类型或者返回值类型获取该对象的Class
   * @param parameterType
   * @return
   * @throws ClassNotFoundException
   */
  private Class<?> getClassType(String parameterType) throws ClassNotFoundException {
    Class<?> aClass = Class.forName(parameterType);
    return aClass;
  }

  /**
   * 解析sql,封装成BoundSql
   * @param sql
   * @return
   */
  private BoundSql getBoundSql(String sql) {
    ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
    GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
    //解析出来的sql
    String parseSql = genericTokenParser.parse(sql);
    //解析出来的id和name
    List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
    BoundSql boundSql = new BoundSql(parseSql, parameterMappings);
    return boundSql;
  }

  private Boolean isObject(Class<?> methodParameterType){
    if (null == methodParameterType){
      return false;
    }
    if (Integer.class.getName().equals(methodParameterType.getName())
        || Long.class.getName().equals(methodParameterType.getName())
        || String.class.getName().equals(methodParameterType.getName())
        || Double.class.getName().equals(methodParameterType.getName())
        || Float.class.getName().equals(methodParameterType.getName())
        || Byte.class.getName().equals(methodParameterType.getName())
        || Short.class.getName().equals(methodParameterType.getName())
        || Character.class.getName().equals(methodParameterType.getName())
        || Boolean.class.getName().equals(methodParameterType.getName())
        || Date.class.getName().equals(methodParameterType.getName())){
      return true;
    }
    return false;
  }
}

八、在这个执行器中就是真正完成对数据库的操作,从连接池中获取一个Connection连接,从MappedStatement中获取到要执行的sql,这里注意这时候的sql还是从UserMapper.xml中解析出来的sql(select * from user where id = #{id}),需要对其进行处理用?替换掉#{},并记录大括号中的参数,因为JDBC中参数的占位符是?,所以这里的getBoundSql()方法就是在做这些事情,最终封装成一个BoundSql对象。

package com.my.config;

import com.my.utils.ParameterMapping;

import java.util.ArrayList;
import java.util.List;

/**
 * @Description: sql
 * @Author lzh
 * @Date 2020/12/6 17:42
 */
public class BoundSql {

  private String sqlText;

  private List<ParameterMapping> parameterMappingList = new ArrayList<ParameterMapping>();

  public BoundSql(String sqlText, List<ParameterMapping> parameterMappingList) {
    this.sqlText = sqlText;
    this.parameterMappingList = parameterMappingList;
  }

  public String getSqlText() {
    return sqlText;
  }

  public void setSqlText(String sqlText) {
    this.sqlText = sqlText;
  }

  public List<ParameterMapping> getParameterMappingList() {
    return parameterMappingList;
  }

  public void setParameterMappingList(List<ParameterMapping> parameterMappingList) {
    this.parameterMappingList = parameterMappingList;
  }
}

 

这其中用到的几个工具类我也贴在这里,这也是从Mybatis源码中拿到的,就是对sql解析处理,这里不用过大关注。

package com.my.utils;

/**
 * @author lzh
 */
public interface TokenHandler {
  String handleToken(String content);
}

 

package com.my.utils;

import java.util.ArrayList;
import java.util.List;




public class ParameterMappingTokenHandler implements TokenHandler {
    private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();

    // context是参数名称 #{id} #{username}

    public String handleToken(String content) {
        parameterMappings.add(buildParameterMapping(content));
        return "?";
    }

    private ParameterMapping buildParameterMapping(String content) {
        ParameterMapping parameterMapping = new ParameterMapping(content);
        return parameterMapping;
    }

    public List<ParameterMapping> getParameterMappings() {
        return parameterMappings;
    }

    public void setParameterMappings(List<ParameterMapping> parameterMappings) {
        this.parameterMappings = parameterMappings;
    }

}
package com.my.utils;

/**
 * @author Clinton Begin
 */
public class GenericTokenParser {

  private final String openToken; //开始标记
  private final String closeToken; //结束标记
  private final TokenHandler handler; //标记处理器

  public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
    this.openToken = openToken;
    this.closeToken = closeToken;
    this.handler = handler;
  }

  /**
   * 解析${}和#{}
   * @param text
   * @return
   * 该方法主要实现了配置文件、脚本等片段中占位符的解析、处理工作,并返回最终需要的数据。
   * 其中,解析工作由该方法完成,处理工作是由处理器handler的handleToken()方法来实现
   */
  public String parse(String text) {
    // 验证参数问题,如果是null,就返回空字符串。
    if (text == null || text.isEmpty()) {
      return "";
    }

    // 下面继续验证是否包含开始标签,如果不包含,默认不是占位符,直接原样返回即可,否则继续执行。
    int start = text.indexOf(openToken, 0);
    if (start == -1) {
      return text;
    }

   // 把text转成字符数组src,并且定义默认偏移量offset=0、存储最终需要返回字符串的变量builder,
    // text变量中占位符对应的变量名expression。判断start是否大于-1(即text中是否存在openToken),如果存在就执行下面代码
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    while (start > -1) {
     // 判断如果开始标记前如果有转义字符,就不作为openToken进行处理,否则继续处理
      if (start > 0 && src[start - 1] == '\\') {
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        //重置expression变量,避免空指针或者老数据干扰。
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {////存在结束标记时
          if (end > offset && src[end - 1] == '\\') {//如果结束标记前面有转义字符时
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {//不存在转义字符,即需要作为参数进行处理
            expression.append(src, offset, end - offset);
            offset = end + closeToken.length();
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          //首先根据参数的key(即expression)进行参数处理,返回?作为占位符
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }
}
package com.my.utils;

public class ParameterMapping {

    private String content;

    public ParameterMapping(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

解析完sql后就是创建PreparedStatement对象,并通过MappedStatement对象中记录的参数类型,利用java反射技术进行赋值,然后执行sql,最后再通过MappedStatement对象中记录的返回值类型对结果进行封装,同样是用java反射,这样就实现了参数的动态赋值和结果的动态封装。这就是整个Mybatis的执行流程,到这里也就完成了IMybatis框架的编写,下面我们进行测试。

九、将IMybatis打包到本地仓库,在IMybatis-test中引入依赖,编写一个用户Pojo类、UserMapper接口和一个测试类,UserMapper.xml在上面已经提供

package com.my.pojo;

/**
 * @Description:
 * @Author lzh
 * @Date 2020/12/6 15:57
 */
public class User {

  private Long id;

  private String name;

  public Long getId() {
    return id;
  }

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

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  @Override
  public String toString() {
    return "User{" +
        "id=" + id +
        ", name='" + name + '\'' +
        '}';
  }
}
package com.my.dao;

import com.my.pojo.User;
import java.util.List;

public interface UserMapper {

  /**
   * 查询所有
   * @return
   */
  List<User> findAll() ;

  /**
   * 查询单条
   * @param user
   * @return
   */
  User findOne(User user);

  /**
   * 根据id查询单条
   * @param id
   * @return
   */
  User findById(Long id);

  /**
   * 根据id删除用户
   * @param id
   * @return
   */
  int deleteById(Long id);

  /**
   * 删除用户
   * @param user
   * @return
   */
  int delete(User user);

  /**
   * 新增用户
   * @param user
   * @return
   */
  int insert(User user);

  /**
   * 修改用户
   * @param user
   * @return
   */
  int update(User user);
}
package com.my.test;

import com.my.dao.UserMapper;
import com.my.io.Resource;
import com.my.pojo.User;
import com.my.sqlSession.SqlSession;
import com.my.sqlSession.SqlSessionFactory;
import com.my.sqlSession.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;

import java.io.InputStream;
import java.util.List;

/**
 * @Description:
 * @Author lzh
 * @Date 2020/12/6 16:05
 */
public class IMybatisTest {

  private SqlSession sqlSession;

  @Before
  public void before() throws Exception {
    InputStream resourceAsStream = Resource.getResourceAsStream("dataSourceConfig.xml");
    SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
    SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(resourceAsStream);
    sqlSession = sqlSessionFactory.createSqlSession();
  }

  @Test
  public void test1() {
    User user = new User();
    user.setId(2L);
    user.setName("王五");

    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user1 = mapper.findOne(user);
    System.out.println(user1);
  }

  @Test
  public void test2() {

    User user = new User();
    user.setId(1L);
    user.setName("王五");
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    List<User> all = mapper.findAll();
    for (User user1 : all) {
      System.out.println(user1);
    }
  }

  @Test
  public void test3() {

    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    User user1= mapper.findById(2L);
    System.out.println(user1);
  }

  @Test
  public void test4() {

    User user = new User();
    user.setId(3L);
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    int row = mapper.delete(user);
    System.out.println(row);
  }

  @Test
  public void test5() {

    User user = new User();
    user.setId(3L);
    user.setName("王五");
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    int row = mapper.update(user);
    System.out.println(row);
  }

  @Test
  public void test6() {

    User user = new User();
    user.setId(3L);
    user.setName("张三");
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    int row = mapper.insert(user);
    System.out.println(row);
  }
}

这里就不把全部的测试结果贴出来了,贴一个看下效果就行,可以看到控制台正常输出,说明我们自己写的IMybatis没问题,可以成功执行。

Mybatis执行流程学习之手写mybatis雏形

 

总结:我们可以看到最后仍然是通过JDBC完成的数据库操作。所以到这里我们可以知道Mybatis最终仍然是调用的JDBC去操作数据库,它只不过在执行JDBC之前还多去做了这一系列解析配置文件,封装各个对象等等这些操作,Mybatis就是对JDBC的包装。

 


开心洋葱 , 版权所有丨如未注明 , 均为原创丨未经授权请勿修改 , 转载请注明Mybatis执行流程学习之手写mybatis雏形
喜欢 (0)

您必须 登录 才能发表评论!

加载中……