Mybatis中的設計模式

songtianer發表於2023-09-28

最近在看《通用原始碼閱讀指導書: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可以做到很流行,它的程式碼必然是有可取之處的,所以運用這些模式到自己的專案中,毫無疑問的會提供專案的質量,使用程式碼更容易擴充套件和維護,這也是我們學習設計模式的目的。

相關文章