Mybatis源码解析

目录

1.mybatis源码结构分析

2.mybatis-spring源码结构分析

3.缓存Cache分析

4.SqlSession分析

4.Interceptor分析

5.总结

6.参考文章


1.mybatis源码结构分析

通过maven的方式引入mybatis的不同版本的包,其中mybatis包主要是实现ORM机制。

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>${mybatis-version}</version>
</dependency>

通过mybatis访问数据库的执行步骤如下:

1)初始化过程中,完成相关配置解析,建立SqlSessionFactory,用于后续创建SqlSession对象。

2)当有调用数据库操作时,通过openSession方法,获取一个SqlSession对象。

3)执行SqlSession的select/update等方法,执行相关SQL语句。

4)执行SQL语句时,首先根据查询条件和映射语句,构建CacheKey对象,检查缓存中是否有对应的查询结果,如果有直接返回;如果没有就进一步从DB中查询数据。

5)采用PrepareStatement方式获取数据库中的数据,首先获取数据库链接(一般是从数据库连接池中获取),然后进行预编译处理,再执行sql语句返回ResultSet,通过TypeHandler的对象进行映射解析处理。

6)完成数据处理,返回SQL结果数据。在执行语句结束,会对SqlSession进行close相关操作。

类的关键包说明:

org.apache.ibatis.annotations:这里主要是SQL的相关注解,方便直接在代码层面书写sql语句,可以不依赖sql的xml文件。

org.apache.ibatis.cache: 这里主要是关于mybatis的缓存相关,mybatis的缓存是session级别的,且通过namespace进行隔离的,缓存是采用HashMap实现的,源码内部底层是org.apache.ibatis.cache.impl.PerpetualCache类用来支撑缓存的实现,其中该模块主要用的设计模式是装饰者模式。

org.apache.ibatis.datasource: 这里主要是与DataSource数据源相关的,实现了JNDI的数据源工厂;这里包含简易的数据源连接池和非数据源连接池的实现。

org.apache.ibatis.executor: 这里主要实现对sql的执行处理,实现从sql的进入执行,并返回结果的全过程。

org.apache.ibatis.plugin: 这里可以实现org.apache.ibatis.plugin.Interceptor接口,来对mybatis的sql执行进行拦截处理,常见的如打印日志,或者做分页拦截等。

org.apache.ibatis.session: 这里主要是针对mybatis的session进行管理,这里有创建SqlSession的工厂,和Session管理类。

org.apache.ibatis.transaction: 这里主要用于对数据库的事务进行相关操作,mybatis的事务隔离级别依赖数据库自身的隔离级别。

org.apache.ibatis.type: 这里主要是针对各种类型的转换的处理,主要是java类型与数据库类型的映射关联管理。

mybatis源码中,几个关键的类如下:

org.apache.ibatis.session.SqlSession:数据库操作的会话接口类,对数据库的所有操作都是基于会话相关,通过SqlSessionManager类中的SqlSessionInterceptor实现对事务的commit。

org.apache.ibatis.cache.Cache: mybatis的缓存接口类,如实现缓存的FIFO,LRU等等都是依赖这个接口的实现。

org.apache.ibatis.session.Configuration: mybatis的所有配置归属在这个类中,作为mybatis的中心配置。

org.apache.ibatis.executor.Executor: 该接口是操作数据库的抽象服务,是数据库操作的核心类。

2.mybatis-spring源码结构分析

通过maven的方式引入mybatis-spring的不同版本,注意与spring的版本相配套应用,该jar实现了将mybatis集成到Spring框架中。

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>${mybatis-spring-version}</version>
</dependency>

该jar中org.mybatis.spring.mapper.MapperScannerConfigurer类是关键,该类中实现了对mybatis和spring的关键集成,且实现了基于接口扫描的方式建立sql相关映射,通过扫描数据库的操作的interface就能实现生成spring的bean对象,减少了很多不必要的重复代码。我们看一下该类的关键说明,如下源码。类中通过basepackage和SqlSessionFactory实现了将对应的SqlSessionFactory与扫描的包的关联,通过org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry方法,在Spring容器的初始化的时候,完成了扫描以及加载相关的操作。

