MyBatis 中的设计模式,这次我总结全了

系列文章目录

这是 MyBatis 源码之旅的第六篇文章,MyBatis 版本号为 3.5.6,源码分析注释已上传到 Github ,前五篇的文章目录如下,建议按照顺序阅读。
  1. MyBatis 初探,使用 MyBatis 简化数据库操作(超详细)
  2. MyBatis Mapper 接口方法执行原理分析
  3. 一条 SQL 是如何在 MyBatis 中执行的
  4. 谈谈 MyBatis 的插件,除了分页你可能还有这些使用场景
  5. MyBatis 缓存机制分析,MyBatis 真的有二级缓存?

前言

软件开发的流程一般可分为分析、设计、实现,设计模式在处于设计或代码实现阶段,以设计思想、设计原则作为指导,相对来说更为具象,是前人对经常遇到的设计问题总结出的一套解决方案,多数设计模式用来解决代码的扩展性问题,在框架中使用的场景较多。

MyBatis 作为一个小巧的持久层框架,在其中也使用了几个设计模式,这里把我能识别出的设计模式做一个总结。很多有关设计模式的书为了便于读者理解设计模式经常会举出一些简单的案例,这样看来好像懂了,但实战时又好像什么都不会,通过 MyBatis 这些设计模式的总结,希望达到深入理解设计模式的目的,知道为什么使用这些设计模式?解决了什么问题?在什么场景下使用?这样读者在遇到相同问题的时候自然就知道选择什么样的设计模式。


单例模式

单例模式是表示一个类只有一个实例,按照单例的范围可以分为线程内单例、进程内单例、集群内单例。更多单例模式的信息可参见《Java 中创建单例的几种方式》

ErrorContext

MyBatis 中使用的单例模式只有一个 ErrorContext,这是一个线程级别的单例模式,MyBatis 同样使用了 ThreadLocal 来实现,MyBatis 使用它作为解析 xml 配置或执行 SQL 时的线程上下文信息,如当前正在解析的资源文件、当前执行的 SQL 等等,具体代码如下。

public class ErrorContext {

    private static final ThreadLocal<ErrorContext> LOCAL = ThreadLocal.withInitial(ErrorContext::new);
    
	... 省略部分字段
	
    private ErrorContext() {
    }

    public static ErrorContext instance() {
        return LOCAL.get();
    }

	... 省略部分实例方法
}

工厂模式

工厂模式用以创建多个类型相似的不同对象(同一个类的多个子类),又可以细分为简单工厂、工厂方法、抽象工厂。具体可参见《设计模式之工厂模式》。这个设计模式在 MyBatis 中使用较多。

SqlSessionFactory

SqlSession 表示 MyBatis 与数据库的一次会话,MyBatis 中默认的 SqlSession 是 DefaultSqlSession,MyBatis 使用 SqlSessionFactory 作为工厂创建 SqlSession,具体的设计模式为工厂方法模式,SqlSessionFactory 仅在 MyBatis 内部使用,并未留给用户扩展。实现代码如下。

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

}

public class DefaultSqlSessionFactory implements SqlSessionFactory {
	...省略实现
}

public class SqlSessionManager implements SqlSessionFactory, SqlSession {
	...省略实现
}

TransactionFactory

Transaction 表示在某次会话中,MyBatis 执行的一个事务,默认的实现是 JdbcTransaction,为了创建 Transaction,MyBatis 抽象出一个 TransactionFactory 作为工厂类,为了允许用户配置,因此设计为工厂方法模式。

MyBatis 中的实现代码如下。

public interface TransactionFactory {

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

    Transaction newTransaction(Connection conn);
    Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);

}

public class JdbcTransactionFactory implements TransactionFactory {
	... 省略实现代码
}

public class ManagedTransactionFactory implements TransactionFactory {
    ... 省略实现代码
}

DataSourceFactory

DataSource 是 JDBC 规范中用于获取 Connection 的类,MyBatis 内置了一些 DataSource,如 UnpooledDataSource、PooledDataSource。为了支持获取不同的 DataSource ,MyBatis 抽象出创建 DataSource 的 DataSourceFactory,为了允许用户进行扩展和配置,同样使用了工厂方法模式。

具体实现如下。

public interface DataSourceFactory {

    void setProperties(Properties props);

    DataSource getDataSource();

}

public class UnpooledDataSourceFactory implements DataSourceFactory {
	...省略实现代码
}

public class PooledDataSourceFactory extends UnpooledDataSourceFactory {
	...省略实现代码
}

public class JndiDataSourceFactory implements DataSourceFactory {
	...省略实现代码
}

其他工厂模式

