1. 首页
  2. Java
  3. 设计模式

设计模式应用,MyBatis中使用了那些设计模式?

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

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

  • Builder模式,例如SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder;
  • 工厂模式,例如SqlSessionFactory、ObjectFactory、MapperProxyFactory;
  • 单例模式,例如ErrorContext和LogFactory;
  • 代理模式,Mybatis实现的核心,比如MapperProxy、ConnectionLogger,用的jdk的动态代理;还有executor.loader包使用了cglib或者javassist达到延迟加载的效果;
  • 组合模式,例如SqlNode和各个子类ChooseSqlNode等;

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

1、Builder模式

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

在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为例,它包含以下方法:

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

2、工厂模式

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

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

设计模式应用,MyBatis中使用了那些设计模式?

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

设计模式应用,MyBatis中使用了那些设计模式?

可以看到,该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,也就是说该工厂生产的不只是一个产品,而是具有Log公共接口的一系列产品,比如Log4jImpl、Slf4jImpl等很多具体的Log。

3、单例模式

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

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

在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: 真实主题角色

设计模式应用,MyBatis中使用了那些设计模式?

这里有两个步骤,第一个是提前创建一个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(MapperProxy<T> mapperProxy)生成代理对象然后返回。

而查看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、组合模式

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

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

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

设计模式应用,MyBatis中使用了那些设计模式?

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

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

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

设计模式应用,MyBatis中使用了那些设计模式?

组合模式的简单之处在于,所有的子节点都是同一类节点,可以递归的向下执行,比如对于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;
 }

以上分析了5种MyBatis中使用到的设计模式,也还有一些文中没有涉及到的模式,欢迎大家继续深挖,感受源码的魅力。

发布者:Craig,转转请注明出处:https://www.goodlymoon.com/archives/1448.html

发表评论

电子邮件地址不会被公开。 必填项已用*标注

QR code