目录
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的能力和局限性,明确其边界和定位,方便后续研发。积跬步,致千里。