• 手写Mybatis源码(原来真的很简单!!!)



    一、JDBC操作数据库_问题分析

    JDBC使用流程

    1. 加载数据库驱动
    2. 创建数据库连接
    3. 创建编译对象
    4. 设置入参执行SQL
    5. 返回结果集

    代码示例

    public class JDBCTest {
        public static void main(String[] args) throws Exception {
            Connection connection = null;
            PreparedStatement preparedStatement = null;
            ResultSet resultSet = null;
            try {
                // 加载数据库驱动
                Class.forName("com.mysql.jdbc.Driver");
                // 通过驱动管理类获取数据库链接
                connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis",
                "root","123456789");
                // 定义sql语句?表示占位符
                String sql = "select * from user where name = ?";
                // 获取预处理statement
                preparedStatement = connection.prepareStatement(sql);
                // 设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值
                preparedStatement.setString(1, "zhangsan");
                // 向数据库发出sql执行查询,查询出结果集
                resultSet = preparedStatement.executeQuery();
                // 遍历查询结果集
                while (resultSet.next()) {
                    int id = resultSet.getInt("id");
                    String username = resultSet.getString("name");
                    // 封装User
                    User user = new User();
                    user.setId(id);
                    user.setName(username);
                    System.out.println(user);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 释放资源
                if (resultSet != null) {
                    try {
                        resultSet.close();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
                if (preparedStatement != null) {
                    try {
                        preparedStatement.close();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
                if (connection != null) {
                    try {
                        connection.close();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    问题分析

    存在问题解决思路
    数据库配置信息存在硬编码问题使用配置文件
    频繁创建、释放数据库连接问题使用数据库连接池
    SQL语句使用配置文件
    数据库配置信息存在硬编码问题使用配置文件

    二、自定义持久层框架_思路分析

    主要分两部分,项目使用端:平常写代码所说的后台服务;持久层框架:即项目使用端引入的jar包

    在这里插入图片描述

    核心接口/类重点说明:

    类名定义角色定位分工协作
    Resources资源辅助类负责读取配置文件转化为输入流
    Configuration数据库资源类负责存储数据库连接信息
    MappedStatementSQL与结果集资源类负责存储SQL映射定义、存储结果集映射定义
    SqlSessionFactoryBuilder会话工厂构建者负责解析配置文件,创建会话工厂SqlSessionFactory
    SqlSessionFactory会话工厂负责创建会话SqlSession
    SqlSession会话指派执行器Executor
    Executor执行器负责执行SQL (配合指定资源Mapped Statement)

    项目使用端:

    1. 引入自定义持久层框架的jar包
    2. sqlMapConfig.xml:数据库配置信息,以及mapper.xml的全路径
    3. mapper.xml:SQL配置信息,存放SQL语句、参数类型、返回值类型相关信息

    注意: sqlMapConfig.xml中引入mapper.xml是为了只读取一次配置文件,否则每个实体类会有一个mapper.xml,则需要读取很多次

    自定义框架本身:

    1. 加载配置文件:根据配置文件的路径,加载配置文件成字节输入流,存储在内存中

    在这里插入图片描述

    1. 创建两个javaBean(容器对象):存放配置文件解析出来的内容

    在这里插入图片描述

    1. 解析配置文件(使用dom4j),并创建SqlSession会话对象

    在这里插入图片描述

    1. 创建SqlSessionFactory接口以及实现类DefaultSqlSessionFactory

    在这里插入图片描述

    1. 创建SqlSession接口以及实现类DefaultSqlSession

    在这里插入图片描述

    SqlSession接口定义以上方法,DefaultSqlSession来决定什么操作调用对应的sql执行器

    1. 创建Executor执行器接口以及实现类SimpleExecutor简单执行器

    在这里插入图片描述

    三、自定义框架_编码

    项目使用端

    创建sqlMapConfig核心配置文件:

    <configuration>
    
        
        <dataSource>
            <property name="driverClassname" value="com.mysql.jdbc.Driver" />
            <property name="url" value="jdbc:mysql://localhost:3306/mybatis" />
            <property name="username" value="root" />
            <property name="password" value="123456789" />
        dataSource>
    
        
        <mappers>
            <mapper resource="mapper/UserMapper.xml"/>
        mappers>
    
    configuration>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    创建映射配置文件:

    获取某个sql语句的唯一标示statementId:namespace.id 如:user.selectList

    <mapper namespace="user">
    
        
        <select id="selectList" resultType="com.xc.pojo.User" >
            select * from user
        select>
    
        
        <select id="selectOne" resultType="com.xc.pojo.User" parameterType="com.xc.pojo.User" >
            select * from user where id = #{id} and name = #{name}
        select>
    
    mapper>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    User实体:

    @Data
    public class User {
        private Integer id;
        private String name;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    pom.xml引入自定义框架

     <dependency>
         <groupId>com.xcgroupId>
         <artifactId>own-mybatisartifactId>
         <version>1.0-SNAPSHOTversion>
     dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    自定义框架本身

    1、加载配置文件

    • 根据配置文件的路径,加载配置文件成字节输入流,存到内存中
    public class Resources {
        public static InputStream getResourceAsStream(String path){
            return Resources.class.getClassLoader().getResourceAsStream(path);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2、创建两个配置类对象

    • 映射配置类:mapper.xml解析出来内容
    • 每个pojo实体都会对应一个mapper.xml文件即一个MapperStatement对象
    • sqlCommandType:第四节 自定义框架_优化才会用到
    @Data
    public class MapperStatement {
    
        //唯一标识 statementId:namespace.id
        private String statementId;
    
        //返回值类型
        private String resultType;
    
        //参数类型
        private String parameterType;
    
        //sql语句
        private String sql;
        
        // 判断当前是什么操作的一个属性-增删改查
        private String sqlCommandType;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 核心配置类:数据库配置信息以及映射配置类的map集合
    • 将多个MapperStatement对象存入Map集合,statementId(namespace.id)作为key
    • 将所有的配置文件都聚合到Configuration中,方便一次读取以及统一管理
    @Data
    public class Configuration {
    
        //数据源对象
        private DataSource dataSource;
    
        //map.xml对象集合 key:statementId
        private Map<String,MapperStatement> mapperStatementMap = new HashMap<>();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    3、解析配置文件,填充配置类对象

    • XMLConfigBuilder类的parse方法:解析核心配置类,返回Configuration对象
    • 创建SqlSession工厂对象,以便之后创建SqlSession会话
    public class SqlSessionFactoryBuilder {
    
        public SqlSessionFactory build(InputStream inputStream) throws Exception {
            //1.解析配置文件,封装容器对象:专门解析核心配置文件的解析类
            XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
            Configuration configuration = xmlConfigBuilder.parse(inputStream);
            //2.创建SqlSessionFactory工厂对象
            return new DefaultSqlSessionFactory(configuration);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • XMLConfigBuilder核心配置解析类里面嵌套着XMLMapperBuilder映射配置文件解析类
    • 输入流转化为Document对象,一是根据property标签获取数据库配置信息并创建数据源添加到configuration
    • 二是根据mapper标签通过XMLMapperBuilder解析类遍历解析配置文件同样添加到configuration的map集合类
    public class XMLConfigBuilder {
    
        private Configuration configuration;
    
        public XMLConfigBuilder() {
            this.configuration = new Configuration();
        }
    
    	// 使用dom4j+xpath解析
        public Configuration parse(InputStream inputStream) throws Exception {
            //将xml转化为Document对象
            Document document = new SAXReader().read(inputStream);
            //获取跟节点,对于sqlMapConfig.xml来说就是标签
            Element rootElement = document.getRootElement();
    
            // -------------解析数据库配置文件----------------
    
            // "//"表示从匹配选择的当前节点,而不考虑它们的位置
            // 即这里获取数据源url用户密码信息
            // 例:
            List<Element> propertyList = rootElement.selectNodes("//property");
            Properties properties = new Properties();
            for (Element element : propertyList) {
                // 获取标签中,name和value属性的值
                String name = element.attributeValue("name");
                String value = element.attributeValue("value");
                properties.setProperty(name,value);
            }
            // 创建数据源对象
            DruidDataSource dataSource = new DruidDataSource();
            dataSource.setDriverClassName(properties.getProperty("driverClassName"));
            dataSource.setUrl(properties.getProperty("url"));
            dataSource.setUsername(properties.getProperty("username"));
            dataSource.setPassword(properties.getProperty("password"));
            // 将创建好的数据源添加到Configuration对象中
            configuration.setDataSource(dataSource);
    
            // -------------解析映射配置文件----------------
    
            /*
            1.获取映射配置文件路径
            2.根据路径进行映射文件的加载解析
            3.封装到MapperStatement,存入configuration的map集合中
            */
            // 例:
            List<Element> mapperList = rootElement.selectNodes("//mapper");
            for (Element element : mapperList) {
                String resource = element.attributeValue("resource");
                InputStream resourceAsStream = Resources.getResourceAsStream(resource);
                // XMLMapperBuilder 专门解析映射配置文件的对象-最后会存入configuration的map集合对象中
                XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
                xmlMapperBuilder.parse(resourceAsStream);
            }
            return configuration;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 与XMLConfigBuilder解析类原理一样
    • 传入configuration,并将解析好的MapperStatement对象添加到mapperStatementMap
    public class XMLMapperBuilder {
    
        private Configuration configuration;
    
        public XMLMapperBuilder(Configuration configuration) {
            this.configuration = configuration;
        }
    
        public void parse(InputStream resourceAsStream) throws Exception {
            // 将输入流转化为Document对象,并获取跟节点
            Document document = new SAXReader().read(resourceAsStream);
            Element rootElement = document.getRootElement();
    
            // 例:
            String namespace = rootElement.attributeValue("namespace");
            /* 例:
                
            */
            List<Element> selectList = rootElement.selectNodes("//select");
            for (Element element : selectList) {
                String id = element.attributeValue("id");
                String resultType = element.attributeValue("resultType");
                String parameterType = element.attributeValue("parameterType");
                String sql = element.getTextTrim();
    
                // 封装MapperStatement对象
                MapperStatement mapperStatement = new MapperStatement();
                String statementId = namespace + "." + id;
                mapperStatement.setStatementId(statementId);
                mapperStatement.setParameterType(parameterType);
                mapperStatement.setResultType(resultType);
                mapperStatement.setSql(sql);
                //第四节 自定义框架_优化才会用到
                mapperStatement.setSqlCommandType("select");
    
                // 添加到configurations的map集合中
                configuration.getMapperStatementMap().put(statementId,mapperStatement);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    4、创建SqlSessionFactory工厂接口及DefaultSqlSessionFactory实现类

    • 为了创建SqlSession会话,调用增删改查方法
    public interface SqlSessionFactory {
    
        // 创建SqlSession对象
        SqlSession openSession();
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 创建简单执行器,与核心配置类共同创建SqlSession会话实现类
    • configuration提供数据配置和sql以及参数和结果集封装
    • simpleExecutor提供JDBC执行sql底层原理
    public class DefaultSqlSessionFactory implements SqlSessionFactory {
    
        private Configuration configuration;
    
        public DefaultSqlSessionFactory(Configuration configuration) {
            this.configuration = configuration;
        }
    
        @Override
        public SqlSession openSession() {
            // 1.创建执行器对象-具体的包装jdbc的sql操作,关闭连接等
            Executor simpleExecutor = new SimpleExecutor();
    
            // 2.创建sqlSession对象-判断执行增删改查哪些操作等
            return new DefaultSqlSession(configuration,simpleExecutor);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    5、创建SqlSession会话接口及DefaultSqlSession实现类

    • statementId(“namespace.id”):定位具体Mapper.xml的sql语句以及入参和返回
    • param:替换sql语句中的占位符?,可能字符串、对象、Map、集合
    public interface SqlSession {
    
        // 查询多个结果
        <E> List<E> selectList(String statementId, Object param) throws Exception;
    
        // 查询单个结果
        <T> T selectOne(String statementId, Object param) throws Exception;
    
        // 清理资源
        void close();
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 利用聚合进来的configuration对象获取MapperStatement映射配置对象向下传给执行器
    • 另外一个聚合进来的executor简单执行器来执行底层JDBC操作
    • DefaultSqlSession的作用则是聚合配置类分发到不同执行器的不同方法
    • 执行器种类:简单执行器、可重用执行器、批量执行器(这里只模拟第一种)
    public class DefaultSqlSession implements SqlSession {
    
        private Configuration configuration;
        private Executor executor;
    
        public DefaultSqlSession(Configuration configuration, Executor executor) {
            this.configuration = configuration;
            this.executor = executor;
        }
    
        @Override
        public <E> List<E> selectList(String statementId, Object param) throws Exception {
            // 根据StatementId获取映射配置对象MapperStatement
            MapperStatement mapperStatement = configuration.getMapperStatementMap().get(statementId);
            // 然后将具体的查询操作委派给SimpleExecutor执行器
            // 执行底层jdbc需要:1.数据库配置,2.sql配置信息
            return executor.query(configuration,mapperStatement,param);
        }
    
        @Override
        public <T> T selectOne(String statementId, Object param) throws Exception {
            // 调用selectList()
            List<Object> selectList = selectList(statementId, param);
            if (selectList.size() == 1){
                return (T) selectList.get(0);
            }else if (selectList.size() > 1){
                throw new Exception("返回数据不止一条!!!");
            }else {
                return null;
            }
        }
    
        @Override
        public void close() {
            executor.close();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    6、创建Executor执行器接口及SimpleExecutor实现类

    • 执行器接口定义增删改查方法,具体的JDBC底层操作由它的实现类来完成
    public interface Executor { 
    
        <E> List<E> query(Configuration configuration, MapperStatement mapperStatement, Object param) 
        	throws Exception;
    
        void close();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 执行器实现类整体流程就是JDBC那一套,从加载驱动到处理结果集
    • getBoundSql方法功能:
      • 一是将 标签的parameterType属性获取全限定类名,反射获取Class对象
        1. 遍历parameterMappingList,获取字段名,加上Class对象获取Field属性类
        2. query查询方法有个param参数,即入参对象(有可能字符串,集合这里只考虑对象),通过Field属性和param对象通过反射获取属性值
      • 结果集根据标签的返回值类型,创建返回对象 String resultType = mapperStatement.getResultType(); Class<?> resultTypeClass = Class.forName(resultType); Object obj = resultTypeClass.newInstance(); // 结果集的元数据信息:字段名,字段值等 // resultSet: 一条结果集对应一张表的所有字段 ResultSetMetaData metaData = resultSet.getMetaData(); for (int i = 1; i <= metaData.getColumnCount(); i++) { //数据库字段名 String columnName = metaData.getColumnName(i); // 字段的值 Object value = resultSet.getObject(columnName); // columnName:数据库字段,而下方需要实体中的字段,如果两边不一样,则这需要有一个转化 // 获取读写方法即get、set方法 PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass); // 获取读方法-字段的set方法 Method writeMethod = propertyDescriptor.getWriteMethod(); // 反射为返回对象赋值 // 参数1:实例对象 参数2:要设置的值 writeMethod.invoke(obj,value); } list.add((E) obj); } return list; } /** * @Description 获取?占位符的sql,以及保存#{}中的字段名称 * * 1、将标签的id、parameterType、resultType一一对应
      • sqlSession创建频繁:通过动态代理创建IUserDao的实现类,内容则是sqlSession.selectOne和sqlSession.selectList;这样sqlSession只需要创建一次,以后每次需要User的CRUD,则调用代理对象对应方法即可

      2、优化代码

      在sqlSession中添加方法

      public interface SqlSession {
      
      	...
      
          // 生成代理对象
          <T> T getMapper(Class<?> mapperClass);
      
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8

      对应实现类方法

      • 动态代理对象:不需要编译,运行期间生成字节码,通过传入的类加载器加载,只存在于内存中
      • mapperClass:UserMapper或IUserDao接口的Class对象
      • 代理类调用接口中的方法,则会被拦截进入invoke方法,因为没有目标类,则具体的实现都在invoke里面了
      • 通过mapperClass和方法名获取到statementId
      • statementId和参数有了,但是DefaultSqlSession要有CRUD查询和更新操作,所以sqlCommandType来区分
      public class DefaultSqlSession implements SqlSession {
      
          private Configuration configuration;
          private Executor executor;
      
          public DefaultSqlSession(Configuration configuration, Executor executor) {
              this.configuration = configuration;
              this.executor = executor;
          }
      
          @Override
          public <E> List<E> selectList(String statementId, Object param) throws Exception {
              // 根据StatementId获取映射配置对象MapperStatement
              MapperStatement mapperStatement = configuration.getMapperStatementMap().get(statementId);
              // 然后将具体的查询操作委派给SimpleExecutor执行器
              // 执行底层jdbc需要:1.数据库配置,2.sql配置信息
              return executor.query(configuration,mapperStatement,param);
          }
      
          @Override
          public <T> T selectOne(String statementId, Object param) throws Exception {
              // 调用selectList()
              List<Object> selectList = selectList(statementId, param);
              if (selectList.size() == 1){
                  return (T) selectList.get(0);
              }else if (selectList.size() > 1){
                  throw new Exception("返回数据不止一条!!!");
              }else {
                  return null;
              }
          }
      
          @Override
          public void close() {
              executor.close();
          }
      
      
          @Override
          public <T> T getMapper(Class<?> mapperClass) {
      
              // 使用JDK动态代理生成基于接口的对象
              // 1、创建一个类(代理类),实现目标接口,实现所有的方法实现
              // 2、动态代理类:代码运行期间生成的,而非编译期
              Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), 
              	new Class[]{mapperClass}, new InvocationHandler() {
                  // proxy:生成的代理对象本身,很少用
                  // method:调用接口中哪个方法,则执行对应代理里的对应方法
                  // args:调用方法的参数
                  @Override
                  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                      // 执行底层JDBC
                      // 1.获取statementId
                      // ps:约定接口中的方法名要与