Mybatis 使用的 9 种设计模式,真是太有用了(一)

简介: Mybatis 使用的 9 种设计模式,真是太有用了
  • 1、Builder模式
  • 2、工厂模式
  • 3、单例模式
  • 4、代理模式
  • 5、组合模式

虽然我们都知道有 23 个设计模式,但是大多停留在概念层面,真实开发中很少遇到,Mybatis源码中使用了大量的设计模式,阅读源码并观察设计模式在其中的应用,能够更深入的理解设计模式。

Mybatis至少遇到了以下的设计模式的使用:

  1. Builder模式,例如SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder;
  2. 工厂模式,例如SqlSessionFactory、ObjectFactory、MapperProxyFactory;
  3. 单例模式,例如ErrorContext和LogFactory;
  4. 代理模式,Mybatis实现的核心,比如MapperProxy、ConnectionLogger,用的jdk的动态代理;还有executor.loader包使用了cglib或者javassist达到延迟加载的效果;
  5. 组合模式,例如SqlNode和各个子类ChooseSqlNode等;
  6. 模板方法模式,例如BaseExecutor和SimpleExecutor,还有BaseTypeHandler和所有的子类例如IntegerTypeHandler;
  7. 适配器模式,例如Log的Mybatis接口和它对jdbc、log4j等各种日志框架的适配实现;
  8. 装饰者模式,例如Cache包中的cache.decorators子包中等各个装饰者的实现;
  9. 迭代器模式,例如迭代器模式PropertyTokenizer;

接下来挨个模式进行解读,先介绍模式自身的知识,然后解读在Mybatis中怎样应用了该模式。

1、Builder模式

Builder模式的定义是“将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。”,它属于创建类模式,一般来说,如果一个对象的构建比较复杂,超出了构造函数所能包含的范围,就可以使用工厂模式和Builder模式,相对于工厂模式会产出一个完整的产品,Builder应用于更加复杂的对象的构建,甚至只会构建产品的一个部分。

image.png

在Mybatis环境的初始化过程中,SqlSessionFactoryBuilder会调用XMLConfigBuilder读取所有的MybatisMapConfig.xml和所有的*Mapper.xml文件,构建Mybatis运行的核心对象Configuration对象,然后将该Configuration对象作为参数构建一个SqlSessionFactory对象。

其中XMLConfigBuilder在构建Configuration对象时,也会调用XMLMapperBuilder用于读取*Mapper文件,而XMLMapperBuilder会使用XMLStatementBuilder来读取和build所有的SQL语句。

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

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

image.png

即根据不同的输入参数来构建SqlSessionFactory这个工厂对象。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能。

项目地址:https://github.com/YunaiV/ruoyi-vue-pro

2、工厂模式

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

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

image.png

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

image.png

可以看到,该Factory的openSession方法重载了很多个,分别支持autoCommit、Executor、Transaction等参数的输入,来构建核心的SqlSession对象。

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

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

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

在这里其实也可以看到端倪,SqlSession的执行,其实是委托给对应的Executor来进行的。

而对于LogFactory,它的实现代码:

public final class LogFactory {
 private static Constructor<? extends Log> logConstructor;
 private LogFactory() {
  // disable construction
 }
 public static Log getLog(Class<?> aClass) {
  return getLog(aClass.getName());
 }

这里有个特别的地方,是Log变量的的类型是Constructor<? extends Log>,也就是说该工厂生产的不只是一个产品,而是具有Log公共接口的一系列产品,比如Log4jImpl、Slf4jImpl等很多具体的Log。

基于微服务的思想,构建在 B2C 电商场景下的项目实战。核心技术栈,是 Spring Boot + Dubbo 。未来,会重构成 Spring Cloud Alibaba 。

项目地址:https://github.com/YunaiV/onemall

3、单例模式

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

单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。单例模式是一种对象创建型模式。单例模式又名单件模式或单态模式。

image.png

在Mybatis中有两个地方用到单例模式,ErrorContext和LogFactory,其中ErrorContext是用在每个线程范围内的单例,用于记录该线程的执行环境错误信息,而LogFactory则是提供给整个Mybatis使用的日志工厂,用于获得针对项目配置好的日志对象。

ErrorContext的单例实现代码:

public class ErrorContext {
 private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();
 private ErrorContext() {
 }
 public static ErrorContext instance() {
  ErrorContext context = LOCAL.get();
  if (context == null) {
   context = new ErrorContext();
   LOCAL.set(context);
  }
  return context;
 }

构造函数是private修饰,具有一个static的局部instance变量和一个获取instance变量的方法,在获取实例的方法中,先判断是否为空如果是的话就先创建,然后返回构造好的对象。

只是这里有个有趣的地方是,LOCAL的静态实例变量使用了ThreadLocal修饰,也就是说它属于每个线程各自的数据,而在instance()方法中,先获取本线程的该实例,如果没有就创建该线程独有的ErrorContext。

4、代理模式

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

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

代理模式包含如下角色:

  • Subject: 抽象主题角色
  • Proxy: 代理主题角色
  • RealSubject: 真实主题角色

image.png

image.png

这里有两个步骤,第一个是提前创建一个Proxy,第二个是使用的时候会自动请求Proxy,然后由Proxy来执行具体事务;

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

/**
 * @author Lasse Voss
 */
public class MapperProxyFactory<T> {
 private final Class<T> mapperInterface;
 private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
 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);
 }
 public T newInstance(SqlSession sqlSession) {
  final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
  return newInstance(mapperProxy);
 }
}

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

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

