1. 程式人生 > >dubbo2.0原始碼中的設計模式與SPI介紹

dubbo2.0原始碼中的設計模式與SPI介紹

Dubbo原始碼包介紹

當我們從github把Dubbo原始碼下載下來之後有如下原始碼包
這裡寫圖片描述
下面來說明每個包的作用,以便我們有目的的閱讀程式碼
dubbo-admin
dubbo管理平臺原始碼包,用來管理dubbo服務的啟動、禁用、降權、介面測試等,操作介面如下
這裡寫圖片描述
dubbo-cluster
叢集模組,將多個服務提供方偽裝為一個提供方,包括:負載均衡, 容錯,路由等,叢集的地址列表可以是靜態配置的,也可以是由註冊中心下發。
dubbo-common
公共邏輯模組,包括Util類和通用模型
dubbo-config
配置模組,是Dubbo對外的API,使用者通過Config使用Dubbo,隱藏Dubbo所有細節
例如:

<beanid=“xxxService”class=“com.xxx.XxxServiceImpl” /> <!-- 和本地服務一樣實現遠端服務 -->
<dubbo:serviceinterface=“com.xxx.XxxService”ref=“xxxService” /> <!-- 增加暴露遠端服務配置 -->
  • 1
  • 2

dubbo-container
容器模組,是一個Standlone的容器,以簡單的Main載入Spring啟動,因為服務通常不需要Tomcat/JBoss等Web容器的特性,沒必要用Web容器去載入服務
dubbo-demo


dubbo服務provider和consumer例子
dubbo-filter
dubbo擴充套件過濾器功能,針對dubbo介面,類似Spring的AOP,經常用的併發控制、超時設定、異常設定、tps限流設定等等都是通過filter來實現的。
dubbo-monitor
監控模組,統計服務呼叫次數,呼叫時間的,呼叫鏈跟蹤的服務
dubbo-registry
註冊中心模組,基於註冊中心下發地址的叢集方式,以及對各種註冊中心的抽象。
dubbo-remoting
遠端通訊模組,相當於Dubbo協議的實現,如果RPC用RMI協議則不需要使用此包
dubbo-rpc
遠端呼叫模組,抽象各種協議,以及動態代理,只包含一對一的呼叫,不關心叢集的管理

dubbo用到的設計模式

工廠模式

Java SPI

在我們分析工廠模式之前我們先了解下Java SPI(service provice interface)。SPI是JDK內建的一種服務發現機制。目前有不少框架用它來做服務的擴充套件發現, 簡單來說,它就是一種動態替換髮現的機制, 舉個例子來說, 有個介面,想執行時動態的給它新增實現,你只需要新增一個實現。
這裡寫圖片描述
而後把新加的實現描述給JDK即可,Dubbo框架就是基於SPI機制提供擴充套件功能。
我們看下檔案目錄
這裡寫圖片描述
各個類的原始碼如下:

