手写Mybatis框架,剖析底层结构原理

一,Mybatis框架介绍

MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架,其主要就完成2件事情:

  1. 封装JDBC操作
  2. 利用反射打通Java类与SQL语句之间的相互转换

MyBatis的主要设计目的就是让我们对执行SQL语句时对输入输出的数据管理更加方便,所以方便地写出SQL和方便地获取SQL的执行结果才是MyBatis的核心竞争力。

二,前提概述

Mybatis的用法这里简单说一下,新建Mapper接口文件,然后在mapper.xml配置文件中写对应的sql相关信息。

public interface UserMapper {
    User selectUserById(Long id);
    List<User> selectList();
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.qfcwx.mapper.UserMapper" >
  <select id="selectUserById" resultType="com.qfcwx.pojo.User">
    SELECT
        id,username,password
    FROM user
    WHERE id = ?
  </select>
  <select id="selectList" resultType="com.qfcwx.pojo.User">
    SELECT
        id,username,password
    FROM user
  </select>
</mapper>

然后调用mapper接口的方法,就完成了对数据库的操作,对于我们而言,操作数据库的步骤明显减少了太多,其实都是Mybatis框架帮我们实现的。

三,动手写Mybatis框架

下面手写一个简单化的Mybatis框架,来分析Mybatis的底层结构以及相互调用。

本框架的思维导图

1,新建Mapper接口类和mapper.xml配置文件

如上前提概述,先写好基本使用的用法内容,然后开始构造Mybatis框架。

mapper.xml文件在resources静态资源文件夹下的mapper文件夹下,可以随意放,只要在resources文件夹下即可,方便获取!

2,引入相关依赖

需要的依赖有两个,mysql驱动依赖以连接数据库,dom4j来解析mapper.xml

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

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

3,自定义置文件

resources下新建一个配置文件db.properties,里面配置连接数据库的相关信息。

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

4,定义Configuration对象

定义一个Configuration对象,其中包含数据源、事务等信息。

声明:本文章的所有内容都是按照Mybatis的基本结构构造而成,虽没有源框架那样庞大,但底层结构完全吻合!

public class Configuration {
    private String driver; //数据库驱动
    private String url;		//连接路径
    private String userName;  //用户名
    private String passWord;  //密码
    //用于存放db.properties和mapper.xml配置文件的信息
    private Map<String, MappedStatement> statementMap = new HashMap<String, MappedStatement>();
}

5,定义MappedStatement对象

需要创建一个与mapper.xml中标签和属性对应的实体类。其中包含(namespace、id、resultType、sql…)等。

MappedStatement对象存放的是解析mapper.xml文件的内容,一个MappedStatement对象对应一条sql语句。

public class MappedStatement {
    private String namespace; //mapper接口类的全包名
    //sourceId这里存包名+方法名  com.ftx.mapper.mapper.java selectList()
    //存这个的意义是作为Configuration类的statementMap的键(唯一)
    private String sourceId;	
    private String resultType;  //返回类型
    private String sql;   //sql语句

6,定义SqlSessionFactory对象

SqlSessionMyBatis的关键对象,通过java操作MyBatis时,可看到,它是由SqlSessionFactory这个工厂来创建的。所以需要先完成SqlSessionFactory的相关代码。

Mybatis预与数据库建立连接,首先需要创建SqlSessionFactory对象,该对象的作用

  • 读取db.properties配置文件,解析数据源的相关信息,填充到configuration对象中。
  • 生产SqlSession

构造方法的作用:

创建该对象即读取数据库配置文件db.properties的信息存到configuration对象中;

获取指定文件下的所有mapper.xml文件并解析所有节点信息存到configuration对象中

public class SqlSessionFactory {
    private final Configuration configuration = new Configuration();
    /**
     * 记录mapper.xml存放的位置
     **/
    private static final String MAPPER_CONFIG_LOCATION = "mapper";
    /**
     * 记录数据路连接信息存放的文件
     **/
    private static final String DB_CONFIG_FILE = "db.properties";

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

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

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

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

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

    }

    //获取SqlSession对象
    public SqlSession openSession() {
        //将数据源信息和sql信息传进去
        return new DefaultSqlSession(configuration);
    }

7,定义SqlSession对象

SqlSessionFactory对象生产出SqlSession对象之后,SqlSession的作用

  • 定义查询接口(泛型)
  • 获取mapper对象
public interface SqlSession {
      //TODO 根据传入的条件查询单一结果
    <T> T selectOne(String statement, Object parameter);
      //TODO 查询集合
      // 方法对应的sql语句,namespace + id    传过来的值就是这种: com.qfcwx.mapper.UserMapper.selectList
    <E> List<E> selectList(String statement, Object parameter);
     //TODO 获取mapper对象
    <T> T getMapper(Class<T> type);
}

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

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

public class DefaultSqlSession implements SqlSession {

    private Configuration configuration;

    private final Executor executor;

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

	//selectOne查询一个对象,对应的List集合中只能有一个值,所以直接使用list.get(0)则可以取出。若是出现了多值的情况,	则程序抛出异常,selectList则查询所有结果。
    @Override
    public <T> T selectOne(String statement, Object parameter) {
        List<T> list = this.selectList(statement, parameter);
        if (list == null || list.size() == 0) {
            return null;
        }
        if (list.size() == 1) {
            return list.get(0);
        } else {
            throw new RuntimeException("too man result");
        }
    }

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

    //创建Mapper.java接口类的动态代理类
    @Override
    public <T> T getMapper(Class<T> type) {
        MappedProxy mappedProxy = new MappedProxy(this);
        return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[]{type}, mappedProxy);
    }

}

动态代理类

作用:上面创建代理类之后会返回一个新的Mapper接口对象,然后调用这个新的Mapper接口的任何方法,都会触发动态代理类。调用selectList()查询方法然后会执行动态代理,判断该查询方法的返回值去执行查询!

public class MappedProxy implements InvocationHandler {

    private SqlSession sqlSession;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Class<?> returnType = method.getReturnType();
        //判断返回值是否为Collection的子类
        if (Collection.class.isAssignableFrom(returnType)) {
            //method.getDeclaringClass().getName() + "." + method.getName()  类名名称+方法名 com.qfcwx.mapper.UserMapper.selectList
            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]);
        }
    }
}

8,定义Executor接口

上面说到SqlSession的功能是基于Executor来实现的,Mybatis的核心接口之一,定义了数据库操作最基本的方法,SqlSession的功能都基于它来实现现在构造Executor接口。

MyBatis源码中Executor的方法定义了查询及更新的方法。还有事务提交和回滚。这里就简单的定义一个查询方法。

public interface Executor {
    <E> List<E> query(MappedStatement statement,Object parameter);
}

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

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

实现Executor接口的实现类

public class DefaultExecutor implements Executor {

    private Configuration configuration;

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

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

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

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

9,进行测试

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

 public static void main(String[] args) {
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactory();
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);

//        User user = mapper.selectUserById(10L);
//        System.out.println(user);
        List<User> userList = mapper.selectList();
        System.out.println(userList);
    }

打印

[User{id=5, username='18838030468', password='123456'}, User{id=6, username='13333333333', password='123456'}]

posted @ 2020-04-27 17:44  你樊不樊  阅读(619)  评论(0编辑  收藏  举报