天天看點

【深入淺出MyBatis系列六】插件原理

#0 系列目錄#

  • 深入淺出MyBatis系列
  • 【深入淺出MyBatis系列一】MyBatis入門
  • 【深入淺出MyBatis系列二】配置簡介(MyBatis源碼篇)
  • 【深入淺出MyBatis系列三】Mapper映射檔案配置
  • 【深入淺出MyBatis系列四】強大的動态SQL
  • 【深入淺出MyBatis系列五】SQL執行流程分析(源碼篇)
  • 【深入淺出MyBatis系列六】插件原理
  • 【深入淺出MyBatis系列七】分頁插件
  • 【深入淺出MyBatis系列八】SQL自動生成插件
  • 【深入淺出MyBatis系列九】改造Cache插件
  • 【深入淺出MyBatis系列十】與Spring內建
  • 【深入淺出MyBatis系列十一】緩存源碼分析
  • 【深入淺出MyBatis系列十二】終結篇:MyBatis原理深入解析

MyBatis提供了一種插件(plugin)的功能,雖然叫做插件,但其實這是攔截器功能。那麼攔截器攔截MyBatis中的哪些内容呢?

MyBatis 允許你在已映射語句執行過程中的某一點進行攔截調用

。預設情況下,MyBatis允許使用插件來攔截的方法調用包括:

  1. Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) 攔截執行器的方法
  2. ParameterHandler (getParameterObject, setParameters) 攔截參數的處理
  3. ResultSetHandler (handleResultSets, handleOutputParameters) 攔截結果集的處理
  4. StatementHandler (prepare, parameterize, batch, update, query) 攔截Sql文法建構的處理

Mybatis采用

責任鍊模式,通過動态代理組織多個攔截器(插件)

,通過這些攔截器可以改變Mybatis的預設行為(諸如SQL重寫之類的),由于插件會深入到Mybatis的核心,是以在編寫自己的插件前最好了解下它的原理,以便寫出安全高效的插件。

#1 攔截器的使用# ##1.1 攔截器介紹及配置## 首先我們看下MyBatis攔截器的接口定義:

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);

}
           

比較簡單,隻有3個方法。 MyBatis預設沒有一個攔截器接口的實作類,開發者們可以實作符合自己需求的攔截器。下面的MyBatis官網的一個攔截器執行個體:

@Intercepts({@Signature(type= Executor.class, method = "update", args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
  public Object intercept(Invocation invocation) throws Throwable {
    return invocation.proceed();
  }
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  public void setProperties(Properties properties) {
  }
}
           

全局xml配置:

<plugins>
    <plugin interceptor="org.format.mybatis.cache.interceptor.ExamplePlugin"></plugin>
</plugins>
           

這個攔截器攔截Executor接口的update方法(其實也就是SqlSession的新增,删除,修改操作),所有執行executor的update方法都會被該攔截器攔截到。 ##1.2 源碼分析## 首先從源頭->配置檔案開始分析:

  1. XMLConfigBuilder

    解析MyBatis全局配置檔案的pluginElement私有方法

private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            String interceptor = child.getStringAttribute("interceptor");
            Properties properties = child.getChildrenAsProperties();
            Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
            interceptorInstance.setProperties(properties);
            configuration.addInterceptor(interceptorInstance);
        }
    }
}
           
  1. 具體的解析代碼其實比較簡單,就不貼了,

    主要就是通過反射執行個體化plugin節點中的interceptor屬性表示的類

    。然後調用全局配置類Configuration的addInterceptor方法。
public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
}
           
  1. 這個interceptorChain是Configuration的内部屬性,

    類型為InterceptorChain,也就是一個攔截器鍊

    ,我們來看下它的定義:
public class InterceptorChain {

    private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

    public Object pluginAll(Object target) {
        for (Interceptor interceptor : interceptors) {
            target = interceptor.plugin(target);
        }
        return target;
    }

    public void addInterceptor(Interceptor interceptor) {
        interceptors.add(interceptor);
    }