/**
 * BeanDefinitionRegistryPostProcessor that searches recursively starting from a base package for
 * interfaces and registers them as {@code MapperFactoryBean}. Note that only interfaces with at
 * least one method will be registered; concrete classes will be ignored.
 * <p>
 * This class was a {code BeanFactoryPostProcessor} until 1.0.1 version. It changed to  
 * {@code BeanDefinitionRegistryPostProcessor} in 1.0.2. See https://jira.springsource.org/browse/SPR-8269
 * for the details.
 * <p>
 * The {@code basePackage} property can contain more than one package name, separated by either
 * commas or semicolons.
 * <p>
 * This class supports filtering the mappers created by either specifying a marker interface or an
 * annotation. The {@code annotationClass} property specifies an annotation to search for. The
 * {@code markerInterface} property specifies a parent interface to search for. If both properties
 * are specified, mappers are added for interfaces that match <em>either</em> criteria. By default,
 * these two properties are null, so all interfaces in the given {@code basePackage} are added as
 * mappers.
 * <p>
 * This configurer enables autowire for all the beans that it creates so that they are
 * automatically autowired with the proper {@code SqlSessionFactory} or {@code SqlSessionTemplate}.
 * If there is more than one {@code SqlSessionFactory} in the application, however, autowiring
 * cannot be used. In this case you must explicitly specify either an {@code SqlSessionFactory} or
 * an {@code SqlSessionTemplate} to use via the <em>bean name</em> properties. Bean names are used
 * rather than actual objects because Spring does not initialize property placeholders until after
 * this class is processed. 
 * <p>
 * Passing in an actual object which may require placeholders (i.e. DB user password) will fail. 
 * Using bean names defers actual object creation until later in the startup
 * process, after all placeholder substituation is completed. However, note that this configurer
 * does support property placeholders of its <em>own</em> properties. The <code>basePackage</code>
 * and bean name properties all support <code>${property}</code> style substitution.
 * <p>
 * Configuration sample:
 * <p>
 *
 * <pre class="code">
 * {@code
 *   <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
 *       <property name="basePackage" value="org.mybatis.spring.sample.mapper" />
 *       <!-- optional unless there are multiple session factories defined -->
 *       <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
 *   </bean>
 * }
 * </pre>
 *
 * @author Hunter Presnall
 * @author Eduardo Macarron
 *
 * @see MapperFactoryBean
 * @see ClassPathMapperScanner
 * @version $Id$
 */
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
  /**
   * {@inheritDoc}
   * 
   * @since 1.0.2
   */
  @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));
  }

}

因此在Spring中mybatis的配置如下源码,通过配置DataSource,SqlSessionFactory和MapperScannerConfigurer即可完成spring与mybatis的集成。

    <bean id="druidDataSource" destroy-method="close"
          class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driverClassName}" />
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />
        <!-- 最大连接数,缺省为8,建议值实例总连接数/服务数量 -->
        <property name="maxActive" value="${jdbc.pool.maxActive}" />
        <!-- 最小空闲连接,缺省为0,建议和initialSize一致 -->
        <property name="minIdle" value="${jdbc.pool.minIdle}" />
        <!-- 初始连接数,缺省为0,建议值实例总连接数/服务数量*0.2 -->
        <property name="initialSize" value="${jdbc.pool.initialSize}" />
        <!-- 异步Evict的定时检测连接是否可用,关闭无效链接 -->
        <property name="testWhileIdle" value="${jdbc.pool.testWhileIdle}" />
        <!-- Evict线程检测时间,单位毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="${jdbc.pool.timeBetweenEvictionRunsMillis}" />
        <!--最大等待时间,当没有可用连接时,连接池等待连接释放的最大时间,超过该时间限制会抛出异常,如果设置-1表示无限等待(默认为无限,调整为2000ms,避免因线程池不够用,而导致请求被无限制挂起)-->
        <property name="maxWait" value="${jdbc.pool.maxWait}"/>
        <!--是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个-->
        <property name="testOnBorrow" value="${jdbc.pool.testOnBorrow}" />
        <property name="validationQuery" value="select 0" />
        <property name="KeepAlive" value="true" />
    </bean>

    <!-- mybatis.配置SqlSessionFactory对象 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!-- 注入数据库连接池 -->
        <property name="dataSource" ref="druidDataSource" />
        <!-- 配置MyBaties全局配置文件:mybatis-config.xml -->
        <property name="configLocation" value="classpath:mybatis-config.xml" />
        <!-- 扫描entity包 使用别名 -->
        <!--		<property name="typeAliasesPackage" value="com.grassland.discover.entity" />-->
        <!-- 扫描sql配置文件:mapper需要的xml文件 -->
        <property name="mapperLocations" value="classpath:mybatis-mapper/*.xml" />
        <property name="plugins">
            <array>
                <bean class="com.github.pagehelper.PageInterceptor">
                    <property name="properties">
                        <value>
                            helperDialect=mysql
                            reasonable=true
                            supportMethodsArguments=true
                            autoRuntimeDialect=true
                        </value>
                    </property>
                </bean>
            </array>
        </property>
    </bean>

   <!-- mybatis.配置事务管理器 -->
    <bean id="transactionManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 注入数据库连接池 -->
        <property name="dataSource" ref="druidDataSource" />
    </bean>

    <!-- mybatis.配置扫描Dao接口包,动态实现Dao接口,注入到spring容器中 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!-- 注入sqlSessionFactory -->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
        <!-- 给出需要扫描Dao接口包 -->
        <property name="basePackage" value="com.xxx.dao" />
    </bean>

