概述
本章再对 Mybatis 用到的设计模式做一个总结。它用到的设计模式也不少。有些前面章节已经经过了,有些则比较简单。
SqlSessionFactoryBuilder:为什么要用建造者模式来创建 SqlSessionFactory?
在《Mybatis如何权衡易用性、性能和灵活性?》章节,通过一个查询用户的例子,展示了用 Mybatis 进行数据库编程。为方便查看,代码重新摘抄到这里。
java
public class MybatisDemo {
public static void main(String[] args) throws IOException {
Reader reader = Resources.getResourceAsReader("mybatis.xml");
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession session = sessionFactory.openSession();
UserMapper userMapper = session.getMapper(UserMapper.class);
UserDo userDo = userMapper.selectById(8);
// ...
}
}
针对这段代码,请你思考下下面这个问题。
之前讲到建造者模式时,我们使用 Builder 类来创建对象,一般都是先级联一组 setXXX()
方法来设置属性,然后再调用 builder()
方法创建最终的对象。但是,在上面这段代码中,通过 SqlSessionFactoryBuilder
来创建 SqlSessionFactory
并不符合这个套路。它既没有 setter 方法,而且 builder()
方法也并非无参,需要传递参数。此外,从上面的代码来看,SqlSessionFactory
对象的创建过程也并不复杂。那直接通过构造函数来创建 SqlSessionFactory
不就行了吗?为什么还要借助建造者模式创建 SqlSessionFactory
呢?
要回答这个问题,先要看下 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);
}
}
SqlSessionFactoryBuilder
类中有大量的 build()
重载函数。为了方便你查看,以及待会儿跟 SqlSessionFactory
类的代码做对比,我们把重载函数抽象出来,贴到这里。
java
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(Reader reader);
public SqlSessionFactory build(Reader reader, String environment);
public SqlSessionFactory build(Reader reader, Properties properties);
public SqlSessionFactory build(Reader reader, String environment, Properties properties);
public SqlSessionFactory build(InputStream inputStream);
public SqlSessionFactory build(InputStream inputStream, String environment);
public SqlSessionFactory build(InputStream inputStream, Properties properties);
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) ;
// 上面的所有方法,最终都会调用这个方法
public SqlSessionFactory build(Configuration config);
}
我们知道,如果一个类包含很多成员变量,而构建对象并不需要设置所有的成员变量,只需要选择性地设置其中几个就可以了。为了满足这样的构建需求,就要定义多个包含不同参数列表的构造函数。为了避免构造函数过多、参数列表过长,我们一般通过无参构造函数加 setter 方法或者通过建造者模式来解决。
从建造者模式的设计初衷上来看,SqlSessionFactoryBuilder
虽然带有 Builder()
后缀,但不要被它的名字所迷惑,它并不是标准的建造者模式。一方面,原始类 SqlSessionFactory
只需要一个参数,并不复杂。另一方面,Builder 类 SqlSessionFactoryBuilder
仍然定义了 n 多个包含不同参数列表的构造函数。
实际上,SqlSessionFactoryBuilder
设计的初衷只不过是为了简化开发。因为构建 SqlSessionFactory
需要先构建 Configuration
,而构建 Configuration
是非常复杂的,需要做很多工作,比如配置的读取、解析、创建 n 多对象等。为了将构建 SqlSessionFactory
的过程隐藏起来,对程序员透明,Mybatis 就设计了 SqlSessionFactoryBuilder
类封装这些构建细节。
SqlSessionFactory:到底属于工厂模式还是建造者模式?
在上面那段 Mybatis 示例代码中,我们通过 SqlSessionFactoryBuilder
创建了 SqlSessionFactory
,然后再通过 SqlSessionFactory
创建了 SqlSession
。刚刚讲了 SqlSessionFactoryBuilder
,现在再来看下 SqlSessionFactory
。
从名字上,你可能已经猜到,SqlSessionFactory
是一个工厂类,用到的设计模式是工厂模式。不过,它跟 SqlSessionFactoryBuilder
类似,名字有很大的迷惑性。实际上,它并不是标准的工厂模式。为什么这么说呢?我们先来看下 SqlSessionFactory
类的源码。
java
public interface SqlSessionFactory {
SqlSession openSession();
SqlSession openSession(boolean autoCommit);
SqlSession openSession(Connection connection);
SqlSession openSession(TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType);
SqlSession openSession(ExecutorType execType, boolean autoCommit);
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType, Connection connection);
Configuration getConfiguration();
}
SqlSessionFactory
是一个接口,DefaultSqlSessionFactory
是它的唯一实现类。DefaultSqlSessionFactory
源码如下所示:
java
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
@Override
public SqlSession openSession(boolean autoCommit) {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, autoCommit);
}
@Override
public SqlSession openSession(ExecutorType execType) {
return openSessionFromDataSource(execType, null, false);
}
@Override
public SqlSession openSession(TransactionIsolationLevel level) {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), level, false);
}
@Override
public SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level) {
return openSessionFromDataSource(execType, level, false);
}
@Override
public SqlSession openSession(ExecutorType execType, boolean autoCommit) {
return openSessionFromDataSource(execType, null, autoCommit);
}
@Override
public SqlSession openSession(Connection connection) {
return openSessionFromConnection(configuration.getDefaultExecutorType(), connection);
}
@Override
public SqlSession openSession(ExecutorType execType, Connection connection) {
return openSessionFromConnection(execType, connection);
}
@Override
public Configuration getConfiguration() {
return configuration;
}
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();
}
}
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();
}
}
// ...
}
从 SqlSessionFactory
、DefaultSqlSessionFactory
的源码来看,它的设计非常类似刚刚的 SqlSessionFactoryBuilder
,通过重载多个 openSession()
函数,支持通过组合 autoCommit
、Executor
、Transaction
等不同的参数,创建 SqlSession
对象。标准的工厂模式通过 type 来创建继承同一个父类的不同子类对象,而这里只不过是通过传递进来的不同参数,来创建同一个类的对象。所以,它更像建造者模式。
虽然设计思路基本一致,但一个叫 xxxBuilder
(SqlSessionFactoryBuilder
),一个叫 xxxFactory
(SqlSessionFactory
)。而且,叫 xxxBuilder
的也并非标准的建造者模式,叫 xxxFactory
的也并非标准的工厂模式。所以,我个人觉得,Mybatis 对这部分代码的设计还是值得优化的。
实际上,这两个类的作用只不过是为了创建 SqlSession
对象,没有其他作用。所以,我更建议参照 Spring 的设计思路,把 SqlSessionFactoryBuilder
和 SqlSessionFactory
的逻辑,放到一个叫 "ApplicationContext
" 的类中。让这个类来全权负责读入配置文件,创建 Configuration
,生成 SqlSession
。
BaseExecutor:模板模式跟普通的继承有什么区别?
如果查阅 SqlSession
和 DefaultSqlSession
的源码,你会发现,SqlSession
执行 SQL 的业务逻辑,都是委托给了 Executor
来实现。Executor
相关的类主要是用来执行 SQL。其中,Executor
本身是一个接口;BaseExecutor
是一个抽象类,实现了 Executor
接口;而 BatchExecutor
、SimpleExecutor
、ReuseExecutor
三个类继承 BaseExecutor
抽象类。
那 BatchExecutor
、SimpleExecutor
、ReuseExecutor
三个类跟 BaseExecutor
是简单的继承关系,还是模板模式关系呢?我们看一下 BaseExecutor
的源码就清楚了。
java
public abstract class BaseExecutor implements Executor {
// ...
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
@Override
public List<BatchResult> flushStatements() throws SQLException {
return flushStatements(false);
}
public List<BatchResult> flushStatements(boolean isRollBack) throws SQLException {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
return doFlushStatements(isRollBack);
}
// ...
@Override
public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
return doQueryCursor(ms, parameter, rowBounds, boundSql);
}
// ...
protected abstract int doUpdate(MappedStatement ms, Object parameter)
throws SQLException;
protected abstract List<BatchResult> doFlushStatements(boolean isRollback)
throws SQLException;
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
throws SQLException;
protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)
throws SQLException;
// ...
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
// ...
}
模板模式基于继承来实现代码复用。如果抽象类中包含模板方法,模板方法调用待子类实现的抽象方法,那这一般就是模板模式的代码实现 。而且,在命名上,模板方法与抽象方法一般是一一对应地,抽象方法在模板方法前面多一个 "do",比如,在 BaseExecutor
类中,其中一个模板方法叫做 update()
,那对应地抽象方法叫做 doUpdate()
。
SqlNode:如何利用解释器模式来解析动态 SQL?
支持配置文件中编写动态 SQL,是 Mybatis 一个非常强大的功能。所谓动态 SQL,就是在 SQL 中可以包含在 trim、if、#{} 等语法标签,在运行时根虎条件来生成不同的 SQL。这么说比较抽象,我举个例子解释下。
xml
<update id="update" parameterType="com.example.User">
UPDATE user
<trim prefix="SET" prefixOverrides=",">
<if test="name != null and name != ''">
name = #{name}
</if>
<if test="age != null and age != ''">
, age = #{age}
</if>
<if test="birthday != null and birthday != ''">
, birthday = #{birthday}
</if>
</trim>
where id = #{id}
</update>
显然,动态 SQL 的语法规则是 Mybtis 自定义的。如果想要根据语法规则,替换掉动态 SQL 中的动态元素,生成真正可以执行的 SQL 语句,Mybatis 还需要实现对应的解释器。这一部分功能就可以看作是解释器模式的应用。实际上,如果你去查看它的代码实现,你会发现,它跟我们在前面讲解解释器模式时举的例子的代码结构非常相似。
前面提到,解释器模式在解释语法规则时,一般会把语法规则分割成小的单元,特别是可以嵌套的小单元,针对每个小单元来解析,最终再把解析结果合并在一起。这里也不例外。Mybatis 把每个语法小单元叫 SqlNode
。SqlNode
的定义如下所示:
java
public interface SqlNode {
boolean apply(DynamicContext context);
}
对应不同的语法小单元,Mybatis 定义的不同 SqlNode
实现类。
整个解释器的调用入口在 DynamicSqlSource.getBoundSql()
方法中,它调用了 rootSqlNode.apply(context)
方法。
ErrorContext:如何实现一个线程唯一的单例模式?
在单例模式章节,我们讲到单例模式时进程唯一的。同时,还讲到单例模式的几种变形,比如线程唯一的单例、集群唯一的单例等等。在 Mybatis 中,ErrorContext
这个类就是标准的单例的变形:现成唯一实例。
它的代码实现如下所示。它基于 Java 的 ThreadLocal
类实现。
java
public class ErrorContext {
private static final String LINE_SEPARATOR = System.getProperty("line.separator","\n");
private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<>();
private ErrorContext stored;
private String resource;
private String activity;
private String object;
private String message;
private String sql;
private Throwable cause;
private ErrorContext() {
}
public static ErrorContext instance() {
ErrorContext context = LOCAL.get();
if (context == null) {
context = new ErrorContext();
LOCAL.set(context);
}
return context;
}
// ...
}
Cache:为什么要用装饰器模式而不设计成继承子类?
前面提到,Mybatis 是一个 ORM 框架。实际上,它不只是简单地完成了对象和数据库之间的互相转化,还提供了很多其他功能,比如缓存、事务等。接下来,再讲讲它的缓存实现。
在 Mybatis 中,缓存功能由接口 Cache
定义。PrepetualCache
类是最基础的缓存类,是一个大小无限的缓存。此外,Mybatis 还设计了 9 个包裹 PrepetualCache
的类装饰器,用来实现功能增强。它们分别是:FifoCache
、LoggingCache
、LruCache
、ScheduleCache
、SerializedCache
、SoftCache
、SynchronizedCache
、WeakCache
、TransactionCache
。
java
public interface Cache {
String getId();
void putObject(Object key, Object value);
Object getObject(Object key);
Object removeObject(Object key);
void clear();
int getSize();
ReadWriteLock getReadWriteLock();
}
public class PerpetualCache implements Cache {
private final String id;
private 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 ReadWriteLock getReadWriteLock() {
return null;
}
}
另外 9 个装饰器的代码结构都类似,我们只浆砌砖的 LruCache
的源码贴到这里。从代码中可以看出,它是标准的装饰器模式的代码实现。
java
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();
}
@Override
public ReadWriteLock getReadWriteLock() {
return null;
}
private void cycleKeyList(Object key) {
keyMap.put(key, key);
if (eldestKey != null) {
delegate.removeObject(eldestKey);
eldestKey = null;
}
}
}
之所以 Mybatis 采用装饰器模式来实现缓存功能,是因为装饰器模式采用了组合,而非继承,更加灵活,能够有效地避免继承关系的组合爆炸。
PropertyTokenizer:如何利用迭代器模式实现一个属性解析器?
前面章节讲过,迭代器模式常用来替代 for 循环遍历集合。Mybatis 的 PropertyTokenizer
类实现了 Java Iterator
接口,是一个迭代器,用来对配置属性进行解析。具体代码如下所示:
java
public class PropertyTokenizer implements Iterator<PropertyTokenizer> {
private String name;
private final String indexedName;
private String index;
private final String children;
public PropertyTokenizer(String fullname) {
int delim = fullname.indexOf('.');
if (delim > -1) {
name = fullname.substring(0, delim);
children = fullname.substring(delim + 1);
} else {
name = fullname;
children = null;
}
indexedName = name;
delim = name.indexOf('[');
if (delim > -1) {
index = name.substring(delim + 1, name.length() - 1);
name = name.substring(0, delim);
}
}
public String getName() {
return name;
}
public String getIndex() {
return index;
}
public String getIndexedName() {
return indexedName;
}
public String getChildren() {
return children;
}
@Override
public boolean hasNext() {
return children != null;
}
@Override
public PropertyTokenizer next() {
return new PropertyTokenizer(children);
}
@Override
public void remove() {
throw new UnsupportedOperationException("Remove is not supported, as it has no meaning in the context of properties.");
}
}
实际上, PropertyTokenizer
也并非标准的迭代器类。它将配置的解析、解析之后的元素、迭代器,这三部分代码都耦合在一个类中,所以看起来稍微有点难懂。不过这样做的好处是能够做到惰性解析。我们不需要事先将整个配置,解析成多个 PropertyTokenizer
对象。只有当我们在调用 next()
函数时,才会解析其中的部分配置。
Log:如何使用适配器模式来适配不同的日志框架
在适配器模式章节我们讲过,Sl4j 为了统一各个不同的日志框架(Log4j、JCL、Logback 等),提供了一套统一的日志接口。不过,Mybatis 并没有直接使用 Sl4j 提供的统一日志规范,而是自己又重复造轮子,定义了一套自己的日志访问接口。
java
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);
}
针对 Log 接口,Mybatis 还提供了各种不同的实现类,分别使用不同的日志框架来实现 Log 接口。
这几个类的代码结构基本一致。我们把其中的 Log4jImpl
的源码贴到下方。在适配器模式中,传递给适配器构造函数的是被适配的类对象,而这里是 clazz
(相当于日志名称 name),所以,从代码实现上来讲,它并非标准的适配器模式。但是,从应用场景上看,这里确实又起到了适配的作用,是典型的适配器模式的应用场景。
java
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 中用到的 8 种设计模式,它们分别是:建造者模式、工程模式、模板模式、解释权模式、单例模式、装饰器模式、适配器模式。再加上上篇文章的职责链模式和动态代理,总共讲了 10 种设计模式。
从两篇文章的讲解中,不知道你发现没有,Mybatis 对很多设计模式的实现,都并非标准的代码实现,都做了比较多的自我改进。实际上,这就是所谓的灵活应用,只借鉴不照搬,根据具体问题针对性地去解决。