在以上工程的基础上,理解 MyBatis 框架的流程,我们可以自己尝试写一个“MyBatis 框架”,以便更深入了解 MyBatis。

 


新建 Maven 项目。
延用以上工程的 Maven 依赖。但这里请注意,我们的目的是自己重写 “MyBatis”,所以就不需要引入 MaBatis 的依赖,所以这里我注释掉了。

<?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.geek</groupId>
    <artifactId>mybatis_my</artifactId>
    <version>1.0-SNAPSHOT</version>


    <dependencies>
<!--
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.5</version>
        </dependency>
-->

        <!-- https://mvnrepository.com/artifact/log4j/log4j -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/junit/junit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

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

</project>

来看此时的项目结构。

需要我们自己重写的类:

class Resources {}
class SqlSessionFactoryBuilder { build() {} }
class SqlSessionFactory { openSession() }
class SqlSession { getMapper() }

mybaties 双写_sql

mybaties 双写_MyBatis_02

我们来自己写读取配置文件的类。

package com.geek.mybatis.io;

import java.io.InputStream;

/**
 * 使用类加载器读取配置文件的类。
 *
 * @author geek
 */
public class Resources {

    /**
     * 根据传入的参数,获取一个字节输入流。
     *
     * @param filePath
     * @return
     */
    public static InputStream getResourceAsStream(String filePath) {
        return Resources.class.getClassLoader().getResourceAsStream(filePath);
    }

}

然后根据 test 类中所缺失的类逐一创建。

mybaties 双写_sql_03

最终项目结构。

mybaties 双写_MyBatis_04

package com.geek.mybatis.sqlSession;

import com.geek.mybatis.cfg.Configuration;
import com.geek.mybatis.sqlSession.defaults.DefaultSqlSessionFactory;
import com.geek.mybatis.utils.XMLConfigBuilder;

import java.io.InputStream;

/**
 * 用于创建一个 SqlSessionFactoryBuilder 对象。
 *
 * @author geek
 */
public class SqlSessionFactoryBuilder {

    /**
     * 根据参数的字节输入流来构建一个 SqlSessionFactory 工厂。
     *
     * @param config
     * @return
     */
    public ISqlSessionFactory build(InputStream config) {
        Configuration cfg = XMLConfigBuilder.loadConfiguration(config);
        return new DefaultSqlSessionFactory(cfg);
    }

}
package com.geek.mybatis.sqlSession;

/**
 * 用于打开一个新的 ISqlSession 对象。
 *
 * @author geek
 */
public interface ISqlSessionFactory {

    /**
     * 用于打开一新的 ISqlSession 对象。
     *
     * @return
     */
    ISqlSession openSession();

}
package com.geek.mybatis.sqlSession;

/**
 * 自定义 MyBatis 中和数据库交互的核心类。
 * 创建 dao 接口的代理对象。
 *
 * @author geek
 */
public interface ISqlSession {

    /**
     * 根据参数创建一个代理对象。
     *
     * @param daoInterfaceClass dao 的接口字节码。
     * @param <T>
     * @return
     */
    <T> T getMapper(Class<T> daoInterfaceClass);
    // 泛形——> 先声明,再使用。<T>。

    /**
     * 释放资源。
     */
    void close();

}



下一步,我们要解析 xml 文件,获取其中的 MySQL 连接数据以及 sql 语句。就需要 dom4j 来解析 xml。

在 pom.xml 中添加依赖。

<!-- https://mvnrepository.com/artifact/dom4j/dom4j -->
<dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>1.6.1</version>
</dependency>

Class ConfigBuilder 中需要使用 XPath 就需要依赖。

<!-- https://mvnrepository.com/artifact/jaxen/jaxen -->
<dependency>
    <groupId>jaxen</groupId>
    <artifactId>jaxen</artifactId>
    <version>1.1.6</version>
</dependency>

需要读取配置文件,就需要实体类,用于封装。

mybaties 双写_Java_05

package com.geek.mybatis.cfg;

import lombok.Data;

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

/**
 * 自定义 MyBatis 的配置类。
 *
 * @author geek
 */
@Data
public class Configuration {

    private String driver;
    private String url;
    private String username;
    private String password;

    private Map<String, Mapper> mappers = new HashMap<String, Mapper>();

    public Map<String, Mapper> getMappers() {
        return mappers;
    }

    public void setMappers(Map<String, Mapper> mappers) {
//        this.mappers = mappers;
        // 此处使用追加的方式。如果使用赋值方式,只能有一个<mapper>标签。前面的会被覆盖。
        this.mappers.putAll(mappers);
    }

}
package com.geek.mybatis.cfg;

import lombok.Data;

/**
 * 用于封装执行 SQL 语句和结果类型的全限定类名。
 *
 * @author geek
 */
@Data
public class Mapper {

    /**
     * SQL。
     */
    private String queryString;
    /**
     * 实体类的全限定类名。
     */
    private String resultType;

}
  • utils 工具类。
package com.geek.mybatis.utils;

import com.geek.mybatis.cfg.Configuration;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

/**
 * 创建数据库连接的工具类。
 *
 * @author geek
 */
public class DataSourceUtil {

