陈中正的网络日志

MyBatis 源码分析

1、MyBatis Simple Demo

一个最简单的MyBatis Demo

1.1、 代码

源码下载:mybatis-demo
对应DB表:mybatis-demo

public class UserService {
    public User getUserById(Integer userId) {
        SqlSession sqlSession = MyBatisUtil.getSqlSessionFactory().openSession();
        try {
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            return userMapper.getUserById(userId);
        } finally {
            sqlSession.close();
        }
    }
}
public class MyBatisUtil
{
    private static SqlSessionFactory factory;

    private MyBatisUtil() {
    }

    static
    {
        Reader reader = null;
        try {
            reader = Resources.getResourceAsReader("mybatis-config.xml");
        } catch (IOException e) {
            throw new RuntimeException(e.getMessage());
        }
        factory = new SqlSessionFactoryBuilder().build(reader);
    }

    public static SqlSessionFactory getSqlSessionFactory()
    {
        return factory;
    }
}
  • mybatis-config.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>
    <properties resource='jdbc.properties'/>
    <typeAliases>
        <typeAlias type='com.sivalabs.mybatisdemo.domain.User' alias='user'></typeAlias>
    </typeAliases>
    <environments default='development'>
        <environment id='development'>
            <transactionManager type='JDBC'/>
            <dataSource type='POOLED'>
                <property name='driver' value='${jdbc.driverClassName}'/>
                <property name='url' value='${jdbc.url}'/>
                <property name='username' value='${jdbc.username}'/>
                <property name='password' value='${jdbc.password}'/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource='com/sivalabs/mybatisdemo/mappers/UserMapper.xml'/>
    </mappers>
</configuration>
  • UserMapper.xml
<?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.sivalabs.mybatisdemo.mappers.UserMapper'>

    <select id='getUserById' parameterType='int' resultType='com.sivalabs.mybatisdemo.domain.User'>
        SELECT
        user_id as userId,
        email_id as emailId ,
        password,
        first_name as firstName,
        last_name as lastName
        FROM USER
        WHERE USER_ID = #{userId}
    </select>
    <!-- Instead of referencing Fully Qualified Class Names we can register Aliases in mybatis-config.xml and use Alias names. -->
    <resultMap type='com.sivalabs.mybatisdemo.domain.User' id='UserResult'>
        <id property='userId' column='user_id'/>
        <result property='emailId' column='email_id'/>
        <result property='password' column='password'/>
        <result property='firstName' column='first_name'/>
        <result property='lastName' column='last_name'/>
    </resultMap>

    <select id='getAllUsers' resultMap='UserResult'>
        SELECT * FROM USER
    </select>

1.2、代码逻辑

  • 在执行userMapper.getUserById(userId)之前,会先执行2.1和2.1代码
  • 2.3是userMapper.getUserById(userId)真正执行的代码

