关于java:几百行代码写个Mybatis原理搞的透透的

57次阅读

共计 56505 个字符,预计需要花费 142 分钟才能阅读完成。

作者:小傅哥
博客:https://bugstack.cn

积淀、分享、成长,让本人和别人都能有所播种!😄

一、前言

Mybatis 最外围的原理也是它最便于应用的体现,为什么这说?

因为咱们在应用 Mybatis 的时候,只须要定义一个不须要写实现类的接口,就能通过注解或者配置 SQL 语句的形式,对数据库进行 CRUD 操作。

那么这是怎么做到的呢,其中有一点十分重要,就是在 Spring 中能够把你的代理对象交给 Spring 容器,这个代理对象就是能够当做是 DAO 接口的具体实现类,而这个被代理的实现类就能够实现对数据库的一个操作,也就是这个封装过程被称为 ORM 框架。

说了根本的流程,咱们来做点测试,让大家能够入手操作起来!学常识,肯定是上手,能力失去!你能够通过以下源码仓库进行练习

源码:https://github.com/fuzhengwei/CodeGuide/wiki


二、把 Bean 塞到 Spring 容器,分几步

  • 对于 Bean 注册的技术场景,在咱们日常用到的技术框架中,MyBatis 是最为常见的。通过在应用 MyBatis 时都只是定义一个接口不须要写实现类,然而这个接口却能够和配置的 SQL 语句关联,执行相应的数据库操作时能够返回对应的后果。那么这个接口与数据库的操作就用到的 Bean 的代理和注册。
  • 咱们都晓得类的调用是不能间接调用没有实现的接口的,所以须要通过代理的形式给接口生成对应的实现类。接下来再通过把代理类放到 Spring 的 FactoryBean 的实现中,最初再把这个 FactoryBean 实现类注册到 Spring 容器。那么当初你的代理类就曾经被注册到 Spring 容器了,接下来就能够通过注解的形式注入到属性中。

依照这个实现形式,咱们来操作一下,看看一个 Bean 的注册过程在代码中是如何实现的。

1. 定义接口

public interface IUserDao {String queryUserInfo();

}
  • 先定义一个相似 DAO 的接口,根本这样的接口在应用 MyBatis 时还是十分常见的。前面咱们会对这个接口做代理和注册。

2. 类代理实现

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Class<?>[] classes = {IUserDao.class};    

InvocationHandler handler = (proxy, method, args) -> "你被代理了" + method.getName();
IUserDao userDao = (IUserDao) Proxy.newProxyInstance(classLoader, classes, handler); 

String res = userDao.queryUserInfo();
logger.info("测试后果:{}", res);
  • Java 自身的代理形式应用起来还是比较简单的,用法也很固定。
  • InvocationHandler 是个接口类,它对应的实现内容就是代理对象的具体实现。
  • 最初就是把代理交给 Proxy 创立代理对象,Proxy.newProxyInstance

3. 实现 Bean 工厂

public class ProxyBeanFactory implements FactoryBean {

    @Override
    public Object getObject() throws Exception {ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        Class[] classes = {IUserDao.class};
        InvocationHandler handler = (proxy, method, args) -> "你被代理了" + method.getName();

        return Proxy.newProxyInstance(classLoader, classes, handler);
    }

    @Override
    public Class<?> getObjectType() {return IUserDao.class;} 

}
  • FactoryBean 在 spring 起到着二当家的位置,它将近有 70 多个小弟(实现它的接口定义),那么它有三个办法;

    • T getObject() throws Exception; 返回 bean 实例对象
    • Class<?> getObjectType(); 返回实例类类型
    • boolean isSingleton(); 判断是否单例,单例会放到 Spring 容器中单实例缓存池中
  • 在这里咱们把下面应用 Java 代理的对象放到了 getObject() 办法中,那么当初再从 Spring 中获取到的对象,就是咱们的代理对象了。

4. Bean 注册

public class RegisterBeanFactory implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(ProxyBeanFactory.class);

        BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(beanDefinition, "userDao");
        BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);
    }

}

在 Spring 的 Bean 治理中,所有的 Bean 最终都会被注册到类 DefaultListableBeanFactory 中,以上这部分代码次要的内容包含:

  • 实现 BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry 办法,获取 Bean 注册对象。
  • 定义 Bean,GenericBeanDefinition,这里次要设置了咱们的代理类工厂。
  • 创立 Bean 定义解决类,BeanDefinitionHolder,这里须要的主要参数;定义 Bean 和名称 setBeanClass(ProxyBeanFactory.class)
  • 最初将咱们本人的 bean 注册到 spring 容器中去,registry.registerBeanDefinition()

5. 测试验证

在下面咱们曾经把自定义代理的 Bean 注册到了 Spring 容器中,接下来咱们来测试下这个代理的 Bean 被如何调用。

1. 定义 spring-config.xml

<bean id="userDao" class="org.itstack.interview.bean.RegisterBeanFactory"/>
  • 这里咱们把 RegisterBeanFactory 配置到 spring 的 xml 配置中,便于启动时加载。

2. 单元测试

@Test
public void test_IUserDao() {BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring-config.xml");
    IUserDao userDao = beanFactory.getBean("userDao", IUserDao.class);
    String res = userDao.queryUserInfo();
    logger.info("测试后果:{}", res);
}

测试后果

22:53:14.759 [main] DEBUG o.s.c.e.PropertySourcesPropertyResolver - Could not find key 'spring.liveBeansView.mbeanDomain' in any property source
22:53:14.760 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'userDao'
22:53:14.796 [main] INFO  org.itstack.interview.test.ApiTest - 测试后果:你被代理了 queryUserInfo

Process finished with exit code 0
  • 从测试后果能够看到,咱们曾经能够通过注入到 Spring 的代理 Bean 对象,实现咱们的预期后果。
  • 其实这个过程也是很多框架中用到的形式,尤其是在一些中间件开发,相似的 ORM 框架都须要应用到。

三、手写个 Mybatis

扩大上一篇源码剖析工程;itstack-demo-mybatis,减少 like 包,模拟 Mybatis 工程。残缺规程下载 https://github.com/fuzhengwei…

itstack-demo-mybatis
└── src
    ├── main
    │   ├── java
    │   │   └── org.itstack.demo
    │   │       ├── dao
    │   │       │    ├── ISchool.java        
    │   │       │    └── IUserDao.java    
    │   │       ├── like
    │   │       │    ├── Configuration.java
    │   │       │    ├── DefaultSqlSession.java
    │   │       │    ├── DefaultSqlSessionFactory.java
    │   │       │    ├── Resources.java
    │   │       │    ├── SqlSession.java
    │   │       │    ├── SqlSessionFactory.java
    │   │       │    ├── SqlSessionFactoryBuilder.java    
    │   │       │    └── SqlSessionFactoryBuilder.java    
    │   │       └── interfaces     
    │   │             ├── School.java    
    │   │            └── User.java
    │   ├── resources    
    │   │   ├── mapper
    │   │   │   ├── School_Mapper.xml
    │   │   │   └── User_Mapper.xml
    │   │   ├── props    
    │   │   │   └── jdbc.properties
    │   │   ├── spring
    │   │   │   ├── mybatis-config-datasource.xml
    │   │   │   └── spring-config-datasource.xml
    │   │   ├── logback.xml
    │   │   ├── mybatis-config.xml
    │   │   └── spring-config.xml
    │   └── webapp
    │       └── WEB-INF
    └── test
         └── java
             └── org.itstack.demo.test
                 ├── ApiLikeTest.java
                 ├── MybatisApiTest.java
                 └── SpringApiTest.java

对于整个 Demo 版本,并不是把所有 Mybatis 全副实现一遍,而是拨丝抽茧将最外围的内容展现给你,从应用上你会感触截然不同,然而实现类曾经全副被替换,外围类包含;

  • Configuration
  • DefaultSqlSession
  • DefaultSqlSessionFactory
  • Resources
  • SqlSession
  • SqlSessionFactory
  • SqlSessionFactoryBuilder
  • XNode

1. 先测试下整个 DemoJdbc 框架

ApiLikeTest.test_queryUserInfoById()

@Test
public void test_queryUserInfoById() {
    String resource = "spring/mybatis-config-datasource.xml";
    Reader reader;
    try {reader = Resources.getResourceAsReader(resource);
        SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
        SqlSession session = sqlMapper.openSession();
        
        try {User user = session.selectOne("org.itstack.demo.dao.IUserDao.queryUserInfoById", 1L);
            System.out.println(JSON.toJSONString(user));
        } finally {session.close();
            reader.close();}
    } catch (Exception e) {e.printStackTrace();
    }
}