package com.swk.spi;
public interfaceHelloInterface {
    public void sayHello();
}
package com.swk.spi.impl;
import com.swk.spi.HelloInterface;
public classImageHelloimplementsHelloInterface{
    @Override
    public void sayHello() {
        System.out.println("image hello");
    }
}
package com.swk.spi.impl;
import com.swk.spi.HelloInterface;
public classTextHelloimplementsHelloInterface{
    @Override
    public void sayHello() {
        System.out.println("text hello");
    }
}
package com.swk.spi;
import java.util.ServiceLoader;
public classSPIMain {

    public static void main(String[] args) {
        ServiceLoader<HelloInterface> loaders = ServiceLoader.load(HelloInterface.class);
        for(HelloInterface in:loaders){
            in.sayHello();
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

com.swk.spi.HelloInterface檔案內容如下,新增一個實現類的全名就可以呼叫這個類裡面的方法

com.swk.spi.impl.TextHello
com.swk.spi.impl.ImageHello
  • 1
  • 2

執行結果如下

text hello
image hello
  • 1
  • 2

下面我們看看dubbo的實現,dubbo的擴充套件機制和java的SPI機制非常相似,但是又增加了如下功能:
1、可以方便的獲取某一個想要的擴充套件實現,java的SPI機制就沒有提供這樣的功能
2、對於擴充套件實現IOC依賴注入功能:
現在實現者A1含有setB()方法,會自動注入一個介面B的實現者,此時注入B1還是B2呢?都不是,而是注入一個動態生成的介面B的實現者B$Adpative,該實現者能夠根據引數的不同,自動引用B1或者B2來完成相應的功能
3、對擴展采用裝飾器模式進行功能增強,類似AOP實現的功能

private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
  • 1

我們以Protocal為例來分析下

@SPI("dubbo")
public interfaceProtocol {

    /**
     * 獲取預設埠,當用戶沒有配置埠時使用。
     * 
     * @return 預設埠
     */
    int getDefaultPort();
    /**
     * 暴露遠端服務:<br>
     * 1. 協議在接收請求時,應記錄請求來源方地址資訊:RpcContext.getContext().setRemoteAddress();<br>
     * 2. export()必須是冪等的,也就是暴露同一個URL的Invoker兩次,和暴露一次沒有區別。<br>
     * 3. export()傳入的Invoker由框架實現並傳入,協議不需要關心。<br>
     * 
     * @param <T> 服務的型別
     * @param invoker 服務的執行體
     * @return exporter 暴露服務的引用,用於取消暴露
     * @throws RpcException 當暴露服務出錯時丟擲,比如埠已佔用
     */
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
    /**
     * 引用遠端服務:<br>
     * 1. 當用戶呼叫refer()所返回的Invoker物件的invoke()方法時,協議需相應執行同URL遠端export()傳入的Invoker物件的invoke()方法。<br>
     * 2. refer()返回的Invoker由協議實現,協議通常需要在此Invoker中傳送遠端請求。<br>
     * 3. 當url中有設定check=false時,連線失敗不能丟擲異常,並內部自動恢復。<br>
     * 
     * @param <T> 服務的型別
     * @param type 服務的型別
     * @param url 遠端服務的URL地址
     * @return invoker 服務的本地代理
     * @throws RpcException 當連線服務提供方失敗時丟擲
     */
    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
    /**
     * 釋放協議:<br>
     * 1. 取消該協議所有已經暴露和引用的服務。<br>
     * 2. 釋放協議所佔用的所有資源,比如連線和埠。<br>
     * 3. 協議在釋放後,依然能暴露和引用新的服務。<br>
     */
    void destroy();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

Protocol的實現類有
這裡寫圖片描述
ExtensionLoader中含有一個靜態屬性,用於快取所有的擴充套件載入例項,這裡載入Protocol.class,就以Protocol.class為key,建立的ExtensionLoader為value儲存到上述EXTENSION_LOADERS中

public class ExtensionLoader<T> {
    private static final Logger logger = LoggerFactory.getLogger(ExtensionLoader.class);

    private static final String SERVICES_DIRECTORY = "META-INF/services/";
    private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";

    private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";
    private static final Pattern NAME_SEPARATOR = Pattern.compile("\\s*[,]+\\s*");

    private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();
    private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<Class<?>, Object>();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

我們來看下,ExtensionLoader例項是如何來載入Protocol的實現類的:
1、先解析Protocol上的Extension註解的name,存至String cachedDefaultName屬性中,作為預設的實現
2、到類路徑下的載入 META-INF/services/com.alibaba.dubbo.rpc.Protocol檔案
這裡寫圖片描述
檔案內容如下

filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=com.alibaba.dubbo.rpc.support.MockProtocol
  • 1
  • 2
  • 3

這裡的

 private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
 private static final ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();
  • 1
  • 2

我們可以理解為一種工廠模式,這種實現方式的優點是可擴充套件性強,想要擴充套件實現,只需要在 classpath 下增加個檔案就可以了,程式碼零侵入。另外,像上面的 Adaptive 實現,可以做到呼叫時動態決定呼叫哪個實現,但是由於這種實現採用了動態代理,會造成程式碼除錯比較麻煩,需要分析出實際呼叫的實現類。

裝飾器模式與責任鏈模式

裝飾器模式(Decorator Pattern)允許向一個現有的物件新增新的功能,同時又不改變其結構
責任鏈模式(Chain of Responsibility Pattern)為請求建立了一個接收者物件的鏈。這種模式給予請求的型別對請求的傳送者和接收者進行解耦
在我們介紹工廠模式的時候我們也提到了裝飾器模式,Dubbo 在啟動和呼叫階段都大量使用了裝飾器模式。
以 Provider 提供的呼叫鏈為例,具體的呼叫鏈程式碼是在 ProtocolFilterWrapper 的 buildInvokerChain 完成的,具體是將註解中含
有 group=provider 的 Filter 實現,按照 order 排序,最後的呼叫順序是檢視文字列印
EchoFilter-》 ClassLoaderFilter-》 GenericFilter-》 ContextFilter-》 ExceptionFilter-》TimeoutFilter-》 MonitorFilter-》 TraceFilter。更確切地說,這裡是裝飾器和責任鏈模式的混合使用。例如, EchoFilter 的作用是判斷是否是回聲測試請求,是的話直接返回內容,這是一種責任鏈的體現,原始碼如下:

/**
 * EchoInvokerFilter
 * 
 * @author william.liangf
 */
@Activate(group = Constants.PROVIDER, order = -110000)
public classEchoFilterimplementsFilter {
    public Result invoke(Invoker<?> invoker, Invocation inv) throws RpcException {
        // 是否是回聲測試請求
        if(inv.getMethodName().equals(Constants.$ECHO) && inv.getArguments() != null && inv.getArguments().length == 1 )
            return new RpcResult(inv.getArguments()[0]);
        return invoker.invoke(inv);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

而像 ClassLoaderFilter則只是在主功能上添加了功能,更改當前執行緒的 ClassLoader,這是典型的裝飾器模式,原始碼如下

/**
 * ClassLoaderInvokerFilter
 * 
 * @author william.liangf
 */
@Activate(group = Constants.PROVIDER, order = -30000)
public classClassLoaderFilterimplementsFilter {
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        ClassLoader ocl = Thread.currentThread().getContextClassLoader();
        // 在主功能上添加了功能,更改當前執行緒的 ClassLoader
        Thread.currentThread().setContextClassLoader(invoker.getInterface().getClassLoader());
        try {
            return invoker.invoke(invocation);
        } finally {
            Thread.currentThread().setContextClassLoader(ocl);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

觀察者模式

當一個物件被修改時,則會自動通知它的依賴物件
在dubbo provider服務啟動時候要向註冊中心註冊自己的服務,在dubbo consumer向註冊中心訂閱服務時則是一種觀察者模式,他開啟了一個listener,註冊中心會每 5 秒定時檢查是否有服務更新,如果有更新,向該服務的提供者傳送一個 notify 訊息, provider 接受到 notify 訊息後,即執行 NotifyListener 的 notify 方法,執行監聽器方法。

public interfaceNotifyListener {
    /**
     * 當收到服務變更通知時觸發。
     * 
     * 通知需處理契約:<br>
     * 1. 總是以服務介面和資料型別為維度全量通知,即不會通知一個服務的同類型的部分資料,使用者不需要對比上一次通知結果。<br>
     * 2. 訂閱時的第一次通知,必須是一個服務的所有型別資料的全量通知。<br>
     * 3. 中途變更時,允許不同型別的資料分開通知,比如:providers, consumers, routers, overrides,允許只通知其中一種型別,但該型別的資料必須是全量的,不是增量的。<br>
     * 4. 如果一種型別的資料為空,需通知一個empty協議並帶category引數的標識性URL資料。<br>
     * 5. 通知者(即註冊中心實現)需保證通知的順序,比如:單執行緒推送,佇列序列化,帶版本對比。<br>
     * 
     * @param urls 已註冊資訊列表,總不為空,含義同{@link com.alibaba.dubbo.registry.RegistryService#lookup(URL)}的返回值。
     */
    void notify(List<URL> urls);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

動態代理模式

在代理模式(Proxy Pattern)中,一個類代表另一個類的功能
Dubbo 擴充套件 jdk spi 的類 ExtensionLoader 的 Adaptive 實現是典型的動態代理實現。 Dubbo需要靈活地控制實現類,即在呼叫階段動態地根據引數決定呼叫哪個實現類,所以採用先生成代理類的方法,能夠做到靈活的呼叫。生成代理類的程式碼是 ExtensionLoader 的createAdaptiveExtensionClassCode 方法。代理類的主要邏輯是,獲取 URL 引數中指定引數的值作為獲取實現類的 key。

 /**
     * 根據URL傳參獲取指定實現類的key
     * 2017年6月4日 上午10:20:25
     * @return
     */
    private String createAdaptiveExtensionClassCode() {
        StringBuilder codeBuidler = new StringBuilder();
        // 根據反射獲取方法集合
        Method[] methods = type.getMethods();
        boolean hasAdaptiveAnnotation = false;
        for(Method m : methods) {
            // 判斷每個方法是否使用了Adaptive註解
            if(m.isAnnotationPresent(Adaptive.class)) {
                hasAdaptiveAnnotation = true;
                break;
            }
        }
        // 完全沒有Adaptive方法,則不需要生成Adaptive類
        if(! hasAdaptiveAnnotation)
            throw new IllegalStateException("No adaptive method on extension " + type.getName() + ", refuse to create the adaptive class!");

        codeBuidler.append("package " + type.getPackage().getName() + ";");
        codeBuidler.append("\nimport " + ExtensionLoader.class.getName() + ";");
        codeBuidler.append("\npublic class " + type.getSimpleName() + "$Adpative" + " implements " + type.getCanonicalName() + " {");

        for (Method method : methods) {
            // 獲取方法返回值型別
            Class<?> rt = method.getReturnType();
            // 獲取方法引數型別
            Class<?>[] pts = method.getParameterTypes();
            // 獲取方法異常型別
            Class<?>[] ets = method.getExceptionTypes();
            // 獲取Adaptive的註解資訊
            Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
            StringBuilder code = new StringBuilder(512);
            if (adaptiveAnnotation == null) {
                code.append("throw new UnsupportedOperationException(\"method ")
                        .append(method.toString()).append(" of interface ")
                        .append(type.getName()).append(" is not adaptive method!\");");
            } else {
                int urlTypeIndex = -1;
                for (int i = 0; i < pts.length; ++i) {
                    // 如果是URL型別
                    if (pts[i].equals(URL.class)) {
                        urlTypeIndex = i;
                        break;
                    }
                }
                // 有型別為URL的引數
                if (urlTypeIndex != -1) {
                    // Null Point check
                    String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"url == null\");",
                                    urlTypeIndex);
                    code.append(s);

                    s = String.format("\n%s url = arg%d;", URL.class.getName(), urlTypeIndex); 
                    code.append(s);
                }
                // 引數沒有URL型別
                else {
                    String attribMethod = null;

                    // 找到引數的URL屬性
                    LBL_PTS:
                    for (int i = 0; i < pts.length; ++i) {
                        Method[] ms = pts[i].getMethods();
                        for (Method m : ms) {
                            String name = m.getName();
                            if ((name.startsWith("get") || name.length() > 3)
                                    && Modifier.isPublic(m.getModifiers())
                                    && !Modifier.isStatic(m.getModifiers())
                                    && m.getParameterTypes().length == 0
                                    && m.getReturnType() == URL.class) {
                                urlTypeIndex = i;
                                attribMethod = name;
                                break LBL_PTS;
                            }
                        }
                    }
                    if(attribMethod == null) {
                        throw new IllegalStateException("fail to create adative class for interface " + type.getName()
                                + ": not found url parameter or url attribute in parameters of method " + method.getName());
                    }

                    // Null point check
                    String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"%s argument == null\");",
                                    urlTypeIndex, pts[urlTypeIndex].getName());
                    code.append(s);
                    s = String.format("\nif (arg%d.%s() == null) throw new IllegalArgumentException(\"%s argument %s() == null\");",
                                    urlTypeIndex, attribMethod, pts[urlTypeIndex].getName(), attribMethod);
                    code.append(s);
                    s = String.format("%s url = arg%d.%s();",URL.class.getName(), urlTypeIndex, attribMethod); 
                    code.append(s);
                }

                String[] value = adaptiveAnnotation.value();
                // 沒有設定Key,則使用“擴充套件點介面名的點分隔 作為Key
                if(value.length == 0) {
                    char[] charArray = type.getSimpleName().toCharArray();
                    StringBuilder sb = new StringBuilder(128);
                    for (int i = 0; i < charArray.length; i++) {
                        if(Character.isUpperCase(charArray[i])) {
                            if(i != 0) {
                                sb.append(".");
                            }
                            sb.append(Character.toLowerCase(charArray[i]));
                        }
                        else {
                            sb.append(charArray[i]);
                        }
                    }
                    value = new String[] {sb.toString()};
                }

                boolean hasInvocation = false;
                for (int i = 0; i < pts.length; ++i) {
                    if (pts[i].getName().equals("com.alibaba.dubbo.rpc.Invocation")) {
                        // Null Point check
                        String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"invocation == null\");", i);
                        code.append(s);
                        s = String.format("\nString methodName = arg%d.getMethodName();", i); 
                        code.append(s);
                        hasInvocation = true;
                        break;
                    }
                }

                String defaultExtName = cachedDefaultName;
                String getNameCode = null;
                for (int i = value.length - 1; i >= 0; --i) {
                    if(i == value.length - 1) {
                        if(null != defaultExtName) {
                            if(!"protocol".equals(value[i]))
                                if (hasInvocation) 
                                    getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                                else
                                    getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName);
                            else
                                getNameCode = String.format("( url.getProtocol() == null ? \"%s\" : url.getProtocol() )", defaultExtName);
                        }
                        else {
                            if(!"protocol".equals(value[i]))
                                if (hasInvocation) 
                                    getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                                else
                                    getNameCode = String.format("url.getParameter(\"%s\")", value[i]);
                            else
                                getNameCode = "url.getProtocol()";
                        }
                    }
                    else {
                        if(!"protocol".equals(value[i]))
                            if (hasInvocation) 
                                getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                            else
                                getNameCode = String.format("url.getParameter(\"%s\", %s)", value[i], getNameCode);
                        else
                            getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", getNameCode);
                    }
                }
                code.append("\nString extName = ").append(getNameCode).append(";");
                // check extName == null?
                String s = String.format("\nif(extName == null) " +
                        "throw new IllegalStateException(\"Fail to get extension(%s) name from url(\" + url.toString() + \") use keys(%s)\");",
                        type.getName(), Arrays.toString(value));
                code.append(s);

                s = String.format("\n%s extension = (%<s)%s.getExtensionLoader(%s.class).getExtension(extName);",
                        type.getName(), ExtensionLoader.class.getSimpleName(), type.getName());
                code.append(s);

                // return statement
                if (!rt.equals(void.class)) {
                    code.append("\nreturn ");
                }
                s = String.format("extension.%s(", method.getName());
                code.append(s);
                for (int i = 0; i < pts.length; i++) {
                    if (i != 0)
                        code.append(", ");
                    code.append("arg").append(i);
                }
                code.append(");");
            }

            codeBuidler.append("\npublic " + rt.getCanonicalName() + " " + method.getName() + "(");
            for (int i = 0; i < pts.length; i ++) {
                if (i > 0) {
                    codeBuidler.append(", ");
                }
                codeBuidler.append(pts[i].getCanonicalName());
                codeBuidler.append(" ");
                codeBuidler.append("arg" + i);
            }
            codeBuidler.append(")");
            if (ets.length > 0) {
                codeBuidler.append(" throws ");
                for (int i = 0; i < ets.length; i ++) {
                    if (i > 0) {
                        codeBuidler.append(", ");
                    }
                    codeBuidler.append(ets[i].getCanonicalName());
                }
            }
            codeBuidler.append(" {");
            codeBuidler.append(code.toString());
            codeBuidler.append("\n}");
        }
        codeBuidler.append("\n}");
        if (logger.isDebugEnabled()) {
            logger.debug(codeBuidler.toString());
        }
        return codeBuidler.toString();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214