剖析Mybatis中用到的设计模式

发布于:2025-04-07 ⋅ 阅读:(31) ⋅ 点赞:(0)

框架对很多设计模式的实现,都并非标准的代码实现,都做了比较多的自我改进。实际上,这就是所谓的灵活应用, 只借鉴不照搬, 根据具体问题针对性地去解决。

涉及到Mybatis源码知识设计模式,这点我之前的博客均有涉及。

可以查看我的专栏~ 点击上述文字也可以跳转至专栏信息页哦。

目录

Mybatis中使用的设计模式

构造者模式(Build模式)

工厂模式

单例模式

代理模式

组合模式

模板方法模式

适配器模式

装饰器模式

MyBatis一级缓存

一级缓存的执行流程

迭代器模式


Mybatis走进源码_振鹏Dong的博客-CSDN博客

本文将综合前者内容,对Mybatis中设计模式应用做以分析。

首先从这几点进行回顾

ORM:Object-Relationl Mapping,对象关系映射,它的作用是在关系型数据库和对象之间作一个映射,这样我们在具体的操作数据库的时候,只要像平时操作对象一样操作它就可以了,ORM框架会根据映射完成对数据库的操作,就不需要再去和复杂的SQL语句打交道了。

Mybatis中使用的设计模式

构造者模式(Build模式)

Builder模式的定义是“将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。”,它属于创建类模式,建造者模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要指定复杂对象的类型就可以得到该对象,而无须知道其内部的具体构造细节。

《effective-java》中第2条也提到:遇到多个构造器参数时,考虑用构建者(Builder)模式

  • 在Mybatis框架的初始化时候,我们是通过SqlSessionFactory 读取mybatis配置文件和所有***Mapper.xml文件,构建Mybatis运行的核心对象——configuration,然后configuration对象作为SqlSessionFatory的对象。

这个过程中,有一个相似的特点,就是这些Builder会读取文件或者配置,然后做大量的XpathParser解析、配置或语法的解析、反射生成对象、存入结果缓存等步骤,这么多的工作都不是一个构造函数所能包括的,因此大量采用了Builder模式来解决。

对于builder的具体类,方法都大都用build*开头,比如SqlSessionFactoryBuilder为例,它包含以下方法:

从建造者模式的设计初衷上来看,SqlSessionFactoryBuilder 虽然带有 Builder 后缀,但 不要被它的名字所迷惑,它并不是标准的建造者模式。一方面,原始类 SqlSessionFactory 的构建只需要一个参数,并不复杂。

另一方面,Builder 类SqlSessionFactoryBuilder 仍然定义了多包含不同参数列表的构造函数。 实际上,SqlSessionFactoryBuilder 设计的初衷只不过是为了简化开发。因为构建 SqlSessionFactory 需要先构建 Configuration,而构建 Configuration 是非常复杂的,需 要做很多工作,比如配置的读取、解析、创建 n 多对象等。为了将构建 SqlSessionFactory 的过程隐藏起来,对程序员透明,MyBatis 就设计了 SqlSessionFactoryBuilder 类封装这些构建细节。

工厂模式

在Mybatis中比如SqlSessionFactory使用的是工厂模式,该工厂没有那么复杂的逻辑,是一个简单工厂模式。

简单工厂模式(Simple Factory Pattern):又称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。

SqlSession可以认为是一个Mybatis工作的核心的接口,通过这个接口可以执行执行SQL语句、获取Mappers、管理事务。类似于连接MySQL的Connection对象。

该Factory的openSession()方法重载了很多个,分别支持autoCommitExecutorTransaction 等参数的输入,来构建核心的SqlSession对象。

DefaultSqlSessionFactory的默认工厂实现里,有一个方法可以看出工厂怎么产出一个产品

这是一个openSession调用的底层方法,该方法先从configuration读取对应的环境配置,然后初始化TransactionFactory获得一个Transaction对象,然后通过Transaction获取一个Executor对象,最后通过configuration、Executor、是否autoCommit三个参数构建了SqlSession

单例模式

单例模式(Singleton Pattern):单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。单例模式是一种对象创建型模式。单例模式又名单件模式或单态模式。在Mybatis中有两个地方用到单例模式,ErrorContextLogFactory,其中ErrorContext是用在每个线程范围内的单例,用于记录该线程的执行环境错误信息,而LogFactory则是提供给整个Mybatis使用的日志工厂,用于获得针对项目配置好的日志对象。