一切顺利后果如下(新人往往会遇到各种问题);

{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000}

Process finished with exit code 0

可能乍一看这测试类齐全和 MybatisApiTest.java 测试的代码截然不同呀,也看不出区别。其实他们的引入的包是不一样;

MybatisApiTest.java 外面引入的包

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

ApiLikeTest.java 外面引入的包

import org.itstack.demo.like.Resources;
import org.itstack.demo.like.SqlSession;
import org.itstack.demo.like.SqlSessionFactory;
import org.itstack.demo.like.SqlSessionFactoryBuilder;

好!接下来咱们开始剖析这部分外围代码。

2. 加载 XML 配置文件

这里咱们采纳 mybatis 的配置文件构造进行解析,在不毁坏原有构造的状况下,最大可能的贴近源码。mybatis 独自应用的应用的时候应用了两个配置文件;数据源配置、Mapper 映射配置,如下;

mybatis-config-datasource.xml & 数据源配置

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/itstack?useUnicode=true"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="mapper/User_Mapper.xml"/>
        <mapper resource="mapper/School_Mapper.xml"/>
    </mappers>

</configuration>

User_Mapper.xml & Mapper 映射配置

<?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="org.itstack.demo.dao.IUserDao">

    <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.po.User">
        SELECT id, name, age, createTime, updateTime
        FROM user
        where id = #{id}
    </select>

    <select id="queryUserList" parameterType="org.itstack.demo.po.User" resultType="org.itstack.demo.po.User">
        SELECT id, name, age, createTime, updateTime
        FROM user
        where age = #{age}
    </select>

</mapper>

这里的加载过程与 mybaits 不同,咱们采纳 dom4j 形式。在案例中会看到最开始获取资源,如下;

ApiLikeTest.test_queryUserInfoById() & 局部截取

String resource = "spring/mybatis-config-datasource.xml";
    Reader reader;
    try {reader = Resources.getResourceAsReader(resource);
    ...

从上能够看到这是通过配置文件地址获取到了读取流的过程,从而为前面解析做根底。首先咱们先看 Resources 类,整个是咱们的资源类。

Resources.java & 资源类

/**
 * 博 客 | https://bugstack.cn
 * Create by 小傅哥 @2020
 */
public class Resources {public static Reader getResourceAsReader(String resource) throws IOException {return new InputStreamReader(getResourceAsStream(resource));
    }

    private static InputStream getResourceAsStream(String resource) throws IOException {ClassLoader[] classLoaders = getClassLoaders();
        for (ClassLoader classLoader : classLoaders) {InputStream inputStream = classLoader.getResourceAsStream(resource);
            if (null != inputStream) {return inputStream;}
        }
        throw new IOException("Could not find resource" + resource);
    }

    private static ClassLoader[] getClassLoaders() {return new ClassLoader[]{ClassLoader.getSystemClassLoader(),
                Thread.currentThread().getContextClassLoader()};
    }

}

这段代码办法的入口是 getResourceAsReader,直到往下以此做了;

  1. 获取 ClassLoader 汇合,最大限度搜寻配置文件
  2. 通过 classLoader.getResourceAsStream 读取配置资源,找到后立刻返回,否则抛出异样

3. 解析 XML 配置文件

配置文件加载后开始进行解析操作,这里咱们也仿照 mybatis 但进行简化,如下;

SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);

SqlSessionFactoryBuilder.build() & 入口构建类

public DefaultSqlSessionFactory build(Reader reader) {SAXReader saxReader = new SAXReader();
    try {Document document = saxReader.read(new InputSource(reader));
        Configuration configuration = parseConfiguration(document.getRootElement());
        return new DefaultSqlSessionFactory(configuration);
    } catch (DocumentException e) {e.printStackTrace();
    }
    return null;
}
  • 通过读取流创立 xml 解析的 Document 类
  • parseConfiguration 进行解析 xml 文件,并将后果设置到配置类中,包含;连接池、数据源、mapper 关系

SqlSessionFactoryBuilder.parseConfiguration() & 解析过程

private Configuration parseConfiguration(Element root) {Configuration configuration = new Configuration();
    configuration.setDataSource(dataSource(root.selectNodes("//dataSource")));
    configuration.setConnection(connection(configuration.dataSource));
    configuration.setMapperElement(mapperElement(root.selectNodes("mappers")));
    return configuration;
}
  • 在后面的 xml 内容中能够看到,咱们须要解析出数据库连接池信息 datasource,还有数据库语句映射关系 mappers

SqlSessionFactoryBuilder.dataSource() & 解析出数据源

private Map<String, String> dataSource(List<Element> list) {Map<String, String> dataSource = new HashMap<>(4);
    Element element = list.get(0);
    List content = element.content();
    for (Object o : content) {Element e = (Element) o;
        String name = e.attributeValue("name");
        String value = e.attributeValue("value");
        dataSource.put(name, value);
    }
    return dataSource;
}
  • 这个过程比较简单,只须要将数据源信息获取即可

SqlSessionFactoryBuilder.connection() & 获取数据库连贯

private Connection connection(Map<String, String> dataSource) {
    try {Class.forName(dataSource.get("driver"));
        return DriverManager.getConnection(dataSource.get("url"), dataSource.get("username"), dataSource.get("password"));
    } catch (ClassNotFoundException | SQLException e) {e.printStackTrace();
    }
    return null;
}
  • 这个就是 jdbc 最原始的代码,获取了数据库连接池

SqlSessionFactoryBuilder.mapperElement() & 解析 SQL 语句

private Map<String, XNode> mapperElement(List<Element> list) {Map<String, XNode> map = new HashMap<>();
    Element element = list.get(0);
    List content = element.content();
    for (Object o : content) {Element e = (Element) o;
        String resource = e.attributeValue("resource");
        try {Reader reader = Resources.getResourceAsReader(resource);
            SAXReader saxReader = new SAXReader();
            Document document = saxReader.read(new InputSource(reader));
            Element root = document.getRootElement();
            // 命名空间
            String namespace = root.attributeValue("namespace");
            // SELECT
            List<Element> selectNodes = root.selectNodes("select");
            for (Element node : selectNodes) {String id = node.attributeValue("id");
                String parameterType = node.attributeValue("parameterType");
                String resultType = node.attributeValue("resultType");
                String sql = node.getText();
                // ? 匹配
                Map<Integer, String> parameter = new HashMap<>();
                Pattern pattern = Pattern.compile("(#\\{(.*?)})");
                Matcher matcher = pattern.matcher(sql);
                for (int i = 1; matcher.find(); i++) {String g1 = matcher.group(1);
                    String g2 = matcher.group(2);
                    parameter.put(i, g2);
                    sql = sql.replace(g1, "?");
                }
                XNode xNode = new XNode();
                xNode.setNamespace(namespace);
                xNode.setId(id);
                xNode.setParameterType(parameterType);
                xNode.setResultType(resultType);
                xNode.setSql(sql);
                xNode.setParameter(parameter);
                
                map.put(namespace + "." + id, xNode);
            }
        } catch (Exception ex) {ex.printStackTrace();
        }
    }
    return map;
}
  • 这个过程首先包含是解析所有的 sql 语句,目前为了测试只解析 select 相干
  • 所有的 sql 语句为了确认惟一,都是应用;namespace + select 中的 id 进行拼接,作为 key,之后与 sql 一起寄存到 map 中。
  • 在 mybaits 的 sql 语句配置中,都有占位符,用于传参。where id = #{id} 所以咱们须要将占位符设置为问号,另外须要将占位符的程序信息与名称寄存到 map 构造,不便后续设置查问时候的入参。

4. 创立 DefaultSqlSessionFactory

最初将初始化后的配置类 Configuration,作为参数进行创立 DefaultSqlSessionFactory,如下;

public DefaultSqlSessionFactory build(Reader reader) {SAXReader saxReader = new SAXReader();
    try {Document document = saxReader.read(new InputSource(reader));
        Configuration configuration = parseConfiguration(document.getRootElement());
        return new DefaultSqlSessionFactory(configuration);
    } catch (DocumentException e) {e.printStackTrace();
    }
    return null;
}

DefaultSqlSessionFactory.java & SqlSessionFactory 的实现类

public class DefaultSqlSessionFactory implements SqlSessionFactory {
    
    private final Configuration configuration;
    
    public DefaultSqlSessionFactory(Configuration configuration) {this.configuration = configuration;}
    