MyBatis 的工厂模式使用较多,其他对于工厂模式使用的地方简单介绍如下,具体实现感兴趣的小伙伴可以直接参阅源码。

  • ObjectFactory:MyBatis 从数据库查询出数据后,需要根据配置将记录转换为对象,MyBatis 使用 ObjectFactory 创建对象,具体为工厂方法模式。

  • ObjectWrapperFactory:ObjectWrapper 是对普通对象的包装,MyBatis 执行 SQL 前需要获取对象的属性值然后设置参数,从数据库查询到数据后又需要存入对象。由于不同的对象如 collection、map、普通 object 设置和获取属性的方法不一样,因此 MyBatis 抽象出 ObjectWrapper,以统一的方法设置获取属性值。使用 ObjectWrapperFactory 作为工厂创建不同的 ObjectWrapper 实例。

  • VFS.VFSHolder:VFS 用于在不同的 Web 容器环境中获取文件资源,VFSHolder 使用简单工厂模式创建 VFS。

  • ReflectorFactory:Reflector 用于设置/获取属性,ReflectorFactory 作为简单工厂创建 Reflector 的实例。

  • ExceptionFactory:这是一个根据现有异常信息及 ErrorContext 信息包装新的运行时异常的简单工厂。

  • MapperProxyFactory:我们定义的 Mapper 为接口,为了可以直接使用,MyBatis 使用 MapperProxyFactory 作为简单工厂创建 Mapper 接口的代理 MapperProxy。

  • LogFactory:为了支持不同的日志实现框架,MyBatis 抽象出一个 Log 作为日志类,LogFactory 作为简单工厂根据配置创建出具体的日志实现类的对象。

  • ProxyFactory:这是一个创建数据库记录对应对象的代理的工厂,以便用于延迟初始化对象的复杂属性,具体使用了抽象工厂,根据配置选择 JDK 或 Javassist 创建代理。


建造者模式

建造者模式可以解决类的构造方法参数过多的问题,具体可参见《设计模式之建造者模式》

在 MyBatis 中,建造者模式主要被用来创建复杂的配置,并且 MyBatis 对建造者模式灵活运用,除了使用最经典的方式实现,还对建造者模式进行了修改。23种设计模式较为灵活,事实上我们使用的时候也不必拘泥于固定的模式。

经典的建造者模式

经典的建造者模式实现,MyBatis 主要用来创建配置。

Environment.Builder

Envrionment 是 MyBatis 执行 SQL 的环境,持有持有事务工厂 TransactionFactory 和数据源 DataSource。MyBatis 创建 Environment 使用的建造者模式代码如下。

public final class Environment {
    private final String id;

    private final TransactionFactory transactionFactory;

    private final DataSource dataSource;

    public Environment(String id, TransactionFactory transactionFactory, DataSource dataSource) {
        ...省略校验及属性赋值代码
    }

    public static class Builder {

        private final String id;

        private TransactionFactory transactionFactory;

        private DataSource dataSource;

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

        public Builder transactionFactory(TransactionFactory transactionFactory) {
            this.transactionFactory = transactionFactory;
            return this;
        }

        public Builder dataSource(DataSource dataSource) {
            this.dataSource = dataSource;
            return this;
        }

        public Environment build() {
            return new Environment(this.id, this.transactionFactory, this.dataSource);
        }

    }
    ...省略 get 方法
}

这里 Environment 的构造方法使用了 public 进行修饰,事实上 MyBatis 内部只使用了 Builder 创建 Environment,修改为 private 也许更为合适。并且 Environment 的参数并不多,不排除 MyBatis 对此有过度设计的嫌疑。

其他

其他对经典的建造者模式的实现和 Environment.Builder 套路类似,包括如下使用的地方。

  • MappedStatement.Builder
  • ParameterMapping.Builder
  • Discriminator.Builder
  • ResultMap.Builder
  • CacheBuilder

改进后的建造者模式

改进后的建造者模式 MyBatis 则主要用来对 xml 或注解进行解析生成配置。

以 SQLSessionFactoryBuilder 为例,实现代码如下。

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(Configuration config) {
        return new DefaultSqlSessionFactory(config);
    }
}

和经典的实现方式不同,SqlSessionFactoryBuilder 并非直接通过方法设置属性,而是委派 XMLConfigBuilder 解析 xml 配置,进而创建 SqlSessionFactory。其他很多与配置相关的建造者模式实现也是直接委派其他类解析配置或者直接解析配置。其他改进后的建造者模式主要包括如下。

  • XMLConfigBuilder
  • XMLMapperBuilder
  • XMLScriptBuilder
  • SqlSourceBuilder
  • XMLStatementBuilder

代理

代理类似于我们日常生活中接触到的中介,它常用于在不改变原始类的情况下为原始类添加新的功能。关于更多代理的内容,可以参见《Java 中创建代理的几种方式》

MyBatis 对代理使用的场景并不局限于功能增强,具体如下。

功能增强

SQL 日志打印