1.2.1 解析配置文件(#hello-world-demo-2-1)

        Reader reader = null;
        try {
            reader = Resources.getResourceAsReader("mybatis-config.xml");
        } catch (IOException e) {
            throw new RuntimeException(e.getMessage());
        }
        factory = new SqlSessionFactoryBuilder().build(reader);

该段代码主要把mybatis-config.xmlUserMapper.xml中的各种配置解析到Configuration实例中去。

1.2.2 构造UserMapper实例

SqlSession sqlSession = MyBatisUtil.getSqlSessionFactory().openSession();

UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
1.2.2.1 通过openSession()来进行实例的初始化

此处可暂时略过。

1.2.2.2 通过getMapper()方法来获取Mapper实例

此处getMapper()方法的内部实现是为UserMapper生成一个动态代理,并且返回:

  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这个类的invoke()方法来实现对应的函数调用流程。

1.2.3 执行对应查询代码

            return userMapper.getUserById(userId);

因为这里的userMapper对象是动态代理对象,所以其实这里执行的是MapperProxy.invoke()方法。
MapperProxy.invoke()方法内容如下:

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
      try {
        return method.invoke(this, 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;
  }

这里生成了一个MapperMethod 对象,并且调用mapperMethod.execute(sqlSession, args)来进行真正的DB查询逻辑。

mapperMethod.execute(sqlSession, args)的内部逻辑如下所示

1.2.3.1 找到接口对应的sql

DefaultSqlSession#selectList()方法内容如下:

  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

根据接口找到对应的sql,然后交由executor执行

1.2.3.2 执行sql,封装结果

缺省情况下executor是SimpleExecutorSimpleExecutor.doQuery()方法内容如下:

  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }
  1. 执行sql,获得结果
  2. 把结果转换成接口中声明的类型然后返回

2、MyBatis Plugin Demo

演示 MyBatis Plugin 功能的 Demo。

2.1、代码

源码下载:mybatis-demo
对应DB表:mybatis-demo

添加Interceptor类:

@Intercepts({@Signature(type = StatementHandler.class, method = "parameterize", args = {Statement.class})})
public class SQLStatsInterceptor implements Interceptor {

    public Object intercept(Invocation invocation) throws Throwable {

        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = statementHandler.getBoundSql();
        String sql = boundSql.getSql();
        System.out.println("mybatis intercept sql:" + sql);
        return invocation.proceed();
    }

    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    public void setProperties(Properties properties) {
        String dialect = properties.getProperty("dialect");
        System.out.println("mybatis intercept dialect:" + dialect);
    }
}

添加interceptor配置:

mybatis-config.xml 添加以下配置,

    <plugins>
        <plugin interceptor="com.sivalabs.mybatisdemo.interceptor.SQLStatsInterceptor">
            <property name="dialect" value="mysql"/>
        </plugin>
    </plugins>

2.2、代码逻辑

2.2.1 解析配置文件

同上。2.1 解析配置文件

2.2.2 构造UserMapper实例

SqlSession sqlSession = MyBatisUtil.getSqlSessionFactory().openSession();

UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
2.2.2.1 通过openSession()来进行实例的初始化。

openSession()的内部内部实现如下:

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

主要业务逻辑为

  1. 开启一个事务
  2. 构造一个newExecutor

configuration.newExecutor()的内部实现如下:

    executor = (Executor) interceptorChain.pluginAll(executor);

把所有的插件(Interceptor)嵌入到Executor中,具体实现如下:

  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
  public static Object wrap(Object target, Interceptor interceptor) {
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

细节已经展现,MyBatis 内部通过 Java 的动态代理来实现 Plugin(Interceptor)的嵌入。

2.2.2.2 通过getMapper()方法来获取Mapper实例。

同上 1.2.2.2

2.2.3 执行对应查询代码

同上 1.2.3

2.2.3.1 找到接口对应的sql

同上 1.2.3.1

2.2.3.2 执行sql,封装结果

缺省情况下executor是SimpleExecutorSimpleExecutor.doQuery()方法内容如下:

  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }
  1. 执行sql,获得结果
  2. 把结果转换成接口中声明的类型然后返回

其中1.调用了configuration.newStatementHandler()来构造一个 statement,其内部实现如下:

  public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

我们看到了interceptorChain.pluginAll(statementHandler),这里的逻辑便和2.2.2.1一致了,用 Java 的动态代理,把Interceptor的代码嵌入到Handler实例中。

这样一来,如 SQLStatsInterceptor 的注解中声明的那样,在调用 StatementHandler.parameterize时,便会回调Interceptor的代码逻辑了。

3、Mybatis With Spring

MyBatis 配合Spring使用时,MyBatis类库会把Mapper对象注册在Spring容器中,这样用户使用Mapper对象就十分方便了。

3.1、代码

源码下载:mybatis-spring-demo
对应DB表:mybatis-demo

UserMapper.javaUserMapper.xml1、2、中的逻辑一样,主要的区别如下:

  • 数据库配置(如dataSourcesqlSessionFactory)由原本的mybatis-config.xml中移动到Spring配置文件中
  • 添加Spring拓展点,把Mapper对象注册到Spring容器中
  • 获取Mapper对象时,由代码获取改为@AutoWired用Spring注入。

Spring配置文件如下:

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

    <!--获取数据库配置文件-->
    <context:property-placeholder location="classpath:db.properties"/>

    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>

        <property name="filters" value="stat"/>

        <property name="maxActive" value="20"/>
        <property name="initialSize" value="1"/>
        <property name="maxWait" value="60000"/>
        <property name="minIdle" value="1"/>

        <property name="timeBetweenEvictionRunsMillis" value="60000"/>
        <property name="minEvictableIdleTimeMillis" value="300000"/>

        <property name="validationQuery" value="SELECT 'x'"/>
        <property name="testWhileIdle" value="true"/>
        <property name="testOnBorrow" value="false"/>
        <property name="testOnReturn" value="false"/>
        <property name="poolPreparedStatements" value="true"/>
        <property name="maxPoolPreparedStatementPerConnectionSize" value="20"/>
    </bean>

    <!--sqlsessionFactory bean-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="configLocation" value="classpath:SqlMapConfig.xml"/>
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--自动扫描mapper接口,并注入sqlsession-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="org.mybatis.mapper"/>
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    </bean>

</beans>

代码测试用例如下:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext-dao.xml")
public class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void testGetUserById() {
        User user = userMapper.getUserById(1);
        Assert.assertNotNull(user);
        System.out.println(user);
    }
}