    @Override
    public SqlSession openSession() {return new DefaultSqlSession(configuration.connection, configuration.mapperElement);
    }
    
}
  • 这个过程比较简单,构造函数只提供了配置类入参
  • 实现 SqlSessionFactory 的 openSession(),用于创立 DefaultSqlSession,也就能够执行 sql 操作

5. 开启 SqlSession

SqlSession session = sqlMapper.openSession();

下面这一步就是创立了 DefaultSqlSession,比较简单。如下;

@Override
public SqlSession openSession() {return new DefaultSqlSession(configuration.connection, configuration.mapperElement);
}

6. 执行 SQL 语句

User user = session.selectOne("org.itstack.demo.dao.IUserDao.queryUserInfoById", 1L);

在 DefaultSqlSession 中通过实现 SqlSession,提供数据库语句查问和敞开连接池,如下;

SqlSession.java & 定义

public interface SqlSession {<T> T selectOne(String statement);

    <T> T selectOne(String statement, Object parameter);

    <T> List<T> selectList(String statement);

    <T> List<T> selectList(String statement, Object parameter);

    void close();}

接下来看具体的执行过程,session.selectOne

DefaultSqlSession.selectOne() & 执行查问

public <T> T selectOne(String statement, Object parameter) {XNode xNode = mapperElement.get(statement);
    Map<Integer, String> parameterMap = xNode.getParameter();
    try {PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
        buildParameter(preparedStatement, parameter, parameterMap);
        ResultSet resultSet = preparedStatement.executeQuery();
        List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
        return objects.get(0);
    } catch (Exception e) {e.printStackTrace();
    }
    return null;
}
  • selectOne 就 objects.get(0);,selectList 就全副返回
  • 通过 statement 获取最后解析 xml 时候的存储的 select 标签信息;

    <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.po.User">
        SELECT id, name, age, createTime, updateTime
        FROM user
        where id = #{id}
    </select>
  • 获取 sql 语句后交给 jdbc 的 PreparedStatement 类进行执行
  • 这里还须要设置入参,咱们将入参设置进行抽取,如下;

    private void buildParameter(PreparedStatement preparedStatement, Object parameter, Map<Integer, String> parameterMap) throws SQLException, IllegalAccessException {int size = parameterMap.size();
          // 单个参数
          if (parameter instanceof Long) {for (int i = 1; i <= size; i++) {preparedStatement.setLong(i, Long.parseLong(parameter.toString()));
              }
              return;
          }
    
          if (parameter instanceof Integer) {for (int i = 1; i <= size; i++) {preparedStatement.setInt(i, Integer.parseInt(parameter.toString()));
              }
              return;
          }
    
          if (parameter instanceof String) {for (int i = 1; i <= size; i++) {preparedStatement.setString(i, parameter.toString());
              }
              return;
          }
    
          Map<String, Object> fieldMap = new HashMap<>();
          // 对象参数
          Field[] declaredFields = parameter.getClass().getDeclaredFields();
          for (Field field : declaredFields) {String name = field.getName();
              field.setAccessible(true);
              Object obj = field.get(parameter);
              field.setAccessible(false);
              fieldMap.put(name, obj);
          }
    
          for (int i = 1; i <= size; i++) {String parameterDefine = parameterMap.get(i);
              Object obj = fieldMap.get(parameterDefine);
    
              if (obj instanceof Short) {preparedStatement.setShort(i, Short.parseShort(obj.toString()));
                  continue;
              }
    
              if (obj instanceof Integer) {preparedStatement.setInt(i, Integer.parseInt(obj.toString()));
                  continue;
              }
    
              if (obj instanceof Long) {preparedStatement.setLong(i, Long.parseLong(obj.toString()));
                  continue;
              }
    
              if (obj instanceof String) {preparedStatement.setString(i, obj.toString());
                  continue;
              }
    
              if (obj instanceof Date) {preparedStatement.setDate(i, (java.sql.Date) obj);
              }
    
          }
    
      }
    • 单个参数比较简单间接设置值即可,Long、Integer、String …
    • 如果是一个类对象,须要通过获取 Field 属性,与参数 Map 进行匹配设置
  • 设置参数后执行查问 preparedStatement.executeQuery()
  • 接下来须要将查问后果转换为咱们的类(次要是反射类的操作),resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));

    private <T> List<T> resultSet2Obj(ResultSet resultSet, Class<?> clazz) {List<T> list = new ArrayList<>();
        try {ResultSetMetaData metaData = resultSet.getMetaData();
            int columnCount = metaData.getColumnCount();
            // 每次遍历行值
            while (resultSet.next()) {T obj = (T) clazz.newInstance();
                for (int i = 1; i <= columnCount; i++) {Object value = resultSet.getObject(i);
                    String columnName = metaData.getColumnName(i);
                    String setMethod = "set" + columnName.substring(0, 1).toUpperCase() + columnName.substring(1);
                    Method method;
                    if (value instanceof Timestamp) {method = clazz.getMethod(setMethod, Date.class);
                    } else {method = clazz.getMethod(setMethod, value.getClass());
                    }
                    method.invoke(obj, value);
                }
                list.add(obj);
            }
        } catch (Exception e) {e.printStackTrace();
        }
        return list;
    }
    • 次要通过反射生成咱们的类对象,这个类的类型定义在 sql 标签上
    • 工夫类型须要判断后处理,Timestamp,与 java 不是一个类型

7. Sql 查问补充阐明

sql 查问有入参、有不须要入参、有查问一个、有查问汇合,只须要正当包装即可,例如上面的查问汇合,入参是对象类型;

ApiLikeTest.test_queryUserList()

@Test
public void test_queryUserList() {
    String resource = "spring/mybatis-config-datasource.xml";
    Reader reader;
    try {reader = Resources.getResourceAsReader(resource);
        SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
        SqlSession session = sqlMapper.openSession();
        
        try {User req = new User();
            req.setAge(18);
            List<User> userList = session.selectList("org.itstack.demo.dao.IUserDao.queryUserList", req);
            System.out.println(JSON.toJSONString(userList));
        } finally {session.close();
            reader.close();}
    } catch (Exception e) {e.printStackTrace();
    }
    
}

*测试后果:*

[{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000},{"age":18,"createTime":1576944000000,"id":2,"name":"豆豆","updateTime":1576944000000}]

Process finished with exit code 0

四、源码剖析(mybatis)

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.4.6</version>
</dependency>

Mybatis 的整个源码还是很大的,以下次要将局部核心内容进行整顿剖析,以便于后续剖析 Mybatis 与 Spring 整合的源码局部。简要包含;容器初始化、配置文件解析、Mapper 加载与动静代理。

1. 从一个简略的案例开始

要学习 Mybatis 源码,最好的形式肯定是从一个简略的点进入,而不是从 Spring 整合开始剖析。SqlSessionFactory 是整个 Mybatis 的外围实例对象,SqlSessionFactory 对象的实例又通过 SqlSessionFactoryBuilder 对象来取得。SqlSessionFactoryBuilder 对象能够从 XML 配置文件加载配置信息,而后创立 SqlSessionFactory。如下例子:

MybatisApiTest.java

public class MybatisApiTest {

    @Test
    public void test_queryUserInfoById() {
        String resource = "spring/mybatis-config-datasource.xml";
        Reader reader;
        try {reader = Resources.getResourceAsReader(resource);
            SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);

            SqlSession session = sqlMapper.openSession();
            try {User user = session.selectOne("org.itstack.demo.dao.IUserDao.queryUserInfoById", 1L);
                System.out.println(JSON.toJSONString(user));
            } finally {session.close();
                reader.close();}
        } catch (IOException e) {e.printStackTrace();
        }
    }

}

dao/IUserDao.java

public interface IUserDao {User queryUserInfoById(Long id);

}

spring/mybatis-config-datasource.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/itstack?useUnicode=true"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="mapper/User_Mapper.xml"/>
    </mappers>

</configuration>

如果一切顺利,那么会有如下后果:

{"age":18,"createTime":1571376957000,"id":1,"name":"花花","updateTime":1571376957000}

从下面的代码块能够看到,外围代码;SqlSessionFactoryBuilder().build(reader),负责 Mybatis 配置文件的加载、解析、构建等职责,直到最终能够通过 SqlSession 来执行并返回后果。

2. 容器初始化

从下面代码能够看到,SqlSessionFactory 是通过 SqlSessionFactoryBuilder 工厂类创立的,而不是间接应用结构器。容器的配置文件加载和初始化流程如下:

  • 流程外围类

    • SqlSessionFactoryBuilder
    • XMLConfigBuilder
    • XPathParser
    • Configuration