    public static Connection getConnection(Configuration cfg) {
        try {
            Class.forName(cfg.getDriver());
            return DriverManager.getConnection(cfg.getUrl(), cfg.getUsername(), cfg.getPassword());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        } catch (SQLException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

}

现在需要
Interface SqlSession 的实现类 Class DefaultSqlSession {}

Interface SqlSessionFactory 的实现类 Class DefaultSqlSessionFactory {}

package com.geek.mybatis.sqlSession.defaults;

import com.geek.mybatis.cfg.Configuration;
import com.geek.mybatis.sqlSession.ISqlSession;
import com.geek.mybatis.sqlSession.ISqlSessionFactory;

/**
 * ISqlSessionFactory 接口的实现类。
 *
 * @author geek
 */
public class DefaultSqlSessionFactory implements ISqlSessionFactory {

    private Configuration configuration;

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

    /**
     * 用于创建一个新的操作数据库对象。
     *
     * @return
     */
    @Override
    public ISqlSession openSession() {
        return new DefaultSqlSession(configuration);
    }

}

public class MapperProxy implements InvocationHandler {}
// 此处需要实现 InvocationHandler。

package com.geek.mybatis.sqlSession.proxy;

import com.geek.mybatis.cfg.Mapper;
import com.geek.mybatis.utils.Executor;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.Map;

/**
 * @author geek
 */
public class MapperProxy implements InvocationHandler {

    // map 的 key 是全限定类名 + 方法名。
    private Map<String, Mapper> mappers;

    private Connection connection;

    public MapperProxy(Map<String, Mapper> mappers, Connection connection) {
        this.mappers = mappers;
        this.connection = connection;
    }

    /**
     * 用于对方法进行增强。
     * 我们的增强其实就是调用 selectList(); 方法。
     *
     * @param proxy
     * @param method
     * @param args
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 获取方法名。
        String methodName = method.getName();
        // 获取方法所在的类名。
        String className = method.getDeclaringClass().getName();
        // 组合 key。
        String key = className + "." + methodName;
        // 获取 mappers 中的 Mapper 对象。
        Mapper mapper = mappers.get(key);
        // 判断是否有 mapper。
        if (mapper == null) {
            throw new IllegalArgumentException("传入的参数有误。");
        }

        // 调用工具类,执行查询所有。
        return new Executor().selectList(mapper, connection);
    }

}

现在需要执行查询的工具类。
utils 包下。

package com.geek.mybatis.utils;

import com.geek.mybatis.cfg.Mapper;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * 负责执行 Sql 语句,并且封装结果。
 */
public class Executor {

    /**
     * 执行查询。
     *
     * @param mapper
     * @param connection
     * @param <E>
     * @return
     */
    public <E> List<E> selectList(Mapper mapper, Connection connection) {
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;

        // 取出 mapper 中的数据。
        String queryString = mapper.getQueryString();// select * from user;
        String resultType = mapper.getResultType();// com.geek.domain.User。

        Class<?> aClass = null;
        try {
            aClass = Class.forName(resultType);

            // 获取 PreparedStatement 对象。
            preparedStatement = connection.prepareStatement(queryString);
            // 执行 SQL 语句,获取结果集。
            resultSet = preparedStatement.executeQuery();
            // 封装结果集。
            List<E> list = new ArrayList<E>();// 定义返回值。
            while (resultSet.next()) {
                // 实例化要封装的实体类对象。
                E o = (E) aClass.newInstance();

                // 取出结果集的元信息,ResultSetMetaData。
                ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
                // 取出总列数。
                int columnCount = resultSetMetaData.getColumnCount();
                // 遍历总列数。
                for (int i = 1; i <= columnCount; i++) {
                    // 获取列名。从 1 开始。
                    String columnName = resultSetMetaData.getColumnName(i);
                    // 根据得到的列名,获取每列的值。
                    Object columnValue = resultSet.getObject(columnName);
                    // 给 Object 赋值,使用 java 内省机制(借助 PropertyDescription 实现属性的封装)。
                    PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, aClass);// 要求:实体类
                    //  获取 ta 的写入方法。
                    Method writeMethod = propertyDescriptor.getWriteMethod();
                    // 把获取的列的值赋值给对象。
                    writeMethod.invoke(o, columnValue);
                }
                // 把赋好值的对象加入到集合中。
                list.add(o);
            }
            return list;
        } catch (ClassNotFoundException | InstantiationException | InvocationTargetException | IntrospectionException | SQLException | IllegalAccessException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        } finally {
            release(preparedStatement, resultSet);
        }
    }

    /**
     * 释放资源。
     *
     * @param preparedStatement
     * @param resultSet
     */
    private void release(PreparedStatement preparedStatement, ResultSet resultSet) {
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

        if (preparedStatement != null) {
            try {
                preparedStatement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

}

现在需要连接数据库的工具类。
utils 包下。

package com.geek.mybatis.utils;

import com.geek.mybatis.cfg.Configuration;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

/**
 * 创建数据库连接的工具类。
 *
 * @author geek
 */
public class DataSourceUtil {

    public static Connection getConnection(Configuration cfg) {
        try {
            Class.forName(cfg.getDriver());
            return DriverManager.getConnection(cfg.getUrl(), cfg.getUsername(), cfg.getPassword());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        } catch (SQLException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

}