Mybatis中的设计模式

mybatis,设计模式 · 浏览次数 : 24

小编点评

**Mybatis的装饰器模式** * 通过在方法上使用装饰器,可以动态添加方法。 * 可以使用装饰器来控制方法的执行顺序。 * 也可以使用装饰器来动态生成方法。 **Mybatis的模板模式** * 使用模板来定义方法的执行模板。 * 模板可以包含参数,并根据参数的值进行动态绑定。 * 模板可以用于定义方法的执行顺序。 **Mybatis的责任链模式** * 通过使用责任链来控制方法的执行顺序。 *责任链可以包含多个方法,并根据方法的执行顺序进行动态绑定。 * 责任链可以用于定义方法的执行顺序。 **Mybatis的代理模式** * 通过使用代理模式来动态生成方法。 *代理模式可以用于将方法的执行结果注入其他方法中。 * 代理模式可以用于定义方法的执行顺序。 **总结** * Mybatis提供多种装饰器模式、模板模式、责任链模式和代理模式。 *这些模式可以用于动态添加方法、控制方法的执行顺序、定义方法的执行顺序等。 * 使用这些模式可以提升项目的质量,使用代码更容易扩展和维护。

正文

最近在看《通用源码阅读指导书:Mybatis源码详解》,这本书一一介绍了Mybatis中的各个包的功能,同时也涉及讲了一些阅读源码的技巧,还讲了一些源码中涉及的设计模式,这是本篇文章介绍的内容

在多说一点这本书,Mybatis是大部分Java开发者都熟悉的一个框架,通过这本书去学习如何阅读源码非常合适,引用书中的一句话:”通过功能猜测源码要比通过源码猜测功能简单得多“,所以在熟悉这个框架的情况下更容易阅读它的源码,通过这本书,可以看到Mybatis是如何使用反射、代理、异常、插件、缓存、配置、注解、设计模式等

1. 装饰器模式

通常的使用场景是在一个核心基本类的基础上,提供大量的装饰类,从而使核心基本类经过不同的装饰类修饰后获得不同的功能。

1.1 例子

装饰器最经典的例子还是JDK本身的InputStream相关类, InputStream通过不断被装饰,提供的功能越来越多

InputStream in = new FileInputStream("/user/wangzheng/test.txt");
InputStream bin = new BufferedInputStream(in);
byte[] data = new byte[128];
while (bin.read(data) != -1) {
  //...
}

1.2 Mybatis实例

Mybatis的缓存功能大家应该都清楚,分为一级缓存和二级缓存,但讲到缓存,可能就要涉及到缓存大小、过期、是不是阻塞等这些操作,我们可以设计一个功能大而全的类,挑选自己想要的功能就行。但Mybatis不是这样设计的,它设计了多个装饰类,每个类负责一个功能,然后可以按需使用,分别维护。

这些装饰类如下:

  • BlockingCache,阻塞缓存,当根据key获取不到value时,会阻塞等待
  • FifoCache,先进先出缓存,会根据指定的大小淘汰缓存,按照FIFO的方式
  • LoggingCache,日志缓存,会记录缓存的使用情况,命中率等
  • LruCache,最近最少使用缓存,根据指定的大小淘汰缓存,按照LRU的方式
  • ScheduledCache,定时清理缓存
  • SerializedCache,序列化缓存,防止被取出来的value被修改
  • SoftCache,软引用缓存
  • SynchronizedCache,同步缓存,防止并发问题
  • TransactionalCache,事务缓存,在事务中查询语句要放到事务结束后执行,不如会读取事务中的一些脏数据
  • WeakCache,弱引用缓存

PerpetualCache是一个基础的缓存,其实就是一个HashMap

public class PerpetualCache implements Cache {

  // Cache的id,一般为所在的namespace
  private final String id;
  // 用来存储要缓存的信息
  private Map<Object, Object> cache = new HashMap<>();

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

LruCache是一个装饰器缓存,用来装饰传进来的缓存,可以是被装饰过的或者PerpetualCache

public class LruCache implements Cache {

  // 被装饰对象
  private final Cache delegate;
  // 使用LinkedHashMap保存的缓存数据的键
  private Map<Object, Object> keyMap;
  // 最近最少使用的数据的键
  private Object eldestKey;

  /**
   * LruCache构造方法
   * @param delegate 被装饰对象
   */
  public LruCache(Cache delegate) {
    this.delegate = delegate;
    setSize(1024);
  }
  ...
}

在使用阶段,Mybatis根据配置,一层层的给cache进行装饰