SqlSessionFactoryBuilder.java

public class SqlSessionFactoryBuilder {public SqlSessionFactory build(Reader reader) {return build(reader, null, null);
  }

  public SqlSessionFactory build(Reader reader, String environment) {return build(reader, environment, null);
  }

  public SqlSessionFactory build(Reader reader, Properties properties) {return build(reader, null, properties);
  }

  public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {ErrorContext.instance().reset();
      try {reader.close();
      } catch (IOException e) {// Intentionally ignore. Prefer previous error.}
    }
  }

  public SqlSessionFactory build(InputStream inputStream) {return build(inputStream, null, null);
  }

  public SqlSessionFactory build(InputStream inputStream, String environment) {return build(inputStream, environment, null);
  }

  public SqlSessionFactory build(InputStream inputStream, Properties properties) {return build(inputStream, null, properties);
  }

  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {ErrorContext.instance().reset();
      try {inputStream.close();
      } catch (IOException e) {// Intentionally ignore. Prefer previous error.}
    }
  }
    
  public SqlSessionFactory build(Configuration config) {return new DefaultSqlSessionFactory(config);
  }

}

从下面的源码能够看到,SqlSessionFactory 提供三种形式 build 构建对象;

  • 字节流:java.io.InputStream
  • 字符流:java.io.Reader
  • 配置类:org.apache.ibatis.session.Configuration

那么,字节流、字符流都会创立配置文件解析类:XMLConfigBuilder,并通过 parser.parse()生成 Configuration,最初调用配置类构建办法生成 SqlSessionFactory。

XMLConfigBuilder.java

public class XMLConfigBuilder extends BaseBuilder {

  private boolean parsed;
  private final XPathParser parser;
  private String environment;
  private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();

  ...
  public XMLConfigBuilder(Reader reader, String environment, Properties props) {this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
  }
  ...
}  
  1. XMLConfigBuilder 对于 XML 文件的加载和解析都委托于 XPathParser,最终应用 JDK 自带的 javax.xml 进行 XML 解析(XPath)
  2. XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver)

    1. reader:应用字符流创立新的输出源,用于对 XML 文件的读取
    2. validation:是否进行 DTD 校验
    3. variables:属性配置信息
    4. entityResolver:Mybatis 硬编码了 new XMLMapperEntityResolver()提供 XML 默认解析器

XMLMapperEntityResolver.java

public class XMLMapperEntityResolver implements EntityResolver {

  private static final String IBATIS_CONFIG_SYSTEM = "ibatis-3-config.dtd";
  private static final String IBATIS_MAPPER_SYSTEM = "ibatis-3-mapper.dtd";
  private static final String MYBATIS_CONFIG_SYSTEM = "mybatis-3-config.dtd";
  private static final String MYBATIS_MAPPER_SYSTEM = "mybatis-3-mapper.dtd";

  private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd";
  private static final String MYBATIS_MAPPER_DTD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd";

  /*
   * Converts a public DTD into a local one
   * 
   * @param publicId The public id that is what comes after "PUBLIC"
   * @param systemId The system id that is what comes after the public id.
   * @return The InputSource for the DTD
   * 
   * @throws org.xml.sax.SAXException If anything goes wrong
   */
  @Override
  public InputSource resolveEntity(String publicId, String systemId) throws SAXException {
    try {if (systemId != null) {String lowerCaseSystemId = systemId.toLowerCase(Locale.ENGLISH);
        if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) {return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId);
        } else if (lowerCaseSystemId.contains(MYBATIS_MAPPER_SYSTEM) || lowerCaseSystemId.contains(IBATIS_MAPPER_SYSTEM)) {return getInputSource(MYBATIS_MAPPER_DTD, publicId, systemId);
        }
      }
      return null;
    } catch (Exception e) {throw new SAXException(e.toString());
    }
  }

  private InputSource getInputSource(String path, String publicId, String systemId) {
    InputSource source = null;
    if (path != null) {
      try {InputStream in = Resources.getResourceAsStream(path);
        source = new InputSource(in);
        source.setPublicId(publicId);
        source.setSystemId(systemId);        
      } catch (IOException e) {// ignore, null is ok}
    }
    return source;
  }

}
  1. Mybatis 依赖于 dtd 文件进行进行解析,其中的 ibatis-3-config.dtd 次要是用于兼容用处
  2. getInputSource(String path, String publicId, String systemId)的调用外面有两个参数 publicId(公共标识符)和 systemId(零碎标示符)

XPathParser.java

public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) {commonConstructor(validation, variables, entityResolver);
  this.document = createDocument(new InputSource(reader));
}

private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
  this.validation = validation;
  this.entityResolver = entityResolver;
  this.variables = variables;
  XPathFactory factory = XPathFactory.newInstance();
  this.xpath = factory.newXPath();}

private Document createDocument(InputSource inputSource) {
  // important: this must only be called AFTER common constructor
  try {DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setValidating(validation);
    factory.setNamespaceAware(false);
    factory.setIgnoringComments(true);
    factory.setIgnoringElementContentWhitespace(false);
    factory.setCoalescing(false);
    factory.setExpandEntityReferences(true);
    DocumentBuilder builder = factory.newDocumentBuilder();
    builder.setEntityResolver(entityResolver);
    builder.setErrorHandler(new ErrorHandler() {
      @Override
      public void error(SAXParseException exception) throws SAXException {throw exception;}
      @Override
      public void fatalError(SAXParseException exception) throws SAXException {throw exception;}
      @Override
      public void warning(SAXParseException exception) throws SAXException {}});
    return builder.parse(inputSource);
  } catch (Exception e) {throw new BuilderException("Error creating document instance.  Cause:" + e, e);
  }
  
}    
  1. 从上到下能够看到次要是为了创立一个 Mybatis 的文档解析器,最初依据 builder.parse(inputSource)返回 Document
  2. 失去 XPathParser 实例后,接下来在调用办法:this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);

     XMLConfigBuilder.this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
    
     private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {super(new Configuration());
       ErrorContext.instance().resource("SQL Mapper Configuration");
       this.configuration.setVariables(props);
       this.parsed = false;
       this.environment = environment;
       this.parser = parser;
     }
  3. 其中调用了父类的构造函数

    public abstract class BaseBuilder {
      protected final Configuration configuration;
      protected final TypeAliasRegistry typeAliasRegistry;
      protected final TypeHandlerRegistry typeHandlerRegistry;
    
      public BaseBuilder(Configuration configuration) {
        this.configuration = configuration;
        this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
        this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();}
    }
  4. XMLConfigBuilder 创立实现后,sqlSessionFactoryBuild 调用 parser.parse()创立 Configuration

    public class XMLConfigBuilder extends BaseBuilder {public Configuration parse() {if (parsed) {throw new BuilderException("Each XMLConfigBuilder can only be used once.");
           }
           parsed = true;
           parseConfiguration(parser.evalNode("/configuration"));
           return configuration;
         }
    }

3. 配置文件解析

这一部分是整个 XML 文件解析和装载的核心内容,其中包含;

  1. 属性解析 propertiesElement
  2. 加载 settings 节点 settingsAsProperties
  3. 载自定义 VFS loadCustomVfs
  4. 解析类型别名 typeAliasesElement
  5. 加载插件 pluginElement
  6. 加载对象工厂 objectFactoryElement
  7. 创建对象包装器工厂 objectWrapperFactoryElement
  8. 加载反射工厂 reflectorFactoryElement
  9. 元素设置 settingsElement
  10. 加载环境配置 environmentsElement
  11. 数据库厂商标识加载 databaseIdProviderElement
  12. 加载类型处理器 typeHandlerElement
  13. (外围 ) 加载 mapper 文件 mapperElement
parseConfiguration(parser.evalNode("/configuration"));

private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      // 属性解析 propertiesElement
      propertiesElement(root.evalNode("properties"));
      // 加载 settings 节点 settingsAsProperties
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      // 加载自定义 VFS loadCustomVfs
      loadCustomVfs(settings);
      // 解析类型别名 typeAliasesElement
      typeAliasesElement(root.evalNode("typeAliases"));
      // 加载插件 pluginElement
      pluginElement(root.evalNode("plugins"));
      // 加载对象工厂 objectFactoryElement
      objectFactoryElement(root.evalNode("objectFactory"));
      // 创建对象包装器工厂 objectWrapperFactoryElement
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      // 加载反射工厂 reflectorFactoryElement
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      // 元素设置
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      // 加载环境配置 environmentsElement
      environmentsElement(root.evalNode("environments"));
      // 数据库厂商标识加载 databaseIdProviderElement
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      // 加载类型处理器 typeHandlerElement
      typeHandlerElement(root.evalNode("typeHandlers"));
      // 加载 mapper 文件 mapperElement
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {throw new BuilderException("Error parsing SQL Mapper Configuration. Cause:" + e, e);
    }
} 

