从源码角度分析MyBatis中的设计模式
MyBatis 的前身是 IBatis,IBatis 是由 Internet 和 Ibatis 组合而成,其目的是想当做互联网的篱笆墙,围绕着数据库提供持久化服务的一个框架,2010 年正式改名为 MyBatis。它是一款优秀的持久层框架,支持自定义 SQL、存储过程及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作,还可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Ordinary Java Object,普通 Java 对象)为数据库中的记录。
MyBatis中应用了很多设计模式,下面分别对几种常用的设计模式进行说明。
工厂模式
MyBatis中最典型的工厂模式是SqlSessionFactory。每一个MyBatis的应用程序都以一个SqlSessionFactory对象的实例为核心.同时SqlSessionFactory也是线程安全的,SqlSessionFactory一旦被创建,应该在应用执行期间都存在.在应用运行期间不要重复创建多次。尤其是在一些根据配置文件初始化对象的情况下,使用单例即可。当遇到new关键字时,我们也要多个心眼,是否可以使用工厂模式。
SqlSession 是 MyBatis 中的重要 Java 接口,可以通过该接口来执行 SQL 命令、获取映射器示例和管理事务,而 SqlSessionFactory 正是用来产生 SqlSession 对象的,所以它在 MyBatis 中是比较核心的接口之一。
工厂模式应用解析:SqlSessionFactory 是一个接口类,它的子类 DefaultSqlSessionFactory有一个 openSession(ExecutorType execType) 的方法,其中使用了工厂模式,源码如下:
private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
try {
boolean autoCommit;
try {
autoCommit = connection.getAutoCommit();
} catch (SQLException e) {
// Failover to true, as most poor drivers
// or databases won't support transactions
autoCommit = true;
}
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
final Transaction tx = transactionFactory.newTransaction(connection);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
方法transactionFactory.newTransaction(connection)创建事务到底是做什么操作呢?我们看下Jdbc的事务方法,如下。
//JdbcTransactionFactory
@Override
public Transaction newTransaction(Connection conn) {
return new JdbcTransaction(conn);
}
//JdbcTransaction
public JdbcTransaction(Connection connection) {
this.connection = connection;
}
从上面代码可以看出,就是利用当前的Connection,设置到JdbcTransaction对象中。那一个Connection包括什么呢?如下图所示。包括语句等。
其中会创建一个Socket链接,比如如果需要取消当前语句的执行,就会新建立一个Socket链接去连数据库,然后在数据库中执行取消。
@Override
public void cancel() throws SQLException {
try {
debugCodeCall("cancel");
checkClosed();
// executingCommand can be reset by another thread
CommandInterface c = executingCommand;
try {
if (c != null) {
c.cancel();
cancelled = true;
}
} finally {
setExecutingStatement(null);
}
} catch (Exception e) {
throw logAndConvert(e);
}
}
//CommandRemote
/**
* Cancel this current statement.
*/
@Override
public void cancel() {
session.cancelStatement(id);
}
//SessionRemote
/**
* Cancel the statement with the given id.
*
* @param id the statement id
*/
public void cancelStatement(int id) {
for (Transfer transfer : transferList) {
try {
Transfer trans = transfer.openNewConnection();
trans.init();
trans.writeInt(clientVersion);
trans.writeInt(clientVersion);
trans.writeString(null);
trans.writeString(null);
trans.writeString(sessionId);
trans.writeInt(SessionRemote.SESSION_CANCEL_STATEMENT);
trans.writeInt(id);
trans.close();
} catch (IOException e) {
trace.debug(e, "could not cancel statement");
}
}
}
//Transfer
/**
* 用相同的地址和端口打开一个新的链接
*
* @return the new transfer object
*/
public Transfer openNewConnection() throws IOException {
InetAddress address = socket.getInetAddress();
int port = socket.getPort();
Socket s2 = NetUtils.createSocket(address, port, ssl);
Transfer trans = new Transfer(null, s2);
trans.setSSL(ssl);
return trans;
}
//Transfer
/** 初始化传输对象,这个方法将会尝试打开一个输入或输出流
*/
public synchronized void init() throws IOException {
if (socket != null) {
in = new DataInputStream(
new BufferedInputStream(
socket.getInputStream(), Transfer.BUFFER_SIZE));
out = new DataOutputStream(
new BufferedOutputStream(
socket.getOutputStream(), Transfer.BUFFER_SIZE));
}
}
从该方法我们可以看出它会 configuration.newExecutor(tx, execType) 读取对应的环境配置,而此方法的源码如下:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
可以看出 newExecutor() 方法为标准的工厂模式,它会根据传递 ExecutorType 值生成相应的对象然后进行返回。
建造者模式(Builder)
建造者模式指的是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。也就是说建造者模式是通过多个模块一步步实现了对象的构建,相同的构建过程可以创建不同的产品。
例如,组装电脑,最终的产品就是一台主机,然而不同的人对它的要求是不同的,比如设计人员需要显卡配置高的;而影片爱好者则需要硬盘足够大的(能把视频都保存起来),但对于显卡却没有太大的要求,我们的装机人员根据每个人不同的要求,组装相应电脑的过程就是建造者模式。
建造者模式在 MyBatis 中的典型代表是 SqlSessionFactoryBuilder。
普通的对象都是通过 new 关键字直接创建的,但是如果创建对象需要的构造参数很多,且不能保证每个参数都是正确的或者不能一次性得到构建所需的所有参数,那么就需要将构建逻辑从对象本身抽离出来,让对象只关注功能,把构建交给构建类,这样可以简化对象的构建,也可以达到分步构建对象的目的,而 SqlSessionFactoryBuilder 的构建过程正是如此。
在 SqlSessionFactoryBuilder 中构建 SqlSessionFactory 对象的过程是这样的,首先需要通过 XMLConfigBuilder 对象读取并解析 XML 的配置文件,然后再将读取到的配置信息存入到 Configuration 类中,然后再通过 build 方法生成我们需要的 DefaultSqlSessionFactory 对象,实现源码如下(在 SqlSessionFactoryBuilder 类中):
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.
}
}
}
这里需要注意,在finally中的异常,不需要再抛出去,否则会“压制”真实的异常。
SqlSessionFactoryBuilder 类相当于一个建造工厂,先读取文件或者配置信息、再解析配置、然后通过反射生成对象,最后再把结果存入缓存,这样就一步步构建造出一个 SqlSessionFactory 对象。
单例模式
单例模式也比较好理解,比如一个人一生当中只能有一个真实的身份证号,每个收费站的窗口都只能一辆车子一辆车子的经过,类似的场景都是属于单例模式。
单例模式在 MyBatis 中的典型代表是 ErrorContext。
ErrorContext 是线程级别的的单例,所以不需要加synchronzed关键字,每个线程中有一个此对象的单例,用于记录该线程的执行环境的错误信息。
public static ErrorContext instance() {
ErrorContext context = LOCAL.get();
if (context == null) {
context = new ErrorContext();
LOCAL.set(context);
}
return context;
}
可以看出 ErrorContext 使用 private 修饰的 ThreadLocal 来保证每个线程拥有一个 ErrorContext 对象,在调用 instance() 方法时再从 ThreadLocal 中获取此单例对象。
适配器模式
适配器模式是指将一个不兼容的接口转换成另一个可以兼容的接口,这样就可以使那些不兼容的类可以一起工作。
例如,最早之前我们用的耳机都是圆形的,而现在大多数的耳机和电源都统一成了方形的 typec 接口,那之前的圆形耳机就不能使用了,只能买一个适配器把圆形接口转化成方形的。适配器模式在 MyBatis 中的典型代表是 Log。
MyBatis 中的日志模块适配了以下多种日志类型:
- SLF4J
- Apache Commons Logging
- Log4j 2
- Log4j
- JDK logging
首先 MyBatis 定义了一个 Log 的接口,用于统一和规范接口的行为,源码如下:
public interface Log {
boolean isDebugEnabled();
boolean isTraceEnabled();
void error(String s, Throwable e);
void error(String s);
void debug(String s);
void trace(String s);
void warn(String s);
}
public class Log4jImpl implements Log {
private static final String FQCN = Log4jImpl.class.getName();
private final Logger log;
public Log4jImpl(String clazz) {
log = Logger.getLogger(clazz);
}
@Override
public boolean isDebugEnabled() {
return log.isDebugEnabled();
}
@Override
public boolean isTraceEnabled() {
return log.isTraceEnabled();
}
@Override
public void error(String s, Throwable e) {
log.log(FQCN, Level.ERROR, s, e);
}
@Override
public void error(String s) {
log.log(FQCN, Level.ERROR, s, null);
}
@Override
public void debug(String s) {
log.log(FQCN, Level.DEBUG, s, null);
}
@Override
public void trace(String s) {
log.log(FQCN, Level.TRACE, s, null);
}
@Override
public void warn(String s) {
log.log(FQCN, Level.WARN, s, null);
}
}
代理模式
代理模式应该是mybatis中最核心的设计模式了。
代理模式在 MyBatis 中的典型代表是 MapperProxyFactory。mybatis中的代理是通过jdk的代理来实现的,也就是说通过获取方法区的被代理类的字节码,在堆上创建构造函数,通过调用构造函数进行实例化对象。
MapperProxyFactory 的 newInstance() 方法就是生成一个具体的代理来实现功能的,源码如下:
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();
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) {
// 这里是通过jdk的代理方式
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
模板方法模式
模板方法模式是最常用的设计模式之一,它是指定义一个操作算法的骨架,而将一些步骤的实现延迟到子类中去实现,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。此模式是基于继承的思想实现代码复用的。
模板方法在 MyBatis 中的典型代表是 BaseExecutor。
在 MyBatis 中 BaseExecutor 实现了大部分 SQL 执行的逻辑,然后再把几个方法交给子类来实现,它的继承关系如下图所示:
比如 doUpdate() 就是交给子类自己去实现的,它在 BaseExecutor 中的定义如下:
在不同的子类中的实现方式不一样。比如,在SimpleExecutor中的实现如下。
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.update(stmt);
} finally {
closeStatement(stmt);
}
}
可以看出 SimpleExecutor 每次使用完 Statement 对象之后,都会把它关闭掉。
但是在ReuseExecutor中,每次使用完 Statement 对象之后不会把它关闭掉。
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
Statement stmt = prepareStatement(handler, ms.getStatementLog());
return handler.update(stmt);
}
装饰器模式
装饰器模式允许向一个现有的对象添加新的功能,同时又不改变其结构,这种类型的设计模式属于结构型模式,它是作为现有类的一个包装。
装饰器模式在 MyBatis 中的典型代表是 Cache。
Cache 除了有数据存储和缓存的基本功能外(由 PerpetualCache 永久缓存实现),还有其他附加的 Cache 类,比如先进先出的 FifoCache、最近最少使用的 LruCache、防止多线程并发访问的 SynchronizedCache 等众多附加功能的缓存类,Cache 所有实现子类如下图所示:
Mybatis缓存
Mybatis 中有一级缓存和二级缓存,默认情况下一级缓存是开启的,而且是不能关闭的。一级缓存是指SqlSession 级别的缓存,当在同一个SqlSession 中进行相同的SQL 语句查询时,第二次以后的查询不会从数据库查询,而是直接从缓存中获取,一级缓存最多缓存1024 条SQL。二级缓存是指可以跨SqlSession 的缓存。是mapper 级别的缓存,对于mapper 级别的缓存不同的sqlsession 是可以共享的。
Mybatis的一级缓存原理(sqlsession级别)
第一次发出一个查询sql,sql 查询结果写入sqlsession 的一级缓存中,缓存使用的数据结构是一个map。其中key为:MapperID+offset+limit+Sql+所有的入参。同一个sqlsession 再次发出相同的sql,就从缓存中取出数据。如果两次中间出现commit 操作(修改、添加、删除),本sqlsession 中的一级缓存区域全部清空,下次再去缓存中查询不到所以要从数据库查询,从数据库查询到再写入缓存。
二级缓存原理(mapper基本)
二级缓存的范围是mapper 级别(mapper 同一个命名空间),mapper 以命名空间为单位创建缓存数据结构,结构是map。mybatis 的二级缓存是通过CacheExecutor 实现的。CacheExecutor其实是Executor 的代理对象。所有的查询操作,在CacheExecutor 中都会先匹配缓存中是否存在,不存在则查询数据库。key:MapperID+offset+limit+Sql+所有的入参。
具体使用需要配置
- Mybatis 全局配置中启用二级缓存配置
- 在对应的Mapper.xml 中配置cache 节点
- 在对应的select 查询节点中添加useCache=true