  private Cache setStandardDecorators(Cache cache) {
    try {
      MetaObject metaCache = SystemMetaObject.forObject(cache);
      // 设置缓存大小
      if (size != null && metaCache.hasSetter("size")) {
        metaCache.setValue("size", size);
      }
      // 如果定义了清理间隔,则使用定时清理装饰器装饰缓存
      if (clearInterval != null) {
        cache = new ScheduledCache(cache);
        ((ScheduledCache) cache).setClearInterval(clearInterval);
      }
      // 如果允许读写,则使用序列化装饰器装饰缓存
      if (readWrite) {
        cache = new SerializedCache(cache);
      }
      // 使用日志装饰器装饰缓存
      cache = new LoggingCache(cache);
      // 使用同步装饰器装饰缓存
      cache = new SynchronizedCache(cache);
      // 如果启用了阻塞功能,则使用阻塞装饰器装饰缓存
      if (blocking) {
        cache = new BlockingCache(cache);
      }
      // 返回被层层装饰的缓存
      return cache;
    } catch (Exception e) {
      throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
  }

2. 模板模式

在模板模式中,需要使用一个抽象类定义一套操作的整体步骤(即模板),而抽象类的子类则完成每个步骤的具体实现。这样,抽象类的不同子类遵循了同样的一套模板。

2.1 例子

JDK中的AbstractList是大部分List、Queue、Stack的父类,其中的addAll方法使用了模板方法,将具体的add方法交给了子类实现

    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);
        boolean modified = false;
        for (E e : c) {
            add(index++, e);
            modified = true;
        }
        return modified;
    }

    public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }

2.2 Mybatis实例

作为ORM框架,Mybatis会负责将Java对象写为数据库中的字段,或者是反过来。例如,会将Java中的String字段写为varchar,Integer字段写为int

那么,这些字段类型就需要一个映射,Mybatis就是通过不同TypeHandler来处理的,例如IntegerTypeHandler是来处理Integer的

BaseTypeHandler是一个特定类型处理的父类,这个父类中定义的一些模板方法,其中的setParameter定义了写到数据库的模板,统一处理了空值和非空值,getResult定义了从数据库读的模板,统一处理了异常,具体的实现由子类来负责

  @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);
      }
    }
  }
  
  
  @Override
  public T getResult(ResultSet rs, int columnIndex) throws SQLException {
    try {
      return getNullableResult(rs, columnIndex);
    } catch (Exception e) {
      throw new ResultMapException("Error attempting to get column #" + columnIndex + " from result set.  Cause: " + e, e);
    }
  }

	
	
  public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

  /**
   * @param columnName Colunm name, when configuration <code>useColumnLabel</code> is <code>false</code>
   */
  public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException;

  public abstract T getNullableResult(ResultSet rs, int columnIndex) throws SQLException;

  public abstract T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException;


2.3 技巧

  • 类中的模板方法使用final, 避免子类重写它

  • 类中的步骤方法使用abstrat或者抛出异常,强迫子类重写它

3. 责任链模式

3.1 例子

最经典的例子是Servlet的Filter,我们配置了多个Filter,这多个Filter会一个接一个执行,执行的过程中是可以中止的

	@Override
	public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain )
			throws IOException, ServletException{
			doSomething();
			filterChain.doFilter( servletRequest, servletResponse );
	}

3.2 Mybatis实例

Mybatis的插件功能可能没有听说过,但PageHelper一定用过,它就是使用Mybatis的插件来实现的,有兴趣的读者可以看下这篇文章5分钟!彻底搞懂MyBatis插件+PageHelper原理 - 知乎 (zhihu.com)

通过一系列的拦截器插件,会对Mybatis的一些核心类进行增强,责任链模式使它很容易扩展,即是可拔插的

public class InterceptorChain {
    // 拦截器链
    private final List<Interceptor> interceptors = new ArrayList<>();

    // target是支持拦截的几个类的实例。该方法依次向所有拦截器插入这几个类的实例
    // 如果某个插件真的需要发挥作用,则返回一个代理对象即可。如果不需要发挥作用,则返回原对象即可

    /**
     * 向所有的拦截器链提供目标对象,由拦截器链给出替换目标对象的对象
     * @param target 目标对象,是MyBatis中支持拦截的几个类(ParameterHandler、ResultSetHandler、StatementHandler、Executor)的实例
     * @return 用来替换目标对象的对象
     */
    public Object pluginAll(Object target) {
        // 依次交给每个拦截器完成目标对象的替换工作
        for (Interceptor interceptor : interceptors) {
            target = interceptor.plugin(target);
        }
        return target;
    }

    /**
     * 向拦截器链增加一个拦截器
     * @param interceptor 要增加的拦截器
     */
    public void addInterceptor(Interceptor interceptor) {
        interceptors.add(interceptor);
    }