所有的 root.evalNode()底层都是调用 XML DOM 办法:Object evaluate(String expression, Object item, QName returnType),表达式参数 expression,通过 XObject resultObject = eval(expression, item)返回最终节点内容,能够参考 http://mybatis.org/dtd/mybati…,如下;

<!ELEMENT configuration (properties?, settings?, typeAliases?, typeHandlers?, objectFactory?, objectWrapperFactory?, reflectorFactory?, plugins?, environments?, databaseIdProvider?, mappers?)>
 
<!ELEMENT databaseIdProvider (property*)>
<!ATTLIST databaseIdProvider
type CDATA #REQUIRED
>
 
<!ELEMENT properties (property*)>
<!ATTLIST properties
resource CDATA #IMPLIED
url CDATA #IMPLIED
>
 
<!ELEMENT property EMPTY>
<!ATTLIST property
name CDATA #REQUIRED
value CDATA #REQUIRED
>
 
<!ELEMENT settings (setting+)>
 
<!ELEMENT setting EMPTY>
<!ATTLIST setting
name CDATA #REQUIRED
value CDATA #REQUIRED
>
 
<!ELEMENT typeAliases (typeAlias*,package*)>
 
<!ELEMENT typeAlias EMPTY>
<!ATTLIST typeAlias
type CDATA #REQUIRED
alias CDATA #IMPLIED
>
 
<!ELEMENT typeHandlers (typeHandler*,package*)>
 
<!ELEMENT typeHandler EMPTY>
<!ATTLIST typeHandler
javaType CDATA #IMPLIED
jdbcType CDATA #IMPLIED
handler CDATA #REQUIRED
>
 
<!ELEMENT objectFactory (property*)>
<!ATTLIST objectFactory
type CDATA #REQUIRED
>
 
<!ELEMENT objectWrapperFactory EMPTY>
<!ATTLIST objectWrapperFactory
type CDATA #REQUIRED
>
 
<!ELEMENT reflectorFactory EMPTY>
<!ATTLIST reflectorFactory
type CDATA #REQUIRED
>
 
<!ELEMENT plugins (plugin+)>
 
<!ELEMENT plugin (property*)>
<!ATTLIST plugin
interceptor CDATA #REQUIRED
>
 
<!ELEMENT environments (environment+)>
<!ATTLIST environments
default CDATA #REQUIRED
>
 
<!ELEMENT environment (transactionManager,dataSource)>
<!ATTLIST environment
id CDATA #REQUIRED
>
 
<!ELEMENT transactionManager (property*)>
<!ATTLIST transactionManager
type CDATA #REQUIRED
>
 
<!ELEMENT dataSource (property*)>
<!ATTLIST dataSource
type CDATA #REQUIRED
>
 
<!ELEMENT mappers (mapper*,package*)>
 
<!ELEMENT mapper EMPTY>
<!ATTLIST mapper
resource CDATA #IMPLIED
url CDATA #IMPLIED
class CDATA #IMPLIED
>
 
<!ELEMENT package EMPTY>
<!ATTLIST package
name CDATA #REQUIRED
>

mybatis-3-config.dtd 定义文件中有 11 个配置文件,如下;

  1. properties?,
  2. settings?,
  3. typeAliases?,
  4. typeHandlers?,
  5. objectFactory?,
  6. objectWrapperFactory?,
  7. reflectorFactory?,
  8. plugins?,
  9. environments?,
  10. databaseIdProvider?,
  11. mappers?

以上每个配置都是可选。最终配置内容会保留到 org.apache.ibatis.session.Configuration,如下;

public class Configuration {

  protected Environment environment;
  // 容许在嵌套语句中应用分页(RowBounds)。如果容许应用则设置为 false。默认为 false
  protected boolean safeRowBoundsEnabled;
  // 容许在嵌套语句中应用分页(ResultHandler)。如果容许应用则设置为 false。protected boolean safeResultHandlerEnabled = true;
  // 是否开启主动驼峰命名规定(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的相似映射。默认 false
  protected boolean mapUnderscoreToCamelCase;
  // 当开启时,任何办法的调用都会加载该对象的所有属性。否则,每个属性会按需加载。默认值 false (true in ≤3.4.1)
  protected boolean aggressiveLazyLoading;
  // 是否容许繁多语句返回多后果集(须要兼容驱动)。protected boolean multipleResultSetsEnabled = true;
  // 容许 JDBC 反对主动生成主键,须要驱动兼容。这就是 insert 时获取 mysql 自增主键 /oracle sequence 的开关。注:一般来说, 这是心愿的后果, 应该默认值为 true 比拟适合。protected boolean useGeneratedKeys;
  // 应用列标签代替列名, 一般来说, 这是心愿的后果
  protected boolean useColumnLabel = true;
  // 是否启用缓存 {默认是开启的,可能这也是你的面试题}
  protected boolean cacheEnabled = true;
  // 指定当后果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)办法,这对于有 Map.keySet() 依赖或 null 值初始化的时候是有用的。protected boolean callSettersOnNulls;
  // 容许应用办法签名中的名称作为语句参数名称。为了应用该个性,你的工程必须采纳 Java 8 编译,并且加上 -parameters 选项。(从 3.4.1 开始)protected boolean useActualParamName = true;
  // 当返回行的所有列都是空时,MyBatis 默认返回 null。当开启这个设置时,MyBatis 会返回一个空实例。请留神,它也实用于嵌套的后果集 (i.e. collectioin and association)。(从 3.4.2 开始)注:这里应该拆分为两个参数比拟适合, 一个用于后果集,一个用于单记录。通常来说,咱们会心愿后果集不是 null,单记录依然是 null
  protected boolean returnInstanceForEmptyRow;
  // 指定 MyBatis 减少到日志名称的前缀。protected String logPrefix;
  // 指定 MyBatis 所用日志的具体实现,未指定时将主动查找。个别倡议指定为 slf4j 或 log4j
  protected Class <? extends Log> logImpl;
   // 指定 VFS 的实现, VFS 是 mybatis 提供的用于拜访 AS 内资源的一个简便接口
  protected Class <? extends VFS> vfsImpl;
  // MyBatis 利用本地缓存机制(Local Cache)避免循环援用(circular references)和减速反复嵌套查问。默认值为 SESSION,这种状况下会缓存一个会话中执行的所有查问。若设置值为 STATEMENT,本地会话仅用在语句执行上,对雷同 SqlSession 的不同调用将不会共享数据。protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
  // 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。某些驱动须要指定列的 JDBC 类型,少数状况间接用个别类型即可,比方 NULL、VARCHAR 或 OTHER。protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
  // 指定对象的哪个办法触发一次提早加载。protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(Arrays.asList(new String[] {"equals", "clone", "hashCode", "toString"}));
  // 设置超时工夫,它决定驱动期待数据库响应的秒数。默认不超时
  protected Integer defaultStatementTimeout;
  // 为驱动的后果集设置默认获取数量。protected Integer defaultFetchSize;
  // SIMPLE 就是一般的执行器;REUSE 执行器会重用预处理语句(prepared statements);BATCH 执行器将重用语句并执行批量更新。protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
  // 指定 MyBatis 应如何主动映射列到字段或属性。NONE 示意勾销主动映射;PARTIAL 只会主动映射没有定义嵌套后果集映射的后果集。FULL 会主动映射任意简单的后果集(无论是否嵌套)。protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
  // 指定发现主动映射指标未知列(或者未知属性类型)的行为。这个值应该设置为 WARNING 比拟适合
  protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
  // settings 下的 properties 属性
  protected Properties variables = new Properties();
  // 默认的反射器工厂, 用于操作属性、结构器不便
  protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
  // 对象工厂, 所有的类 resultMap 类都须要依赖于对象工厂来实例化
  protected ObjectFactory objectFactory = new DefaultObjectFactory();
  // 对象包装器工厂, 次要用来在创立非原生对象, 比方减少了某些监控或者非凡属性的代理类
  protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
  // 提早加载的全局开关。当开启时,所有关联对象都会提早加载。特定关联关系中可通过设置 fetchType 属性来笼罩该项的开关状态。protected boolean lazyLoadingEnabled = false;
  // 指定 Mybatis 创立具备提早加载能力的对象所用到的代理工具。MyBatis 3.3+ 应用 JAVASSIST
  protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
  // MyBatis 能够依据不同的数据库厂商执行不同的语句,这种多厂商的反对是基于映射语句中的 databaseId 属性。protected String databaseId;
  ...
}