public class ErrorContext {
    private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<>();
    // LOCAL的静态实例变量使用了ThreadLocal修饰,
    //也就是说它属于每个线程各自的数据,
    //而在instance()方法中,先获取本线程的该实例,如果没有就创建该线程独有的ErrorContext。
    
    private ErrorContext() {
    }
    
    public static ErrorContext instance() {
    // 构造函数是private修饰,具有一个static的局部instance变量和一个获取instance变量的方法,
    // 在获取实例的方法中,先判断是否为空如果是的话就先创建,然后返回构造好的对象。
        ErrorContext context = LOCAL.get();
        if (context == null) {
            context = new ErrorContext();
            LOCAL.set(context);
        }
        return context;
    }
}

代理模式

代理模式可以认为是Mybatis的核心使用的模式,正是由于这个模式,我们只需要编写Mapper.java接口,不需要实现,由Mybatis后台帮我们完成具体SQL的执行。

代理模式(Proxy Pattern) :给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式的英 文叫做Proxy,它是一种对象结构型模式。

当我们使用ConfigurationgetMapper方法时,会调用mapperRegistry.getMapper方法,而该方法又会调用mapperProxyFactory.newInstance(sqlSession)来生成一个具体的代理:


/**
 * Mapper Proxy 工厂类
 *
 * @author Lasse Voss
 */
public class MapperProxyFactory<T> {

    /**
     * Mapper 接口
     */
    private final Class<T> mapperInterface;
    /**
     * 方法与 MapperMethod 的映射
     */
    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) {

        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
    }

   //MapperProxyFactory类中的newInstance方法
    public T newInstance(SqlSession sqlSession) {
        // 创建了JDK动态代理的invocationHandler接口的实现类mapperProxy
        final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
        // 调用了重载方法
        return newInstance(mapperProxy);
    }

}

在这里,先通过T newInstance(SqlSession sqlSession)方法会得到一个MapperProxy对象,然后调用T newInstance(MapperProxy<T> mapperProxy)生成代理对象然后返回。

而查看MapperProxy的代码,可以看到如下内容:

非常典型~,该MapperProxy类实现了InvocationHandler接口,并且实现了该接口的invoke方法。通过这种方式,我们只需要编写Mapper.java接口类,当真正执行一个Mapper接口的时候,就会转发给MapperProxy.invoke方法,而该方法则会调用后续的sqlSession.cud>executor.execute>prepareStatement等一系列方法,完成SQL的执行和返回。

public class MapperProxy<T> implements InvocationHandler, Serializable {

    private static final long serialVersionUID = -6424540398559729838L;

    /**
     * SqlSession 对象
     */
    private final SqlSession sqlSession;
    /**
     * Mapper 接口
     */
    private final Class<T> mapperInterface;
    /**
     * 方法与 MapperMethod 的映射
     *
     * 从 {@link MapperProxyFactory#methodCache} 传递过来
     */
    private final Map<Method, MapperMethod> methodCache;

    // 构造,传入了SqlSession,说明每个session中的代理对象的不同的!
    public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            // 如果是 Object 定义的方法,直接调用
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, args);

            } else if (isDefaultMethod(method)) {
                return invokeDefaultMethod(proxy, method, args);
            }
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
        // 获得 MapperMethod 对象
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        // 重点在这:MapperMethod最终调用了执行的方法
        return mapperMethod.execute(sqlSession, args);
    }

}

组合模式

将对象组合成树形结构以表示整个部分的层次结构.组合模式可以让用户统一对待单个对象和对象的组合.

组合模式其实就是将一组对象(文件夹和文件)组织成树形结构,以表示一种'部分-整体' 的层次结构,(目录与子目录的嵌套结构). 组合模式让客户端可以统一单个对象(文件)和组合对象(文件夹)的处理逻辑(递归遍历)。Mybatis支持动态SQL的强大功能,比如下面的这个SQL:

<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">
    UPDATE users
    <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>

在这里面使用到了trim、if等动态元素,可以根据条件来生成不同情况下的SQL;在DynamicSqlSource.getBoundSql方法里,调用了rootSqlNode.apply(context)方法,apply方法是所有的动态节点都实现的接口:

/**
 * SQL Node 接口,每个 XML Node 会解析成对应的 SQL Node 对象
 * @author Clinton Begin
 */
public interface SqlNode {

    /**
     * 应用当前 SQL Node 节点
     *
     * @param context 上下文
     * @return 当前 SQL Node 节点是否应用成功。
     */
    boolean apply(DynamicContext context);
}

组合模式的简单之处在于,所有的子节点都是同一类节点,可以递归的向下执行,比如对于TextSqlNode,因为它是最底层的叶子节点,所以直接将对应的内容append到SQL语句中:

但是对于IfSqlNode,就需要先做判断,如果判断通过,仍然会调用子元素的SqlNode,即contents.apply方法,实现递归的解析。

模板方法模式

在操作中定义算法的框架,将一些步骤推迟到子类中。模板方法让子类在不改变算法结构的情况下重新定义算法的某些步骤。

模板方法中的算法可以理解为广义上的业务逻辑,并不是特指某一个实际的算法.定义中所说的算法的框架就是模板, 包含算法框架的方法就是模板方法。

模板类定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

在Mybatis中,sqlSession的SQL执行,都是委托给Executor实现的,Executor包含以下结构

其中的BaseExecutor就采用了模板方法模式,它实现了大部分的SQL执行逻辑,然后把以下几个方法交给子类定制化完成:

该模板方法类有几个子类的具体实现,使用了不同的策略:

  • 简单SimpleExecutor:每执行一次updateselect,就开启一个Statement对象,用完立刻关闭Statement对象。(可以是StatementPrepareStatement对象)

  • 重用ReuseExecutor:执行updateselect,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map内,供下一次使用。(可以是StatementPrepareStatement对象)

  • 批量BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理的;BatchExecutor相当于维护了多个桶,每个桶里都装了很多属于自己的SQL,就像苹果蓝里装了很多苹果,番茄蓝里装了很多番茄,最后,再统一倒进仓库。(可以是Statement或PrepareStatement对象)

模板模式基于继承来实现代码复用。如果抽象类中包含模板方法,模板方法调用有待子类实 现的抽象方法,那这一般就是模板模式的代码实现。而且,在命名上,模板方法与抽象方法 一般是一一对应的,抽象方法在模板方法前面多一个“do”,比如,在 BaseExecutor 类 中,其中一个模板方法叫 update(),那对应的抽象方法就叫 doUpdate()。

适配器模式

适配器模式(Adapter Pattern) :将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。

在Mybatsi的logging包中,有一个Log接口:

该接口定义了Mybatis直接使用的日志方法,而Log接口具体由谁来实现呢?Mybatis提供了多种日志框架的实现,这些实现都匹配这个Log接口所定义的接口方法,最终实现了所有外部日志框架到Mybatis日志包的适配:比如对于Log4jImpl的实现来说,该实现持有了org.apache.log4j.Logger的实例,然后所有的日志方法,均委托该实例来实现。

package org.apache.ibatis.logging.log4j;

import org.apache.ibatis.logging.Log;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;

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

在适配器模式中,传递给适配器构造函数的是被适配的类对象,而这里是 clazz(相当于日志名称 name),所以,从代码实现上来讲,它并非标准的适配器模式。但是,从应用 场景上来看,这里确实又起到了适配的作用,是典型的适配器模式的应用场景。

装饰器模式

动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类实现更为灵活。其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。

在mybatis中,缓存的功能由根接口Cache(org.apache.ibatis.cache.Cache)定义。整个体系采用装饰器设计模式,数据存储和缓存的基本功能由PerpetualCache(org.apache.ibatis.cache.impl.PerpetualCache)永久缓存实现,然后通过一系列的装饰器来对PerpetualCache永久缓存进行缓存策略等方面的控制。

  • FifoCache:先进先出算法,缓存回收策略

  • LoggingCache:输出缓存命中的日志信息

  • LruCache:最近最少使用算法,缓存回收策略

  • ScheduledCache:调度缓存,负责定时清空缓存

  • SerializedCache:缓存序列化和反序列化存储

  • SoftCache:基于软引用实现的缓存管理策略

  • SynchronizedCache:同步的缓存装饰器,用于防止多线程并发访问

  • WeakCache:基于弱引用实现的缓存管理策略