public class MapperProxy<T> implements InvocationHandler, Serializable {
 @Override
 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
   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);
  }
  final MapperMethod mapperMethod = cachedMapperMethod(method);
  return mapperMethod.execute(sqlSession, args);
 }

非常典型的,该MapperProxy类实现了InvocationHandler接口,并且实现了该接口的invoke方法。

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

5、组合模式

组合模式组合多个对象形成树形结构以表示“整体-部分”的结构层次。

组合模式对单个对象(叶子对象)和组合对象(组合对象)具有一致性,它将对象组织到树结构中,可以用来描述整体与部分的关系。同时它也模糊了简单元素(叶子对象)和复杂元素(容器对象)的概念,使得客户能够像处理简单元素一样来处理复杂元素,从而使客户程序能够与复杂元素的内部结构解耦。

在使用组合模式中需要注意一点也是组合模式最关键的地方:叶子对象和组合对象实现相同的接口。这就是组合模式能够将叶子节点和对象节点进行一致处理的原因。

image.png

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方法是所有的动态节点都实现的接口:

public interface SqlNode {
 boolean apply(DynamicContext context);
}

对于实现该SqlSource接口的所有节点,就是整个组合模式树的各个节点:

image.png

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

@Override
 public boolean apply(DynamicContext context) {
  GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
  context.appendSql(parser.parse(text));
  return true;
 }

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

@Override
 public boolean apply(DynamicContext context) {
  if (evaluator.evaluateBoolean(test, context.getBindings())) {
   contents.apply(context);
   return true;
  }
  return false;
 }


相关文章
|
6月前
|
设计模式 Java 数据库连接
MyBatis 设计模式解析
MyBatis 设计模式解析
|
设计模式 SQL XML
「分享」从Mybatis源码中,学习到的10种设计模式
23种设计模式,Mybatis源码中竟然用了一半!怒卷Mybatis设计模式,从源码中学习设计模式,太爽了!
2457 2
「分享」从Mybatis源码中,学习到的10种设计模式
|
设计模式 Java 数据库连接
Mybatis源码分析一-Mybatis基础架构以及设计模式
Mybatis源码分析一-Mybatis基础架构以及设计模式
172 0
|
设计模式 SQL 存储
【MyBatis】面试官:MyBatis中是如何使用设计模式的?
本篇文章了解一下MyBatis中运用到的设计模式,我们在实际的开发中很大程度上只是对设计模式留在一个理论的环节上,缺少实践,通过源码,我们可以学习一下这些设计模式的实践方式,有利于我们能够更深入的理解和使用设计模式。
|
SQL 设计模式 存储
Mybatis 使用的 9 种设计模式,真是太有用了(二)
Mybatis 使用的 9 种设计模式,真是太有用了(二)
Mybatis 使用的 9 种设计模式,真是太有用了(二)
|
设计模式 SQL XML
MyBatis 中的设计模式
前言 软件开发的流程一般可分为分析、设计、实现,设计模式在处于设计或代码实现阶段,以设计思想、设计原则作为指导,相对来说更为具象,是前人对经常遇到的设计问题总结出的一套解决方案,多数设计模式用来解决代码的扩展性问题,在框架中使用的场景较多。
306 0
|
设计模式 安全 Java
设计模式系列之组合模式及其在JDK和MyBatis源码中的运用详解
设计模式系列之组合模式及其在JDK和MyBatis源码中的运用详解
设计模式系列之组合模式及其在JDK和MyBatis源码中的运用详解
|
16天前
|
设计模式 安全 Java
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
|
2月前
|
设计模式 数据库连接 PHP
PHP中的设计模式:提升代码的可维护性与扩展性在软件开发过程中,设计模式是开发者们经常用到的工具之一。它们提供了经过验证的解决方案,可以帮助我们解决常见的软件设计问题。本文将介绍PHP中常用的设计模式,以及如何利用这些模式来提高代码的可维护性和扩展性。我们将从基础的设计模式入手,逐步深入到更复杂的应用场景。通过实际案例分析,读者可以更好地理解如何在PHP开发中应用这些设计模式,从而写出更加高效、灵活和易于维护的代码。
本文探讨了PHP中常用的设计模式及其在实际项目中的应用。内容涵盖设计模式的基本概念、分类和具体使用场景,重点介绍了单例模式、工厂模式和观察者模式等常见模式。通过具体的代码示例,展示了如何在PHP项目中有效利用设计模式来提升代码的可维护性和扩展性。文章还讨论了设计模式的选择原则和注意事项,帮助开发者在不同情境下做出最佳决策。
|
18天前
|
设计模式 开发者 Python
Python编程中的设计模式:工厂方法模式###
本文深入浅出地探讨了Python编程中的一种重要设计模式——工厂方法模式。通过具体案例和代码示例,我们将了解工厂方法模式的定义、应用场景、实现步骤以及其优势与潜在缺点。无论你是Python新手还是有经验的开发者,都能从本文中获得关于如何在实际项目中有效应用工厂方法模式的启发。 ###