以上能够看到,Mybatis 把所有的配置;resultMap、Sql 语句、插件、缓存等都保护在 Configuration 中。这里还有一个小技巧,在 Configuration 还有一个 StrictMap 外部类,它继承于 HashMap 欠缺了 put 时防重、get 时取不到值的异样解决,如下;

protected static class StrictMap<V> extends HashMap<String, V> {

    private static final long serialVersionUID = -4950446264854982944L;
    private final String name;

    public StrictMap(String name, int initialCapacity, float loadFactor) {super(initialCapacity, loadFactor);
      this.name = name;
    }

    public StrictMap(String name, int initialCapacity) {super(initialCapacity);
      this.name = name;
    }

    public StrictMap(String name) {super();
      this.name = name;
    }

    public StrictMap(String name, Map<String, ? extends V> m) {super(m);
      this.name = name;
    }
}    

(外围)加载 mapper 文件 mapperElement

Mapper 文件解决是 Mybatis 框架的外围服务,所有的 SQL 语句都编写在 Mapper 中,这块也是咱们剖析的重点,其余模块能够后续解说。

XMLConfigBuilder.parseConfiguration()->mapperElement(root.evalNode(“mappers”));

private void mapperElement(XNode parent) throws Exception {if (parent != null) {for (XNode child : parent.getChildren()) {// 如果要同时应用 package 主动扫描和通过 mapper 明确指定要加载的 mapper,肯定要确保 package 主动扫描的范畴不蕴含明确指定的 mapper,否则在通过 package 扫描的 interface 的时候,尝试加载对应 xml 文件的 loadXmlResource()的逻辑中呈现判重出错,报 org.apache.ibatis.binding.BindingException 异样,即便 xml 文件中蕴含的内容和 mapper 接口中蕴含的语句不反复也会出错,包含加载 mapper 接口时主动加载的 xml mapper 也一样会出错。if ("package".equals(child.getName())) {String mapperPackage = child.getStringAttribute("name");
         configuration.addMappers(mapperPackage);
       } else {String resource = child.getStringAttribute("resource");
         String url = child.getStringAttribute("url");
         String mapperClass = child.getStringAttribute("class");
         if (resource != null && url == null && mapperClass == null) {ErrorContext.instance().resource(resource);
           InputStream inputStream = Resources.getResourceAsStream(resource);
           XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
           mapperParser.parse();} else if (resource == null && url != null && mapperClass == null) {ErrorContext.instance().resource(url);
           InputStream inputStream = Resources.getUrlAsStream(url);
           XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
           mapperParser.parse();} else if (resource == null && url == null && mapperClass != null) {Class<?> mapperInterface = Resources.classForName(mapperClass);
           configuration.addMapper(mapperInterface);
         } else {throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
         }
       }
     }
   }
}
  • Mybatis 提供了两类配置 Mapper 的办法,第一类是应用 package 主动搜寻的模式,这样指定 package 下所有接口都会被注册为 mapper,也是在 Spring 中比拟罕用的形式,例如:

    <mappers>
      <package name="org.itstack.demo"/>
    </mappers>
  • 另外一类是明确指定 Mapper,这又能够通过 resource、url 或者 class 进行细分,例如;

    <mappers>
        <mapper resource="mapper/User_Mapper.xml"/>
        <mapper class=""/>
        <mapper url=""/>
    </mappers>

4. Mapper 加载与动静代理

通过 package 形式主动搜寻加载,生成对应的 mapper 代理类,代码块和流程,如下;

private void mapperElement(XNode parent) throws Exception {if (parent != null) {for (XNode child : parent.getChildren()) {if ("package".equals(child.getName())) {String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
      } else {...}
    }
  }
}

Mapper 加载到生成代理对象的流程中,次要的外围类包含;

  1. XMLConfigBuilder
  2. Configuration
  3. MapperRegistry
  4. MapperAnnotationBuilder
  5. MapperProxyFactory

MapperRegistry.java

解析加载 Mapper

public void addMappers(String packageName, Class<?> superType) {// mybatis 框架提供的搜寻 classpath 下指定 package 以及子 package 中符合条件 (注解或者继承于某个类 / 接口) 的类,默认应用 Thread.currentThread().getContextClassLoader()返回的加载器, 和 spring 的工具类必由之路。ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();   
  // 无条件的加载所有的类, 因为调用方传递了 Object.class 作为父类, 这也给当前的指定 mapper 接口预留了余地
  resolverUtil.find(new ResolverUtil.IsA(superType), packageName); 
 // 所有匹配的 calss 都被存储在 ResolverUtil.matches 字段中
  Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
  for (Class<?> mapperClass : mapperSet) {   
    // 调用 addMapper 办法进行具体的 mapper 类 / 接口解析
    addMapper(mapperClass);
  }
}

生成代理类:MapperProxyFactory

public <T> void addMapper(Class<T> type) {    
  // 对于 mybatis mapper 接口文件,必须是 interface,不能是 class
  if (type.isInterface()) {if (hasMapper(type)) {throw new BindingException("Type" + type + "is already known to the MapperRegistry.");
    }
    boolean loadCompleted = false;
    try {      
      // 为 mapper 接口创立一个 MapperProxyFactory 代理
      knownMappers.put(type, new MapperProxyFactory<T>(type));
      // It's important that the type is added before the parser is run
      // otherwise the binding may automatically be attempted by the
      // mapper parser. If the type is already known, it won't try.
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      parser.parse();
      loadCompleted = true;
    } finally {if (!loadCompleted) {knownMappers.remove(type);
      }
    }
  }
}

在 MapperRegistry 中保护了接口类与代理工程的映射关系,knownMappers;

private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();

MapperProxyFactory.java

public class MapperProxyFactory<T> {
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
  public MapperProxyFactory(Class<T> mapperInterface) {this.mapperInterface = mapperInterface;}
  public Class<T> getMapperInterface() {return mapperInterface;}
  public Map<Method, MapperMethod> getMethodCache() {return methodCache;}
  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface}, mapperProxy);
  }
  public T newInstance(SqlSession sqlSession) {final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }
}

如上是 Mapper 的代理类工程,构造函数中的 mapperInterface 就是对应的接口类,当实例化时候会取得具体的 MapperProxy 代理,外面次要蕴含了 SqlSession。

五、源码剖析(mybatis-spring)

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.3.2</version>
</dependency>

作为一款好用的 ORM 框架,肯定是萝莉脸 ( 单纯 )、御姐心( 弱小 ),铺的了床( 屏蔽与 JDBC 间接打交道 )、暖的了房( 速度性能好)!鉴于这些长处简直在国内互联网大部分开发框架都会应用到 Mybatis,尤其在一些须要高性能的场景下须要优化 sql 那么肯定须要手写 sql 在 xml 中。那么,筹备好了吗!开始剖析剖析它的源码;

1. 从一个简略的案例开始

与剖析 mybatis 源码一样,先做一个简略的案例;定义 dao、编写配置文件、junit 单元测试;

SpringApiTest.java

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring-config.xml")
public class SpringApiTest {private Logger logger = LoggerFactory.getLogger(SpringApiTest.class);

    @Resource
    private ISchoolDao schoolDao;
    @Resource
    private IUserDao userDao;

    @Test
    public void test_queryRuleTreeByTreeId(){School ruleTree = schoolDao.querySchoolInfoById(1L);
        logger.info(JSON.toJSONString(ruleTree));

        User user = userDao.queryUserInfoById(1L);
        logger.info(JSON.toJSONString(user));
    }

}

spring-config-datasource.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 1. 数据库连接池:DriverManagerDataSource 也能够应用 DBCP2-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="${db.jdbc.driverClassName}"/>
        <property name="url" value="${db.jdbc.url}"/>
        <property name="username" value="${db.jdbc.username}"/>
        <property name="password" value="${db.jdbc.password}"/>
    </bean>

    <!-- 2. 配置 SqlSessionFactory 对象 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!-- 注入数据库连接池 -->
        <property name="dataSource" ref="dataSource"/>
        <!-- 配置 MyBaties 全局配置文件:mybatis-config.xml -->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <!-- 扫描 entity 包 应用别名 -->
        <property name="typeAliasesPackage" value="org.itstack.demo.po"/>
        <!-- 扫描 sql 配置文件:mapper 须要的 xml 文件 -->
        <property name="mapperLocations" value="classpath:mapper/*.xml"/>
    </bean>

    <!-- 3. 配置扫描 Dao 接口包,动静实现 Dao 接口,注入到 spring 容器中 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!-- 注入 sqlSessionFactory -->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
        <!-- 给出须要扫描 Dao 接口包,多个逗号隔开 -->
        <property name="basePackage" value="org.itstack.demo.dao"/>
    </bean>
              