    public List<Interceptor> getInterceptors() {
        return Collections.unmodifiableList(interceptors);
    }

}
           
  1. 現在我們了解了攔截器配置的解析以及攔截器的歸屬,

    現在我們回過頭看下為何攔截器會攔截這些方法

    (Executor,ParameterHandler,ResultSetHandler,StatementHandler的部分方法):
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
}

public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {
    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
}

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
}

public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
        executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
        executor = new ReuseExecutor(this, transaction);
    } else {
        executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
        executor = new CachingExecutor(executor, autoCommit);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}
           

以上4個方法都是Configuration的方法。這些方法在MyBatis的一個操作(新增,删除,修改,查詢)中都會被執行到,執行的先後順序是Executor,ParameterHandler,ResultSetHandler,StatementHandler(

其中ParameterHandler和ResultSetHandler的建立是在建立StatementHandler[3個可用的實作類CallableStatementHandler,PreparedStatementHandler,SimpleStatementHandler]的時候

,其構造函數調用的[這3個實作類的構造函數其實都調用了父類BaseStatementHandler的構造函數])。

這4個方法執行個體化了對應的對象之後,都會調用interceptorChain的pluginAll方法,InterceptorChain的pluginAll剛才已經介紹過了,就是周遊所有的攔截器,然後調用各個攔截器的plugin方法。

注意:攔截器的plugin方法的傳回值會直接被指派給原先的對象

由于可以攔截StatementHandler,這個接口主要處理sql文法的建構

,是以比如分頁的功能,可以用攔截器實作,

隻需要在攔截器的plugin方法中處理StatementHandler接口實作類中的sql即可

,可使用反射實作。

MyBatis還提供了

@Intercepts和 @Signature關于攔截器的注解

。官網的例子就是使用了這2個注解,還包括了Plugin類的使用:

@Override
public Object plugin(Object target) {
    return Plugin.wrap(target, this);
}
           

#2 代理鍊的生成#

Mybatis支援對Executor、StatementHandler、ParameterHandler和ResultSetHandler進行攔截

,也就是說會對這4種對象進行代理。通過檢視Configuration類的源代碼我們可以看到,每次都對目标對象進行代理鍊的生成。

下面以Executor為例。Mybatis在建立Executor對象時會執行下面一行代碼:

executor =(Executor) interceptorChain.pluginAll(executor);
           

InterceptorChain裡儲存了所有的攔截器,它在mybatis初始化的時候建立

。上面這句代碼的含義是調用攔截器鍊裡的每個攔截器依次對executor進行plugin(插入?)代碼如下:

/** 
  * 每一個攔截器對目标類都進行一次代理 
  * @param target 
  * @return 層層代理後的對象 
  */  
 public Object pluginAll(Object target) {  
     for(Interceptor interceptor : interceptors) {  
         target= interceptor.plugin(target);  
     }  
     return target;  
 }  
           

下面以一個簡單的例子來看看這個plugin方法裡到底發生了什麼:

@Intercepts({@Signature(type = Executor.class, method ="update", args = {MappedStatement.class, Object.class})})  
public class ExamplePlugin implements Interceptor {  
    @Override  
    public Object intercept(Invocation invocation) throws Throwable {  
        return invocation.proceed();  
    }  
  
    @Override  
    public Object plugin(Object target) {  
        return Plugin.wrap(target, this);  
    }  
  
    @Override  
    public void setProperties(Properties properties) {  
    }
}  
           

每一個攔截器都必須實作上面的三個方法,其中:

  1. Object intercept(Invocation invocation)是

    實作攔截邏輯的地方

    内部要通過invocation.proceed()顯式地推進責任鍊前進

    ,也就是調用下一個攔截器攔截目标方法。
  2. Object plugin(Object target)就是

    用目前這個攔截器生成對目标target的代理

    ,實際是通過Plugin.wrap(target,this)來完成的,把目标target和攔截器this傳給了包裝函數。
  3. setProperties(Properties properties)用于

    設定額外的參數

    ,參數配置在

    攔截器的Properties節點裡