为了打印 SQL 相关的日志,MyBatis 为 JDBC 规范中的各种类型提供了代理,在执行对应方法的时候就会进行日志打印。代理类包括如下。

  • ConnectionLogger:打印 Connection 类#prepareStatement#prepareCall方法执行日志。
  • PreparedStatementLogger:打印 PreparedStatement 类执行的 SQL 及参数。
  • StatementLogger:打印 Statement 类执行的 SQL。
  • ResultSetLogger:打印 ResultSet 返回的记录条数。

以 ConnectionLogger 为例,其实现代码如下。

public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {

    private final Connection connection;

    private ConnectionLogger(Connection conn, Log statementLog, int queryStack) {
        super(statementLog, queryStack);
        this.connection = conn;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] params)
        throws Throwable {
        try {
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, params);
            }
            if ("prepareStatement".equals(method.getName()) || "prepareCall".equals(method.getName())) {
                if (isDebugEnabled()) {
                    debug(" Preparing: " + removeExtraWhitespace((String) params[0]), true);
                }
                PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
                stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
                return stmt;
            }
            ... 省略部分代码
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
    }

    public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
        InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
        ClassLoader cl = Connection.class.getClassLoader();
        return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
    }   
}

ConnectionLogger 作为 Connection 的代理对象,实现了 JDK 动态代理所需的 InvocationHandler,自身作为工厂提供了创建 Connection 代理实例的方法。在 Connection 相关方法执行时,就会进行日志打印。

插件实现

代理模式同样在 MyBatis 中的插件实现中有使用。

我们自定义的插件需要实现 Interceptor,这个 Interceptor 会被放在 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);
    }
}

在 MyBatis 执行 SQL 的生命周期中,InterceptorChain#pluginAll会为生命周期中使用到的 ParameterHandler、ResultSetHandler、StatementHandler、Executor 创建代理对象,从而可以在生命周期中插入我们自定义的逻辑,如分页、数据库字段加密等。具体可参见谈谈 MyBatis 的插件,除了分页你可能还有这些使用场景

生成接口实现

在 MyBatis 的前身 IBatis 中,我们调用 SqlSession 中的方法执行 SQL,方法参数中使用字符串硬编码的方式定位要执行的 SQL,很容易出现拼写错误,为了解决这个问题,MyBatis 允许我们创建 Mapper 接口,然后 MyBatis 为我们的 Mapper 创建代理,底层仍然是调用了 SqlSession 的方法。

Mapper 接口的代理类为 MapperProxy,它由 MapperProxyFactory 通过简单工厂创建。

类加载处理

除了上述 MyBatis 中代理的使用场景,为了保证 DriverManager 中注册的 Driver 由系统类加载器加载,MyBatis 还为其内部使用到的 Drvier 提供了代理。具体如下。

    private static class DriverProxy implements Driver {
        private Driver driver;

        DriverProxy(Driver d) {
            this.driver = d;
        }

        @Override
        public boolean acceptsURL(String u) throws SQLException {
            return this.driver.acceptsURL(u);
        }

		...省略部分方法
    }

装饰器

装饰器的实现和静态代理的代码实现很相似,但是它们的使用场景不太一样,装饰器主要用在对原有功能的增强,而不是为原有类型添加新的功能。关于更多装饰器模式的内容,可以参考《透过 Java IO 流学习装饰器模式》

MyBatis 对装饰器模式的使用如下。

数据源

MyBatis 内置了不支持池的数据源 UnpooledDataSource 和支持池的数据源 PooledDataSource。由于这两者部分实现类似,因此 PooledDataSource 使用装饰器模式,对 UnpooledDataSource 进行增强,提供了池,以便减少反复获取释放连接的资源消耗。

缓存

为了加快查询以及解决循环引用问题,MyBatis 添加了缓存功能,在 mapper xml 文件中可以配置不同的缓存参数,如刷新间隔、缓存数量、清除策略等等,根据不同的参数,需要创建不同的缓存对象。

由于 MyBatis 事先并不知道用户会进行哪些配置,如果为各种配置的组合创建不同的缓存类,那么将导致 MyBatis 中存在较多的的缓存类,并且也会导致实现的复杂。为了解决这个问题,MyBatis 使用装饰器模式,通过为原有 Cache 进行装饰,以支持不同的参数,这样不同的参数就可以创建出不同的 Cache。

以支持日志打印的 LoggingCache 为例,其实现如下。

public class LoggingCache implements Cache {

    private final Log log;

    private final Cache delegate;

    public LoggingCache(Cache delegate) {
        this.delegate = delegate;
        this.log = LogFactory.getLog(getId());
    }

    @Override
    public Object getObject(Object key) {
        requests++;
        final Object value = delegate.getObject(key);
        if (value != null) {
            hits++;
        }
        if (log.isDebugEnabled()) {
            log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());
        }
        return value;
    }
    ...省略部分代码
}