</beans>

如果一切顺利,那么会有如下后果:

{"address":"北京市海淀区颐和园路 5 号","createTime":1571376957000,"id":1,"name":"北京大学","updateTime":1571376957000}
{"age":18,"createTime":1571376957000,"id":1,"name":"花花","updateTime":1571376957000}

从下面单元测试的代码能够看到,两个没有办法体的注解就这么神奇的执行了咱们的 xml 中的配置语句并输入了后果。其实次要得益于以下两个类;

  • org.mybatis.spring.SqlSessionFactoryBean
  • org.mybatis.spring.mapper.MapperScannerConfigurer

2. 扫描拆卸注册(MapperScannerConfigurer)

MapperScannerConfigurer 为整个 Dao 接口层生成动静代理类注册,启动到了核心作用。这个类实现了如下接口,用来对扫描的 Mapper 进行解决:

  • BeanDefinitionRegistryPostProcessor
  • InitializingBean
  • ApplicationContextAware
  • BeanNameAware

整体类图如下;

执行流程如下;

下面的类图 + 流程图,其实曾经很分明的形容了 MapperScannerConfigurer 初始化过程,但对于头一次看的新人来说仍旧是我太难了,好持续!

MapperScannerConfigurer.java & 局部截取

@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {if (this.processPropertyPlaceHolders) {processPropertyPlaceHolders();
  }
  ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
  scanner.setAddToConfig(this.addToConfig);
  scanner.setAnnotationClass(this.annotationClass);
  scanner.setMarkerInterface(this.markerInterface);
  scanner.setSqlSessionFactory(this.sqlSessionFactory);
  scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
  scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
  scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
  scanner.setResourceLoader(this.applicationContext);
  scanner.setBeanNameGenerator(this.nameGenerator);
  scanner.registerFilters();
  scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
  • 实现了 BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry 用于注册 Bean 到 Spring 容器中
  • 306 行:new ClassPathMapperScanner(registry); 硬编码类门路扫描器,用于解析 Mybatis 的 Mapper 文件
  • 317 行:scanner.scan 对 Mapper 进行扫描。这里蕴含了一个继承类实现关系的调用,也就是本文结尾的测试题。

ClassPathMapperScanner.java & 局部截取

@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
  if (beanDefinitions.isEmpty()) {logger.warn("No MyBatis mapper was found in'" + Arrays.toString(basePackages) + "'package. Please check your configuration.");
  } else {processBeanDefinitions(beanDefinitions);
  }
  return beanDefinitions;
}
  • 优先调用父类的 super.doScan(basePackages); 进行注册 Bean 信息

ClassPathBeanDefinitionScanner.java & 局部截取

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {Assert.notEmpty(basePackages, "At least one base package must be specified");
    Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>();
    for (String basePackage : basePackages) {Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
        for (BeanDefinition candidate : candidates) {ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
            candidate.setScope(scopeMetadata.getScopeName());
            String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
            if (candidate instanceof AbstractBeanDefinition) {postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
            }
            if (candidate instanceof AnnotatedBeanDefinition) {AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate)
            }
            if (checkCandidate(beanName, candidate)) {BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
                definitionHolder =
                        AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.regi
                beanDefinitions.add(definitionHolder);
                registerBeanDefinition(definitionHolder, this.registry);
            }
        }
    }
    return beanDefinitions;
}
  • 优先调用了父类的 doScan 办法,用于 Mapper 扫描和 Bean 的定义以及注册到 DefaultListableBeanFactory。{DefaultListableBeanFactory 是 Spring 中 IOC 容器的始祖,所有须要实例化的类都须要注册进来,之后在初始化}
  • 272 行:findCandidateComponents(basePackage),扫描 package 包门路,对于注解类的有另外的形式,大同小异
  • 288 行:registerBeanDefinition(definitionHolder, this.registry); 注册 Bean 信息的过程,最终会调用到:org.springframework.beans.factory.support.DefaultListableBeanFactory

ClassPathMapperScanner.java & 局部截取

**processBeanDefinitions(beanDefinitions);**

private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
  GenericBeanDefinition definition;
  for (BeanDefinitionHolder holder : beanDefinitions) {definition = (GenericBeanDefinition) holder.getBeanDefinition();
    if (logger.isDebugEnabled()) {logger.debug("Creating MapperFactoryBean with name'" + holder.getBeanName() 
        + "'and'" + definition.getBeanClassName() + "'mapperInterface");
    }
    // the mapper interface is the original class of the bean
    // but, the actual class of the bean is MapperFactoryBean
    definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
    definition.setBeanClass(this.mapperFactoryBean.getClass());
    definition.getPropertyValues().add("addToConfig", this.addToConfig);
    boolean explicitFactoryUsed = false;
    if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
      explicitFactoryUsed = true;
    } else if (this.sqlSessionFactory != null) {definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
      explicitFactoryUsed = true;
    }
    if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {if (explicitFactoryUsed) {logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
      }
      definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
      explicitFactoryUsed = true;
    } else if (this.sqlSessionTemplate != null) {if (explicitFactoryUsed) {logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
      }
      definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
      explicitFactoryUsed = true;
    }
    if (!explicitFactoryUsed) {if (logger.isDebugEnabled()) {logger.debug("Enabling autowire by type for MapperFactoryBean with name'" + holder.getBeanName() + "'.");
      }
      definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
    }
  }
}
  • 163 行:super.doScan(basePackages);,调用完父类办法后开始执行外部办法:processBeanDefinitions(beanDefinitions)
  • 186 行:definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); 设置 BeanName 参数,也就是咱们的:ISchoolDao、IUserDao
  • 187 行 :definition.setBeanClass(this.mapperFactoryBean.getClass());,设置 BeanClass,接口自身是没有类的,那么这里将MapperFactoryBean 类设置进来,最终所有的 dao 层接口类都是这个MapperFactoryBean

MapperFactoryBean.java & 局部截取

这个类有继承也有接口实现,最好先理解下整体类图,如下;