    /**
     * 获取拦截器列表
     * @return 拦截器列表
     */
    public List<Interceptor> getInterceptors() {
        return Collections.unmodifiableList(interceptors);
    }

}

3.3 一些区别

上面的两个责任链其实是有些区别的,Servlet的Filter需要主动调用FiterChain的doFilter方法,来确保继续执行下一个Filter,所以也可以不执行,使流程中止,而Mybatis的插件则不可以中止

4. 代理模式

代理模式(Proxy Pattern)是指建立某一个对象的代理对象,并且由代理对象控制对原对象的引用。

  • 静态代理
  • 动态代理,JDK、CGLIB

4.1 例子

下面是一个JDK动态代理的例子,实现InvocationHandler接口和使用Proxy.newProxyInstance,功能是为控制层增加一个耗时统计的功能

public class MetricsCollectorProxy {
  private MetricsCollector metricsCollector;

  public MetricsCollectorProxy() {
    this.metricsCollector = new MetricsCollector();
  }

  public Object createProxy(Object proxiedObject) {
    Class<?>[] interfaces = proxiedObject.getClass().getInterfaces();
    DynamicProxyHandler handler = new DynamicProxyHandler(proxiedObject);
    return Proxy.newProxyInstance(proxiedObject.getClass().getClassLoader(), interfaces, handler);
  }

  private class DynamicProxyHandler implements InvocationHandler {
    private Object proxiedObject;

    public DynamicProxyHandler(Object proxiedObject) {
      this.proxiedObject = proxiedObject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      long startTimestamp = System.currentTimeMillis();
      Object result = method.invoke(proxiedObject, args);
      long endTimeStamp = System.currentTimeMillis();
      long responseTime = endTimeStamp - startTimestamp;
      String apiName = proxiedObject.getClass().getName() + ":" + method.getName();
      RequestInfo requestInfo = new RequestInfo(apiName, responseTime, startTimestamp);
      metricsCollector.recordRequest(requestInfo);
      return result;
    }
  }
}

//MetricsCollectorProxy使用举例
MetricsCollectorProxy proxy = new MetricsCollectorProxy();
IUserController userController = (IUserController) proxy.createProxy(new UserController());

4.2 Mybatis实例

Mybatis中使用代理的地方很多,我这挑了一个从功能上我们最熟悉的例子

用Mybatis都知道,只需要写一个Mapper接口和xml文件,然后就可以根据Mapper接口的方法来执行xml文件中对应的SQL语句,那这块是怎么实现的呢?其实就是Mybatis自动帮我们生成了一个代理类

下面是一个实现了InvocationHandler的代理类,在invoke方法中,对Object方法和默认方法不处理,其他的方法则使用MapperMethod来处理,MapperMethod其实就是真正执行SQL语句的类,我们的Mapper接口生成了代理类MapperProxy,代理了MapperMethod这个类

public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  // 该Map的键为方法,值为MapperMethod对象。通过该属性,完成了MapperProxy内(即映射接口内)方法和MapperMethod的绑定
  private final Map<Method, MapperMethod> methodCache;

  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 {
      if (Object.class.equals(method.getDeclaringClass())) { // 继承自Object的方法
        // 直接执行原有方法
        return method.invoke(this, args);
      } else if (method.isDefault()) { // 默认方法
        // 执行默认方法
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    // 找对对应的MapperMethod对象
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    // 调用MapperMethod中的execute方法
    return mapperMethod.execute(sqlSession, args);
  }

  private MapperMethod cachedMapperMethod(Method method) {
    return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
  }

  private Object invokeDefaultMethod(Object proxy, Method method, Object[] args)
      throws Throwable {
    final Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class
        .getDeclaredConstructor(Class.class, int.class);
    if (!constructor.isAccessible()) {
      constructor.setAccessible(true);
    }
    final Class<?> declaringClass = method.getDeclaringClass();
    return constructor
        .newInstance(declaringClass,
            MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
                | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC)
        .unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args);
  }

}

下面是生成MapperProxy的工厂,可以看到也是用了Proxy.newProxyInstance生成了代理类

public class MapperProxyFactory<T> {

  ...
  private final Class<T> mapperInterface;
  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }
  
  @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<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

}