MyBatis一级缓存

缓存就是内存中的数据,常常来自对数据库查询结果的保存。使用缓存,我们可以避免频繁的与数据库进行交互,进而提高响应速度。

MyBatis也提供了对缓存的支持,分为一级缓存和二级缓存,可以通过下图来理解:

①、一级缓存是SqlSession级别的缓存。在操作数据库时需要构造sqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。

②、二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的

一级缓存默认是开启的

① 我们使用同一个sqlSession,对User表根据相同id进行两次查询,查看他们发出sql语句的情况

② 同样是对user表进行两次查询,只不过两次查询之间进行了一次update操作。

总结

1、第一次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,如果没有,从 数据库查询用户信息。得到用户信息,将用户信息存储到一级缓存中。

2、 如果中间sqlSession去执行commit操作(执行插入、更新、删除),则会清空SqlSession中的 一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。

3、 第二次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,缓存中有,直接从缓存中获取用户信息

一级缓存的执行流程

我们进入到org.apache.ibatis.executor.Executor中 看到一个方法CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) ,见名思意是一个创建CacheKey的方法 找到它的实现类和方法org.apache.ibatis.executor.BaseExecuto.createCacheKey

在执行SQL前如何在一级缓存中找不到Key,那么将会执行sql,我们来看一下执行sql前后会做些什么,

进入list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

总结

  1. 一级缓存的数据结构是一个HashMap<Object,Object>,它的value就是查询结果,它的key是CacheKeyCacheKey中有一个list属性,statementId,params,rowbounds,sql等参数都存入到了这个list

  2. 先创建CacheKey,会首先根据CacheKey查询缓存中有没有,如果有,就处理缓存中的参数,如果没有,就执行sql,执行sql后执行sql后把结果存入缓存

迭代器模式

迭代器(Iterator)模式,又叫做游标(Cursor)模式。GOF给出的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。

在软件系统中,容器对象拥有两个职责: 一是存储数据,而是遍历数据。从依赖性上看,前者是聚合对象的基本职责。而后者是可变化的,又是可分离的。因此可以将遍历数据的行为从容器中抽取出来,封装到迭代器对象中,由迭代器来提供遍历数据的行为,这将简化聚合对象的设计,更加符合单一职责原则。

Java的Iterator就是迭代器模式的接口,只要实现了该接口,就相当于应用了迭代器模式:

比如Mybatis的PropertyTokenizer是property包中的重量级类,该类会被reflection包中其他的类频繁的引用到。这个类实现了Iterator接口,在使用时经常被用到的是Iterator接口中的hasNext这个函数。

/**
 * 属性分词器
 *
 * @author Clinton Begin
 */
public class PropertyTokenizer implements Iterator<PropertyTokenizer> {

    /**
     * 当前字符串
     */
    private String name;
    /**
     * 索引的 {@link #name} ,因为 {@link #name} 如果存在 {@link #index} 会被更改
     */
    private final String indexedName;
    /**
     * 编号。
     *
     * 对于数组 name[0] ,则 index = 0
     * 对于 Map map[key] ,则 index = key
     */
    private String index;
    /**
     * 剩余字符串
     */
    private final String children;

    public PropertyTokenizer(String fullname) {
        // 初始化 name、children 字符串,使用 . 作为分隔
        int delim = fullname.indexOf('.');
        if (delim > -1) {
            name = fullname.substring(0, delim);
            children = fullname.substring(delim + 1);
        } else {
            name = fullname;
            children = null;
        }
        // 记录当前 name
        indexedName = name;
        // 若存在 [ ,则获得 index ,并修改 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.");
    }

}

可以看到,这个类传入一个字符串到构造函数,然后提供了iterator方法对解析后的子串进行遍历,是一个很常用的方法类。

实际上,PropertyTokenizer 类也并非标准的迭代器类。它将配置的解析、解析之后的元素、迭代器,这三部分本该放到三个类中的代码,都耦合在一个类中,所以看起来稍微有点难懂。不过,这样做的好处是能够做到惰性解析。我们不需要事先将整个配置,解析成多个 PropertyTokenizer 对象。只有当我们在调用 next() 函数的时候,才会解析其中部分配置。