注解裡描述的是指定攔截方法的簽名 [type,method,args] (即對哪種對象的哪種方法進行攔截),它在攔截前用于決斷

定義自己的Interceptor最重要的是要實作plugin方法和intercept方法,在plugin方法中我們可以決定是否要進行攔截進而決定要傳回一個什麼樣的目标對象。而intercept方法就是要進行攔截的時候要執行的方法。

對于plugin方法而言,其實Mybatis已經為我們提供了一個實作。

Mybatis中有一個叫做Plugin的類,裡面有一個靜态方法wrap(Object target,Interceptor interceptor),通過該方法可以決定要傳回的對象是目标對象還是對應的代理

。這裡我們先來看一下Plugin的源碼:

package org.apache.ibatis.plugin;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.apache.ibatis.reflection.ExceptionUtil;

//這個類是Mybatis攔截器的核心,大家可以看到該類繼承了InvocationHandler
//又是JDK動态代理機制
public class Plugin implements InvocationHandler {

  //目标對象
  private Object target;
  //攔截器
  private Interceptor interceptor;
  //記錄需要被攔截的類與方法
  private Map<Class<?>, Set<Method>> signatureMap;

  private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
    this.target = target;
    this.interceptor = interceptor;
    this.signatureMap = signatureMap;
  }

  //一個靜态方法,對一個目标對象進行包裝,生成代理類。
  public static Object wrap(Object target, Interceptor interceptor) {
    //首先根據interceptor上面定義的注解 擷取需要攔截的資訊
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    //目标對象的Class
    Class<?> type = target.getClass();
    //傳回需要攔截的接口資訊
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    //如果長度為>0 則傳回代理類 否則不做處理
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

  //代理對象每次調用的方法
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      //通過method參數定義的類 去signatureMap當中查詢需要攔截的方法集合
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      //判斷是否需要攔截
      if (methods != null && methods.contains(method)) {
        return interceptor.intercept(new Invocation(target, method, args));
      }
      //不攔截 直接通過目标對象調用方法
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

  //根據攔截器接口(Interceptor)實作類上面的注解擷取相關資訊
  private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    //擷取注解資訊
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    //為空則抛出異常
    if (interceptsAnnotation == null) { // issue #251
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());      
    }
    //獲得Signature注解資訊
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
    //循環注解資訊
    for (Signature sig : sigs) {
      //根據Signature注解定義的type資訊去signatureMap當中查詢需要攔截方法的集合
      Set<Method> methods = signatureMap.get(sig.type());
      //第一次肯定為null 就建立一個并放入signatureMap
      if (methods == null) {
        methods = new HashSet<Method>();
        signatureMap.put(sig.type(), methods);
      }
      try {
        //找到sig.type當中定義的方法 并加入到集合
        Method method = sig.type().getMethod(sig.method(), sig.args());
        methods.add(method);
      } catch (NoSuchMethodException e) {
        throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
      }
    }
    return signatureMap;
  }

  //根據對象類型與signatureMap擷取接口資訊
  private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<Class<?>>();
    //循環type類型的接口資訊 如果該類型存在與signatureMap當中則加入到set當中去
    while (type != null) {
      for (Class<?> c : type.getInterfaces()) {
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    //轉換為數組傳回
    return interfaces.toArray(new Class<?>[interfaces.size()]);
  }

}
           

下面是倆個注解類的定義源碼:

package org.apache.ibatis.plugin;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
  Signature[] value();
}
           
package org.apache.ibatis.plugin;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Signature {
  Class<?> type();

  String method();

  Class<?>[] args();
}
           

#3 Plugin.wrap方法# 從前面可以看出,每個攔截器的plugin方法是通過調用Plugin.wrap方法來實作的。代碼如下:

public static Object wrap(Object target, Interceptor interceptor) {  
   // 從攔截器的注解中擷取攔截的類名和方法資訊  
   Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);  
   Class<?> type = target.getClass();  
   // 解析被攔截對象的所有接口(注意是接口)  
   Class<?>[] interfaces = getAllInterfaces(type, signatureMap);  
   if(interfaces.length > 0) {  
        // 生成代理對象, Plugin對象為該代理對象的InvocationHandler  (InvocationHandler屬于java代理的一個重要概念,不熟悉的請參考相關概念)  
        return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target,interceptor,signatureMap));  
    }  
    return target;  
}
           

這個Plugin類有三個屬性:

private Object target;// 被代理的目标類

private Interceptor interceptor;// 對應的攔截器

private Map<Class<?>, Set<Method>> signatureMap;// 攔截器攔截的方法緩存

getSignatureMap方法:

private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    if (interceptsAnnotation == null) { // issue #251
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());      
    }
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
    for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.get(sig.type());
      if (methods == null) {
        methods = new HashSet<Method>();
        signatureMap.put(sig.type(), methods);
      }
      try {
        Method method = sig.type().getMethod(sig.method(), sig.args());
        methods.add(method);
      } catch (NoSuchMethodException e) {
        throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
      }
    }
    return signatureMap;
}
           

**

getSignatureMap方法解釋:

**首先會拿到攔截器這個類的 @Interceptors注解,然後拿到這個注解的屬性 @Signature注解集合,然後周遊這個集合,周遊的時候拿出 @Signature注解的type屬性(Class類型),然後根據這個type得到帶有method屬性和args屬性的Method。由于 @Interceptors注解的 @Signature屬性是一個屬性,是以最終會傳回一個以type為key,value為Set<Method>的Map。

@Intercepts({@Signature(type= Executor.class, method = "update", args = {MappedStatement.class,Object.class})})
           

比如這個 @Interceptors注解會傳回一個key為Executor,value為集合(這個集合隻有一個元素,也就是Method執行個體,這個Method執行個體就是Executor接口的update方法,且這個方法帶有MappedStatement和Object類型的參數)。這個Method執行個體是根據 @Signature的method和args屬性得到的。如果args參數跟type類型的method方法對應不上,那麼将會抛出異常。

getAllInterfaces方法:

private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<Class<?>>();
    while (type != null) {
      for (Class<?> c : type.getInterfaces()) {
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    return interfaces.toArray(new Class<?>[interfaces.size()]);
}
           

**

getAllInterfaces方法解釋:

**根據目标執行個體target(這個target就是之前所說的MyBatis攔截器可以攔截的類,Executor,ParameterHandler,ResultSetHandler,StatementHandler)和它的父類們,傳回signatureMap中含有target實作的接口數組。

是以Plugin這個類的作用就是根據 @Interceptors注解,得到這個注解的屬性 @Signature數組,然後根據每個 @Signature注解的type,method,args屬性使用反射找到對應的Method。最終根據調用的target對象實作的接口決定是否傳回一個代理對象替代原先的target對象。

我們再次結合(Executor)interceptorChain.pluginAll(executor)這個語句來看,這個語句内部對executor執行了多次plugin,第一次plugin後通過Plugin.wrap方法生成了第一個代理類,姑且就叫executorProxy1,這個代理類的target屬性是該executor對象。第二次plugin後通過Plugin.wrap方法生成了第二個代理類,姑且叫executorProxy2,這個代理類的target屬性是executorProxy1...這樣通過每個代理類的target屬性就構成了一個代理鍊(

從最後一個executorProxyN往前查找,通過target屬性可以找到最原始的executor類

)。

#4 代理鍊上的攔截# 代理鍊生成後,

對原始目标的方法調用都轉移到代理者的invoke方法上來了

。Plugin作為InvocationHandler的實作類,他的invoke方法是怎麼樣的呢?

比如MyBatis官網的例子,當Configuration調用newExecutor方法的時候,由于Executor接口的update(MappedStatement ms, Object parameter)方法被攔截器被截獲。是以最終傳回的是一個代理類Plugin,而不是Executor。這樣調用方法的時候,如果是個代理類,那麼會執行:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  
    try {  
       Set<Method> methods = signatureMap.get(method.getDeclaringClass());  
        if(methods != null && methods.contains(method)) {  
           // 調用代理類所屬攔截器的intercept方法,  
           return interceptor.intercept(new Invocation(target, method, args));  
        }  
        return method.invoke(target, args);  
    } catch(Exception e) {  
        throw ExceptionUtil.unwrapThrowable(e);  
    }  
}
           