这个类就十分重要了,最终所有的 sql 信息执行都会通过这个类获取 getObject(),也就是 SqlSession 获取 mapper 的代理类:MapperProxyFactory->MapperProxy

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {

  private Class<T> mapperInterface;

  private boolean addToConfig = true;

  public MapperFactoryBean() {//intentionally empty}
  
  public MapperFactoryBean(Class<T> mapperInterface) {this.mapperInterface = mapperInterface;}

  /**  
   * 当 SpringBean 容器初始化时候会调用到 checkDaoConfig(),他是继承类中的形象办法
   * {@inheritDoc}
   */
  @Override
  protected void checkDaoConfig() {super.checkDaoConfig();

    notNull(this.mapperInterface, "Property'mapperInterface'is required");

    Configuration configuration = getSqlSession().getConfiguration();
    if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
      try {configuration.addMapper(this.mapperInterface);
      } catch (Exception e) {logger.error("Error while adding the mapper'" + this.mapperInterface + "'to configuration.", e);
        throw new IllegalArgumentException(e);
      } finally {ErrorContext.instance().reset();}
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public T getObject() throws Exception {return getSqlSession().getMapper(this.mapperInterface);
  }

  ...
}
  • 72 行:checkDaoConfig(),当 SpringBean 容器初始化时候会调用到 checkDaoConfig(),他是继承类中的形象办法
  • 95 行:getSqlSession().getMapper(this.mapperInterface);,通过接口获取 Mapper(代理类),调用过程如下;

    • DefaultSqlSession.getMapper(Class<T> type),获取 Mapper
    • Configuration.getMapper(Class<T> type, SqlSession sqlSession),从配置中获取
    • MapperRegistry.getMapper(Class<T> type, SqlSession sqlSession),从注册核心获取到实例化生成

      public <T> T getMapper(Class<T> type, SqlSession sqlSession) {final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
        if (mapperProxyFactory == null) {throw new BindingException("Type" + type + "is not known to the MapperRegistry.");
        }
        try {return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {throw new BindingException("Error getting mapper instance. Cause:" + e, e);
        }
      }
      
    • mapperProxyFactory.newInstance(sqlSession);,通过反射工程生成 MapperProxy

      @SuppressWarnings("unchecked")
      protected T newInstance(MapperProxy<T> mapperProxy) {return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface}, mapperProxy);
      }
      public T newInstance(SqlSession sqlSession) {final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
      }

MapperProxy.java & 局部截取

public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache;

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

  private MapperMethod cachedMapperMethod(Method method) {MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
  }

  @UsesJava7
  private Object invokeDefaultMethod(Object proxy, Method method, Object[] args)
      throws Throwable {
    final Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class
        .getDeclaredConstructor(Class.class, int.class);
    if (!constructor.isAccessible()) {constructor.setAccessible(true);
    }
    final Class<?> declaringClass = method.getDeclaringClass();
    return constructor
        .newInstance(declaringClass,
            MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
                | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC)
        .unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args);
  }

  ...
}
  • 58 行:final MapperMethod mapperMethod = cachedMapperMethod(method);,从缓存中获取 MapperMethod
  • 59 行 :mapperMethod.execute(sqlSession, args);,执行 SQL 语句,并返回后果(到这对于查问获取后果就到骨头(干) 层了);INSERT、UPDATE、DELETE、SELECT

    public Object execute(SqlSession sqlSession, Object[] args) {
      Object result;
      switch (command.getType()) {
        case INSERT: {Object param = method.convertArgsToSqlCommandParam(args);
          result = rowCountResult(sqlSession.insert(command.getName(), param));
          break;
        }
        case UPDATE: {Object param = method.convertArgsToSqlCommandParam(args);
          result = rowCountResult(sqlSession.update(command.getName(), param));
          break;
        }
        case DELETE: {Object param = method.convertArgsToSqlCommandParam(args);
          result = rowCountResult(sqlSession.delete(command.getName(), param));
          break;
        }
        case SELECT:
          if (method.returnsVoid() && method.hasResultHandler()) {executeWithResultHandler(sqlSession, args);
            result = null;
          } else if (method.returnsMany()) {result = executeForMany(sqlSession, args);
          } else if (method.returnsMap()) {result = executeForMap(sqlSession, args);
          } else if (method.returnsCursor()) {result = executeForCursor(sqlSession, args);
          } else {Object param = method.convertArgsToSqlCommandParam(args);
            result = sqlSession.selectOne(command.getName(), param);
          }
          break;
        case FLUSH:
          result = sqlSession.flushStatements();
          break;
        default:
          throw new BindingException("Unknown execution method for:" + command.getName());
      }
      if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {throw new BindingException("Mapper method'" + command.getName() 
            + "attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
      }
      return result;
    }

以上对于 MapperScannerConfigurer 这一层就剖析完了,从扫描定义注入到为 Spring 容器筹备 Bean 的信息,代理、反射、SQL 执行,根本就包含全副核心内容了,接下来在剖析下 SqlSessionFactoryBean

3. SqlSession 容器工厂初始化(SqlSessionFactoryBean)

SqlSessionFactoryBean 初始化过程中须要对一些本身内容进行解决,因而也须要实现如下接口;

  • FactoryBean<SqlSessionFactory>
  • InitializingBean -> void afterPropertiesSet() throws Exception
  • ApplicationListener<ApplicationEvent>

以上的流程其实曾经很清晰的形容整个外围流程,但同样对于新手上路会有阻碍,那么!好,持续!

SqlSessionFactoryBean.java & 局部截取

public void afterPropertiesSet() throws Exception {notNull(dataSource, "Property'dataSource'is required");
  notNull(sqlSessionFactoryBuilder, "Property'sqlSessionFactoryBuilder'is required");
  state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
            "Property'configuration'and'configLocation'can not specified with together");
  this.sqlSessionFactory = buildSqlSessionFactory();}
  • afterPropertiesSet(),InitializingBean 接口为 bean 提供了初始化办法的形式,它只包含 afterPropertiesSet 办法,但凡继承该接口的类,在初始化 bean 的时候都会执行该办法。
  • 380 行:buildSqlSessionFactory(); 外部办法构建,外围性能持续往下看。

SqlSessionFactoryBean.java & 局部截取

protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
  Configuration configuration;
  XMLConfigBuilder xmlConfigBuilder = null;
  
  ...

  if (!isEmpty(this.mapperLocations)) {for (Resource mapperLocation : this.mapperLocations) {if (mapperLocation == null) {continue;}
      try {XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
            configuration, mapperLocation.toString(), configuration.getSqlFragments());
        xmlMapperBuilder.parse();} catch (Exception e) {throw new NestedIOException("Failed to parse mapping resource:'" + mapperLocation + "'", e);
      } finally {ErrorContext.instance().reset();}
      if (LOGGER.isDebugEnabled()) {LOGGER.debug("Parsed mapper file:'" + mapperLocation + "'");
      }
    }
  } else {if (LOGGER.isDebugEnabled()) {LOGGER.debug("Property'mapperLocations'was not specified or no matching resources found");
    }
  }
  return this.sqlSessionFactoryBuilder.build(configuration);
}
  • 513 行:for (Resource mapperLocation : this.mapperLocations) 循环解析 Mapper 内容
  • 519 行:XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(…) 解析 XMLMapperBuilder
  • 521 行:xmlMapperBuilder.parse() 执行解析,具体如下;

XMLMapperBuilder.java & 局部截取

public class XMLMapperBuilder extends BaseBuilder {
   private final XPathParser parser;
   private final MapperBuilderAssistant builderAssistant;
   private final Map<String, XNode> sqlFragments;
   private final String resource;

   private void bindMapperForNamespace() {String namespace = builderAssistant.getCurrentNamespace();
     if (namespace != null) {
       Class<?> boundType = null;
       try {boundType = Resources.classForName(namespace);
       } catch (ClassNotFoundException e) {//ignore, bound type is not required}
       if (boundType != null) {if (!configuration.hasMapper(boundType)) {
           // Spring may not know the real resource name so we set a flag
           // to prevent loading again this resource from the mapper interface
           // look at MapperAnnotationBuilder#loadXmlResource
           configuration.addLoadedResource("namespace:" + namespace);
           configuration.addMapper(boundType);
         }
       }
     }
   }
}
  • 这里 413 行 十分重要,configuration.addMapper(boundType);,真正到了增加 Mapper 到配置核心

MapperRegistry.java & 局部截取

public class MapperRegistry {public <T> void addMapper(Class<T> type) {if (type.isInterface()) {if (hasMapper(type)) {throw new BindingException("Type" + type + "is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {knownMappers.put(type, new MapperProxyFactory<T>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {if (!loadCompleted) {knownMappers.remove(type);
        }
      }
    }
  }
  
}
  • 67 行:创立代理工程 knownMappers.put(type, new MapperProxyFactory<T>(type));

截至到这,MapperScannerConfigurer、SqlSessionFactoryBean,两个类干的事件就相交融了;

  • 第一个用于扫描 Dao 接口设置代理类注册到 IOC 中,用于后续生成 Bean 实体类,MapperFactoryBean,并能够通过 mapperInterface 从 Configuration 获取 Mapper
  • 另一个用于生成 SqlSession 工厂初始化,解析 Mapper 里的 XML 配置进行动静代理 MapperProxyFactory->MapperProxy 注入到 Configuration 的 Mapper
  • 最终在注解类的帮忙下进行办法注入,等执行操作时候即可取得动静代理对象,从而执行相应的 CRUD 操作

    @Resource
    private ISchoolDao schoolDao;
    
    schoolDao.querySchoolInfoById(1L);

六、综上总结

  • 剖析过程较长篇幅也很大,不肯定一天就能看懂整个流程,但当耐下心来一点点钻研,还是能够取得很多的播种的。当前在遇到这类的异样就能够迎刃而解了,同时也有助于面试、招聘!
  • 之所以剖析 Mybatis 最开始是想在 Dao 上加自定义注解,发现切面拦挡不到。想到这是被动静代理的类,之后层层往往下扒直到 MapperProxy.invoke!当然,Mybatis 提供了自定义插件开发。
  • 以上的源码剖析只是对局部核心内容进行剖析,如果心愿理解全副能够参考资料;MyBatis 3 源码深度解析,并调试代码。IDEA 中还是很不便看源码的,包含能够查看类图、调用程序等。
  • mybatis、mybatis-spring 中其实最重要的是将 Mapper 配置文件解析与接口类组装成代理类进行映射,以此来不便对数据库的 CRUD 操作。从源码剖析后,能够取得更多的编程教训(套路)。
  • Mybatis 相干链接;

    • https://github.com/mybatis/mybatis-3
    • https://mybatis.org/mybatis-3/zh/index.html

正文完
 0