3.缓存Cache分析

接口类org.apache.ibatis.cache.Cache是Mybatis缓存的实现关键,缓存是基于SqlSession级别的,其中关键实现类是:org.apache.ibatis.cache.impl.PerpetualCache,如源码。该类是基于HashMap实现了sql查询的缓存。


package org.apache.ibatis.cache.impl;

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

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;

/**
 * @author Clinton Begin
 */
public class PerpetualCache implements Cache {

  private final String id;

  private final Map<Object, Object> cache = new HashMap<>();

  public PerpetualCache(String id) {
    this.id = id;
  }

  @Override
  public String getId() {
    return id;
  }

  @Override
  public int getSize() {
    return cache.size();
  }

  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

  @Override
  public Object getObject(Object key) {
    return cache.get(key);
  }

  @Override
  public Object removeObject(Object key) {
    return cache.remove(key);
  }

  @Override
  public void clear() {
    cache.clear();
  }

  @Override
  public boolean equals(Object o) {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    if (this == o) {
      return true;
    }
    if (!(o instanceof Cache)) {
      return false;
    }

    Cache otherCache = (Cache) o;
    return getId().equals(otherCache.getId());
  }

  @Override
  public int hashCode() {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    return getId().hashCode();
  }

}

关于缓存,mybatis实现了多种缓存策略,均在org.apache.ibatis.cache.decorators包中,该包中的类是采用装饰者模式,将缓存的操作委托到更底层的实现,如上面的PerpetualCache类。常见的有同步缓存SynchronizedCache对缓存的每一个方法都加上了synchronized方法;有LRU缓存LruCache类实现了基于LRU算法操作的缓存,该缓存中主要是基于LinkedHashMap类,重写了removeEldestEntry方法,改写了移除过期的key的策略方式;还有基于队列的FIFO的缓存策略FifoCache,基于CountDownLatch的BlockingCache类实现了阻塞的缓存策略,基于弱引用ReferenceQueue实现的WeakCache类等,非常丰富。

这里我们就拿一个源码做展示,即LruCache类的源码如下,这里两个点值得关注: 1)setSize方法初始化的时候,将keyMap设置为LinkedHashMap并重写了removeEldestEntry方法,实现LRU算法; 2)在putObject方法(底层LinkedHashMap会调用removeEldestEntry方法,设置eldestKey属性)之后调用cycleKeyList方法,该方法中会进行根据putObject执行的结果,进行决定是否需要从缓存中移除对应的key,如果需要就从缓存中通过委托对象delegate进行remove操作。

public class LruCache implements Cache {

  private final Cache delegate;
  private Map<Object, Object> keyMap;
  private Object eldestKey;

  public LruCache(Cache delegate) {
    this.delegate = delegate;
    setSize(1024);
  }

  @Override
  public String getId() {
    return delegate.getId();
  }

  @Override
  public int getSize() {
    return delegate.getSize();
  }

  public void setSize(final int size) {
    keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
      private static final long serialVersionUID = 4267176411845948333L;

      @Override
      protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
        boolean tooBig = size() > size;
        if (tooBig) {
          eldestKey = eldest.getKey();
        }
        return tooBig;
      }
    };
  }

  @Override
  public void putObject(Object key, Object value) {
    delegate.putObject(key, value);
    cycleKeyList(key);
  }

  @Override
  public Object getObject(Object key) {
    keyMap.get(key); // touch
    return delegate.getObject(key);
  }

  @Override
  public Object removeObject(Object key) {
    return delegate.removeObject(key);
  }

  @Override
  public void clear() {
    delegate.clear();
    keyMap.clear();
  }

  private void cycleKeyList(Object key) {
    keyMap.put(key, key);
    if (eldestKey != null) {
      delegate.removeObject(eldestKey);
      eldestKey = null;
    }
  }

}

4.SqlSession分析