沒錯,如果找到對應的方法被代理之後,那麼會執行Interceptor接口的interceptor方法。

在invoke裡,如果方法簽名和攔截中的簽名一緻,就調用攔截器的攔截方法

。我們看到傳遞給攔截器的是一個Invocation對象,這個對象是什麼樣子的,他的功能又是什麼呢?

public class Invocation {  
  
    private Object target;  
    private Method method;  
    private Object[] args;  
   
    public Invocation(Object target, Method method, Object[] args) {  
        this.target =target;  
        this.method =method;  
        this.args =args;  
    }  
    ...  
  
    public Object proceed() throws InvocationTargetException, IllegalAccessException {  
        return method.invoke(target, args);  
    }  
} 
           

可以看到,Invocation類儲存了代理對象的目标類,執行的目标類方法以及傳遞給它的參數。

在每個攔截器的intercept方法内,

最後一個語句一定是return invocation.proceed()

(不這麼做的話攔截器鍊就斷了,你的mybatis基本上就不能正常工作了)。

invocation.proceed()隻是簡單的調用了下target的對應方法,如果target還是個代理,就又回到了上面的Plugin.invoke方法了

。這樣就形成了攔截器的調用鍊推進。

public Object intercept(Invocation invocation) throws Throwable {  
    //完成代理類本身的邏輯  
    ...
    //通過invocation.proceed()方法完成調用鍊的推進
    return invocation.proceed();
}
           

#5 總結# MyBatis攔截器接口提供的3個方法中,plugin方法用于某些處理器(Handler)的建構過程。interceptor方法用于處理代理類的執行。setProperties方法用于攔截器屬性的設定。

其實MyBatis官網提供的使用 @Interceptors和 @Signature注解以及Plugin類這樣處理攔截器的方法,我們不一定要直接這樣使用。我們也可以抛棄這3個類,直接在plugin方法内部根據target執行個體的類型做相應的操作。

總體來說MyBatis攔截器還是很簡單的,攔截器本身不需要太多的知識點,但是學習攔截器需要對MyBatis中的各個接口很熟悉,因為攔截器涉及到了各個接口的知識點。

我們假設在MyBatis配置了一個插件,在運作時會發生什麼?

  1. 所有可能被攔截的處理類都會生成一個代理
  2. 處理類代理在執行對應方法時,判斷要不要執行插件中的攔截方法
  3. 執行插接中的攔截方法後,推進目标的執行

如果有N個插件,就有N個代理,每個代理都要執行上面的邏輯。這裡面的層層代理要多次生成動态代理,是比較影響性能的。雖然能指定插件攔截的位置,但這個是在執行方法時動态判斷,初始化的時候就是簡單的把插件包裝到了所有可以攔截的地方。

是以,在編寫插件時需注意以下幾個原則:

  1. 不編寫不必要的插件;
  2. 實作plugin方法時判斷一下目标類型,是本插件要攔截的對象才執行Plugin.wrap方法,否者直接傳回目标本省,這樣可以減少目标被代理的次數。

版權聲明:本文為CSDN部落客「weixin_34029680」的原創文章,遵循CC 4.0 BY-SA版權協定,轉載請附上原文出處連結及本聲明。

原文連結:https://blog.csdn.net/weixin_34029680/article/details/91531982