这个是MapperRegistry的一个方法,通过这个方法将一个Mapper接口生成一个代理类

  public <T> void addMapper(Class<T> type) {
    // 要加入的肯定是接口,否则不添加
    if (type.isInterface()) {
      // 加入的是接口
      if (hasMapper(type)) {
        // 如果添加重复
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        knownMappers.put(type, new MapperProxyFactory<>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

通过上面这几个类,就把Mapper接口对应的一系列的MapperMethod到上面,至于MapperMethod是怎么和SQL关联上的,有兴趣的读者可以自己去看一下,其实也很简单,根据方法名中xml文件取对应的SQL就可以,但还是涉及很多参数处理的东西

5. 其他模式

除了这些模式之外,Mybatis还用了很多其他的模式,因为也比较简单,所以就没列出来了,例如单例模式、建造者模式、工厂模式等

6. 总结

  • 我们主要介绍了Mybatis的装饰器模式、模板模式、责任链模式、代理模式。

  • 平时我们在学习设计模式的过程中,常常会见到一些和实际工程无关的例子代码,感觉这些设计模式只能用于这些例子,无法用于实际的项目。通过学习Mybatis的实际运用,可以加深我们对设计模式的理解

  • 既然Mybatis可以做到很流行,它的代码必然是有可取之处的,所以运用这些模式到自己的项目中,毫无疑问的会提供项目的质量,使用代码更容易扩展和维护,这也是我们学习设计模式的目的。

与Mybatis中的设计模式相似的内容:

Mybatis中的设计模式

最近在看《通用源码阅读指导书:Mybatis源码详解》,这本书一一介绍了Mybatis中的各个包的功能,同时也涉及讲了一些阅读源码的技巧,还讲了一些源码中涉及的设计模式,这是本篇文章介绍的内容 在多说一点这本书,Mybatis是大部分Java开发者都熟悉的一个框架,通过这本书去学习如何阅读源码非常合

MyBatis-Plus 实现多租户管理的实践

本文主要讲解使用Mybatis-Plus结合dynamic-datasource来实现多租户管理 在现代企业应用中,多租户(Multi-Tenant)架构已经成为一个非常重要的设计模式。多租户架构允许多个租户共享同一应用程序实例,但每个租户的数据彼此隔离。实现这一点可以大大提高资源利用率并降低运营成

『手写Mybatis』实现映射器的注册和使用

前言 如何面对复杂系统的设计? 我们可以把 Spring、MyBatis、Dubbo 这样的大型框架或者一些公司内部的较核心的项目,都可以称为复杂的系统。 这样的工程也不在是初学编程手里的玩具项目,没有所谓的 CRUD,更多时候要面对的都是对系统分层的结构设计和聚合逻辑功能的实现,再通过层层转换进行

实战指南,SpringBoot + Mybatis 如何对接多数据源

本文分享自华为云社区 《实战指南,SpringBoot + Mybatis 如何对接多数据源》,作者:战斧。 在我们开发一些具有综合功能的项目时,往往会碰到一种情况,需要同时连接多个数据库,这个时候就需要用到多数据源的设计。而Spring与Myabtis其实做了多数据源的适配,只需少许改动即可对接多

Mybatis执行器

mybatis执行sql语句的操作是由执行器(Executor)完成的,mybatis中一共提供了3种Executor: 类型 名称 功能 REUSE 重用执行器 缓存PreparedStatement,下一次执行相同的sql可重用 BATCH 批量执行器 将修改操作记录在本地,等待程序触发或有下一

mybaits-plus实现自定义字典转换

需求:字典实现类似mybatis-plus中@EnumValue的功能,假设枚举类中应用使用code,数据库存储对应的value 思路:Mybatis支持对Executor、StatementHandler、PameterHandler和ResultSetHandler进行拦截,也就是说会对这4种对

【java深入学习第1章】深入探究 MyBatis-Spring 中 SqlSession 的原理与应用

前言 在使用 MyBatis 进行持久层开发时,通常会与 Spring 框架集成,以便更好地管理事务和依赖注入。在 MyBatis-Spring 集成中,SqlSession 是一个非常重要的概念。本文将详细介绍 SqlSessionTemplate 和 SqlSessionDaoSupport,并

MyBatis源码之MyBatis中SQL语句执行过程

本文是MyBatis源码之MyBatis中SQL语句执行过程,使用图文并茂的方式,讲解了SQL语句执行过程,调用了哪些方法,和这些方法是如何调用的。

如何规避MyBatis使用过程中带来的全表更新风险

不知大家在使用MyBatis有没有过程人工梳理代码的经理?但由于web应用数量多,代码行数几十万行,人力梳理代码费时又费力。基于此,架构师根据MyBatis的扩展点推出一款插件做到降低全表更新的风险,降低人工成本。

【简写Mybatis-02】注册机的实现以及SqlSession处理

学习源码一定一定不要太关注代码的编写,而是注意代码实现思想:通过设问方式来体现代码中的思想;方法:5W+1H