SqlSession的关键操作类位于org.apache.ibatis.session包中,通过SqlSessionFactory打开数据库SQL操作的相关会话,关键点位于SqlSessionManager类中,该类采用了代理proxy模式,采用了SqlSessionInterceptor类拦截了sql的所有操作,这里有两个非常巧妙的地方:1) 采用ThreadLocal<SqlSession> localSqlSession变量,采用ThreadLocal的形式,保证在同一个线程中采用同一个SqlSession对象进行数据库的操作; 2)在org.apache.ibatis.session.SqlSessionManager.SqlSessionInterceptor#invoke方法中,即拦截所有的数据库操作,如果没有session则进行开启会话,根据需要创建事务的开始,如果有session直接从ThreadLocal中获取对应的SqlSession对象进行处理,处理完成进行事务的commit操作。具体的关于SqlSessionManager的关键源码如下,非关键代码没有展示出来。

public class SqlSessionManager implements SqlSessionFactory, SqlSession {

  private final SqlSessionFactory sqlSessionFactory;
  private final SqlSession sqlSessionProxy;

  private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<>();

  private SqlSessionManager(SqlSessionFactory sqlSessionFactory) {
    this.sqlSessionFactory = sqlSessionFactory;
    this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[]{SqlSession.class},
        new SqlSessionInterceptor());
  }
 @Override
  public void rollback(boolean force) {
    final SqlSession sqlSession = localSqlSession.get();
    if (sqlSession == null) {
      throw new SqlSessionException("Error:  Cannot rollback.  No managed session is started.");
    }
    sqlSession.rollback(force);
  }

  @Override
  public List<BatchResult> flushStatements() {
    final SqlSession sqlSession = localSqlSession.get();
    if (sqlSession == null) {
      throw new SqlSessionException("Error:  Cannot rollback.  No managed session is started.");
    }
    return sqlSession.flushStatements();
  }

  @Override
  public void close() {
    final SqlSession sqlSession = localSqlSession.get();
    if (sqlSession == null) {
      throw new SqlSessionException("Error:  Cannot close.  No managed session is started.");
    }
    try {
      sqlSession.close();
    } finally {
      localSqlSession.set(null);
    }
  }

  private class SqlSessionInterceptor implements InvocationHandler {
    public SqlSessionInterceptor() {
        // Prevent Synthetic Access
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      final SqlSession sqlSession = SqlSessionManager.this.localSqlSession.get();
      if (sqlSession != null) {
//说明ThreadLocal中已经有了,直接执行
        try {
          return method.invoke(sqlSession, args);
        } catch (Throwable t) {
          throw ExceptionUtil.unwrapThrowable(t);
        }
      } else {
//说明ThreadLocal中没有,则开启新的Session,并执行相关的SQL语句
        try (SqlSession autoSqlSession = openSession()) {
          try {
            final Object result = method.invoke(autoSqlSession, args);
            autoSqlSession.commit();
            return result;
          } catch (Throwable t) {
            autoSqlSession.rollback();
            throw ExceptionUtil.unwrapThrowable(t);
          }
        }
      }
    }
  }

}

在mybatis-spring的包中,org.mybatis.spring.SqlSessionTemplate对象做了mybatis包中的SqlSessionManager类中类似的方法。

4.Interceptor分析

接口类org.apache.ibatis.plugin.Interceptor可以通过实现该接口可以为sql定制相关的切面操作。可以通过在mybatis的xml配置文件中增加对应plugins的interceptor配置,实现增加拦截器处理。这里拦截处理主要是通过org.apache.ibatis.plugin.InterceptorChain#pluginAll建立一个责任链模式,其中在方法中依赖的plugin方法中依赖Plugin.wrap方法,该方法实际上是封装了一层代理,实现通过层层调用拦截相关操作。关于InterceptorChain参考如下源码:

//拦截责任链
public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<>();

  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }

  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }

}

//拦截接口
public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

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

  default void setProperties(Properties properties) {
    // NOP
  }

}


//插件类
public class Plugin implements InvocationHandler {

  private final Object target;
  private final Interceptor interceptor;
  private final Map<Class<?>, Set<Method>> signatureMap;

  private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
    this.target = target;
    this.interceptor = interceptor;
    this.signatureMap = signatureMap;
  }

  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) {
//注意这里是封装生成了代理类,返回Plugin对象
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

 @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
//注意这里才是真正的拦截之后的处理,按需调用拦截器的intercept方法
      if (methods != null && methods.contains(method)) {
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }
}

5.总结

mybatis基于JDBC实现的ORM框架,为研发提供了不少的便捷性,熟悉mybatis的能力和局限性,明确其边界和定位,方便后续研发。积跬步,致千里。

6.参考文章

1.https://blog.csdn.net/top_code/article/details/55657776

2.https://www.jianshu.com/p/48dfeb0b79b6

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值