组合模式

组合模式将一组对象组织成树形结构,以表示一种“部分 - 整体”的层次结构。由于其用于处理树形结构的数据,因此使用场景来说相对受限。组合模式并无固定的代码模板,每种场景不太相同。

MyBatis 主要将组合模式应用在 mapper xml 文件内容的解析,由于 xml 标签可以相互嵌套,因此可以表示树形结构,使用组合模式最为合适不过。MyBatis 将 xml 中的节点抽象为 SqlNode,解析节点代码的入口为XMLLanguageDriver#createSqlSource,感兴趣的朋友可自行阅读源码。


适配器模式

适配器模式将不兼容的接口转换为兼容的接口,多是因为旧的接口设计不合理所致。更多适配器模式的内容,可以参考《设计模式之适配器模式》

在 Java 的开源库中,关于日志的框架众多,包括 log4j、log4j2、commons-loging 等等,为了支持不同的日志实现,MyBatis 使用适配器模式提供了新的日志接口 Log,以便对不同的日志实现进行整合。运行时根据配置或项目中存在的日志实现,使用不同的日志框架。

以支持 FIFO 的 FifoCache 为例,其实现代码如下。

public class FifoCache implements Cache {

    private final Cache delegate;

    private final Deque<Object> keyList;

    private int size;

    public FifoCache(Cache delegate) {
        this.delegate = delegate;
        this.keyList = new LinkedList<>();
        this.size = 1024;
    }

    @Override
    public void putObject(Object key, Object value) {
        cycleKeyList(key);
        delegate.putObject(key, value);
    }
    
    private void cycleKeyList(Object key) {
        keyList.addLast(key);
        if (keyList.size() > size) {
            Object oldestKey = keyList.removeFirst();
            delegate.removeObject(oldestKey);
        }
    }
	...省略部分代码
}

责任链

责任链模式将请求的发送与接收者解耦,由链上的每个对象处理请求。更多关于责任链模式的内容,可以参考《设计模式之责任链模式》

MyBatis 使用责任链模式主要用来实现插件,在 MyBatis 执行 SQL 的生命周期中,将插件组成一个链,在特定的生命周期中执行额外操作。

MyBatis 对插件的责任链模式实现如下。

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

}

模板方法

模板方法定义了算法的骨架,将算法中的某些步骤推迟到子类来实现。主要用于复用和扩展。更多关于模板方法的内容,可以参考《设计模式之模板方法》

MyBatis 对模板方法的使用主要有两块,包括如下。

  • BaseExecutor:Executor 负责组装 SQL 执行所需的的各模块,完成整个流程。为了复用代码,把重复的逻辑提取到模板方法中。
  • BaseTypeHandler:TypeHandler 用于设置 JDBC 中 SQL 的参数,以及从 ResultSet 中获取值。

以 BaseTypeHandler 为例,其实现代码如下。

public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> {

    @Override
    public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
        if (parameter == null) {
            if (jdbcType == null) {
                throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
            }
            try {
                ps.setNull(i, jdbcType.TYPE_CODE);
            } catch (SQLException e) {
                throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . "
                    + "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. "
                    + "Cause: " + e, e);
            }
        } else {
            try {
            	// 子类实现
                setNonNullParameter(ps, i, parameter, jdbcType);
            } catch (Exception e) {
                throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . "
                    + "Try setting a different JdbcType for this parameter or a different configuration property. "
                    + "Cause: " + e, e);
            }
        }
    }

    public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
    
	...省略部分代码
}

迭代器模式

迭代器模式用于遍历对象,大多数编程语言都使用迭代器模式实现对集合对象的遍历。

MyBatis 中的迭代器和传统的迭代器模式实现不太相同,它主要用迭代器实现对嵌套的属性遍历。MyBatis 中使用 PropertyTokenizer 表示嵌套的属性,其实现如下。

public class PropertyTokenizer implements Iterator<PropertyTokenizer> {

    /**
     * 属性名称,如 props
     */
    private String name;

    /**
     * 带索引的属性名称,如 props[1]
     */
    private final String indexedName;

    /**
     * 索引,如 1
     */
    private String index;

    /**
     * 子属性,如 props[1].cd 中的 cd
     */
    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);
        }
    }

	...省略get方法

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

总结

MyBatis 中使用的设计模式和其场景具有一定关系,并且 MyBatis 对设计模式的使用并不拘泥于传统的实现方式,MyBatis 还结合场景进行了一些改进。不过也有一些代码不排除 MyBatis 过度使用设计模式的嫌疑。MyBatis 作为一个小巧的框架,结合官网文档,其源码阅读难度较低,理解其底层实现也便于我们日常排查问题,推荐大家阅读。

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大鹏cool

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值