3.2、代码逻辑

3.2.1 把Mapper对象注册到Spring容器中

这部分逻辑主要是通过以下Spring拓展点实现的:

    <!--自动扫描mapper接口,并注入sqlsession-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="org.mybatis.mapper"/>
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    </bean>

在Spring中添加以上Beam即可注册Mapper对象到Spring容器。

因为MapperScannerConfigurer实现了Spring的BeanDefinitionRegistryPostProcessor这个拓展点,该拓展点作用如下:

Spring允许在Bean创建之前,读取Bean的元属性,并根据自己的需求对元属性进行改变 12

Spring会回调BeanDefinitionRegistryPostProcessor这个接口的postProcessBeanDefinitionRegistry方法,看下该方法内容的内容定义:

  @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));
  }

这里的逻辑是,扫描basePackage包,找出所有符合要求的Bean注册到Spring容器中。

更具体的分为以下两步:

3.2.1.1 容器中添加Mapper对象

org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan内容如下:

     * Perform a scan within the specified base packages,
     * returning the registered bean definitions.
     * <p>This method does <i>not</i> register an annotation config processor
     * but rather leaves this up to the caller.
     * @param basePackages the packages to check for annotated classes
     * @return set of beans registered if any for tooling registration purposes (never {@code null})
     */
    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.registry);
                    beanDefinitions.add(definitionHolder);
                    registerBeanDefinition(definitionHolder, this.registry);
                }
            }
        }
        return beanDefinitions;
    }

这个类的主要逻辑是找出符合要求的bean,包装成一个BeanDefinitionHolder,然后丢进Spring容器中。

3.2.1.2 指定Mapper对象的内部逻辑

org.mybatis.spring.mapper.ClassPathMapperScanner#doScan

  /**
   * Calls the parent search that will search and register all the candidates.
   * Then the registered objects are post processed to set them as
   * MapperFactoryBeans
   */
  @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;
  }

org.mybatis.spring.mapper.ClassPathMapperScanner#processBeanDefinitions

  private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    GenericBeanDefinition definition;
    for (BeanDefinitionHolder holder : beanDefinitions) {
        .....
        definition.setBeanClass(this.mapperFactoryBean.getClass());
        .....
    }
    .....
}

这里的主要逻辑是把每个BeanDefinitionHolderBeanClass都设置成了MapperFactoryBean,而MapperFactoryBeangetObject逻辑如下:

org.mybatis.spring.mapper.MapperFactoryBean#getObject

  public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
  }

getMapper的底层实现为为Mapper对象生成一个动态代理对象:

  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);
  }

这里和1.2.2.2便一样了。

3.2.2 执行对应查询代码

此部分内容和 1.2.3的逻辑一致。

Categories:  JavaWeb 

« 微服务接口定义实践 machine learning 笔记 »