首頁 > 軟體

Java類載入器與雙親委派機制和執行緒上下文類載入器專項解讀分析

2022-12-23 14:00:37

一、類載入器

類載入器就是根據類的二進位制名(binary name)讀取java編譯器編譯好的位元組碼檔案(.class檔案),並且轉化生成一個java.lang.Class類的一個範例。

每個範例用來表示一個Java類,jvm就是用這些範例來生成java物件的。

如new一個String物件;反射生成一個String物件,都會用到String.class 這個java.lang.Class類的物件。

基本上所有的類載入器都是java.lang.ClassLoader 類的一個範例

類載入器3大分類

類載入器載入類說明
啟動類載入器(Bootstrap ClassLoader)JAVA_HOME/jre/lib無法直接存取
拓展類載入器(Extension ClassLoader)JAVA_HOME/jre/lib/ext上級為 Bootstrap,顯示為 null
應用類載入器(Application ClassLoader)classpath上級為 Extension
自定義類載入器自定義上級為 Application

類載入器載入順序如下

類載入器的核心方法

方法名說明
getParent()返回該類載入器的父類別載入器
loadClass(String name)載入名為name的類,返回java.lang.Class類的範例
findClass(String name)查詢名字為name的類,返回的結果是java.lang.Class類的範例
findLoadedClass(String name)查詢名字為name的已經被載入過的類,返回的結果是java.lang.Class類的範例
defineClass(String name,byte[] b,int off,int len)根據位元組陣列b中的資料轉化成Java類,返回的結果是java.lang.Class類的範例

上述方法的name引數都是binary name(類的二進位制名字),如

java.lang.String <包名>.<類名>

java.concurrent.locks.AbstractQueuedSynchronizer$Node <包名>.<類名>$<內部類名>

java.net.URLClassLoader$1 <包名>.<類名>.<匿名內部類名>

1.啟動類載入器

啟動類載入器是jvm在執行時,內嵌在jvm中的一段特殊的用來載入java核心類庫的C++程式碼

String.class 物件就是由啟動類載入器載入的,啟動類載入器具體載入哪些核心程式碼可以通過獲取值為 "sun.boot.class.path" 的系統屬性獲得。

啟動類載入器不是java原生程式碼編寫的,所以其也不是java.lang.ClassLoader類的範例,其沒有getParent方法

public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("com.mycompany.load.F");
        System.out.println(aClass.getClassLoader()); // AppClassLoader  ExtClassLoader
    }

輸出

cd D:JavaJavaProjectjvm-demomyjvmoutproductionmyjvm
D:JavaJavaProjectjvm-demomyjvmoutproductionmyjvm>java -Xbootclasspath/a:. com.mycompany.load.Load4
bootstrap F init
null

列印 null,表示它的類載入器是 Bootstrap ClassLoader

-Xbootclasspath 表示設定 bootclasspath

其中 /a:. 表示將當前目錄追加至 bootclasspath 之後

用這個辦法替換核心類

java -Xbootclasspath:<new bootclasspath>

java -Xbootclasspath/a:<後追加路徑>

java -Xbootclasspath/p:<前追加路徑>

2.拓展類載入器

拓展類載入器用來載入jvm實現的一個拓展目錄,該目錄下的所有java類都由此類載入器載入。

此路徑可以通過獲取"java.ext.dirs"的系統屬性獲得。拓展類載入器就是java.lang.ClassLoader類的一個範例,其getParent方法返回的是引導類載入器(在 HotSpot虛擬機器器中用null表示引導類載入)

D:JavaJavaProjectjvm-demomyjvmoutproductionmyjvm>jar -cvf my.jar com/mycompany/load/F.class
已新增清單
正在新增: com/mycompany/load/F.class(輸入 = 481) (輸出 = 322)(壓縮了 33%)

將 jar 包拷貝到 JAVA_HOME/jre/lib/ext,重新執行程式碼即可

3.應用類載入器

應用類載入器又稱為系統類載入器,開發者可用通過 java.lang.ClassLoader.getSystemClassLoader()方法獲得此類載入器的範例,系統類載入器也因此得名。其主要負責載入程式開發者自己編寫的java類

一般來說,java應用都是用此類載入器完成載入的,可以通過獲取"java.class.path"的系統屬性(也就是我們常說的classpath)來獲取應用類載入器載入的類路徑。應用類載入器是java.lang.ClassLoader類的一個範例,其getParent方法返回的是拓展類載入器

4.類的名稱空間

在程式執行過程中,一個類並不是簡單由其二進位制名字(binary name)定義的,而是通過其二進位制名和其定義載入器所確定的名稱空間(run-time package)所共同確定的。

同一個二進位制名的類由不同的定義載入器載入時,其返回的Class物件不是同一個,那麼由不同的Class物件所建立的物件,其型別也不是相同的。

類似Test cannot be cast to Test的java.lang.ClassCastException 的奇怪錯誤很多情況下都是類的二進位制名相同,而定義載入器不同造成的

package com.mycompany.load;
import sun.misc.Launcher;
public class Load6 {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        ClassLoader classLoader = new Launcher().getClassLoader(); //1 new一個新的類載入器
        System.out.println(classLoader);
        /*
        這是因為 1處獲取的應用類載入器a和jvm用來載入Load6.class物件的應用類載入器b不是同一個範例,
        那麼構成這兩個類的run-time package也就是不同的。所以即使它們的二進位制名字相同,
        但是由a定義的Load6類所建立的物件顯然不能轉化為由b定義的Load6類的範例。
        這種情況下jvm就會丟擲ClassCastException
        * */
        Class<?> aClass = classLoader.loadClass("com.mycompany.load.Load6");
        Load6 load6  = (Load6)aClass.newInstance(); //2
        //Exception in thread "main" java.lang.ClassCastException: com.mycompany.load.Load6 cannot be cast to com.mycompany.load.Load6
    }
}

報出異常:

java.lang.ClassCastException: com.mycompany.load.Load6 cannot be cast to com.mycompany.load.Load6

這是因為 1處獲取的應用類載入器a和jvm用來載入Load6.class物件的應用類載入器b不是同一個範例, 那麼構成這兩個類的run-time package也就是不同的。

所以即使它們的二進位制名字相同, 但是由a定義的Load6類所建立的物件顯然不能轉化為由b定義的Load6類的範例。 這種情況下jvm就會丟擲ClassCastException

相同二進位制名字的類,如果其定義載入器不同,也算是不同的兩個類

二、雙親委派機制

雙親委派機制 Parent Delegation Model,又稱為父級委託模型。所謂的雙親委派,就是指呼叫類載入器的 loadClass 方法時,查詢類的規則(雙親,理解為上級更為合適,因為它們之間並沒有繼承關係)

1.類載入機制流程

Java編譯器把Java原始檔編譯成.class檔案,再由JVM裝載.class檔案到記憶體中,JVM裝載完成後得到一個Class物件位元組碼。有了位元組碼物件,就可以範例化使用

2.類載入器載入順序

3.雙親委派機制流程

1、載入類MyClass.class,從低層級到高層級一級一級委派,先由應用層載入器委派給擴充套件類載入器,再由擴充套件類委派給啟動類載入器

(1)如果是自定義載入器掛載到應用程式類載入器

(2)應用程式類載入器將類載入請求委託給擴充套件類載入器

(3)擴充套件類載入器將類載入請求委託給啟動類載入器

2、啟動類載入器載入失敗,再由擴充套件類載入器載入,擴充套件類載入器載入失敗,最後由應用類載入器載入

(1)啟動類載入器在載入路徑下查詢並載入Class檔案,如果未找到目標Class檔案,則交由擴充套件類載入器載入

(2)擴充套件類載入器在載入路徑下查詢並載入Class檔案,如果未找到目標Class檔案,則交由應用程式類載入器載入

(3)應用程式類載入器在載入路徑下查詢並載入Class檔案,如果未找到目標Class檔案,則交由自定義載入器載入

(4)在自定義載入器下查詢並載入使用者指定目錄下的Class檔案,如果在自定義載入路徑下未找到目標Class檔案,則丟擲ClassNotFoud異常

3、如果應用類載入器也找不到那就報ClassNotFound異常了

4.原始碼分析

protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 1. 檢查該類是否已經載入
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        // 2. 有上級的話,委派上級 loadClass
                        c = parent.loadClass(name, false);
                    } else {
                        // 3. 如果沒有上級了(ExtClassLoader),則委派
                        BootstrapClassLoader
                                c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }
                if (c == null) {
                    long t1 = System.nanoTime();
                    // 4. 每一層找不到,呼叫 findClass 方法(每個類載入器自己擴充套件)來載入
                    c = findClass(name);
                    // 5. 記錄耗時
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

執行流程為:

1、sun.misc.Launcher$AppClassLoader //1 處, 開始檢視已載入的類,如果沒有

2、sun.misc.Launcher$AppClassLoader // 2 處,委派上級 sun.misc.Launcher$ExtClassLoader.loadClass()

3、sun.misc.Launcher$ExtClassLoader // 1 處,檢視已載入的類,如果沒有

4、sun.misc.Launcher$ExtClassLoader // 3 處,沒有上級了,則委派 BootstrapClassLoader 查詢

5、BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 類,沒有

6、sun.misc.Launcher$ExtClassLoader // 4 處,呼叫自己的 findClass 方法,是在 JAVA_HOME/jre/lib/ext 下找 類,顯然沒有,回到 sun.misc.Launcher$AppClassLoader 的 // 2 處

7、繼續執行到 sun.misc.Launcher$AppClassLoader // 4 處,呼叫它自己的 findClass 方法,在 classpath 下查詢,找到了

5.雙親委派機制優缺點

優點:

1、保證安全性,層級關係代表優先順序,也就是所有類的載入,優先給啟動類載入器,這樣就保證了核心類庫類

2、避免類的重複載入,如果父類別載入器載入過了,子類載入器就沒有必要再去載入了,確保一個類的全域性唯一性

缺點:

檢查類是否載入的委派過程是單向的, 這個方式雖然從結構上說比較清晰,使各個 ClassLoader 的職責非常明確, 但是同時會帶來一個問題, 即頂層的ClassLoader 無法存取底層的ClassLoader 所載入的類

通常情況下, 啟動類載入器中的類為系統核心類, 包括一些重要的系統介面,而在應用類載入器中, 為應用類。 按照這種模式, 應用類存取系統類自然是沒有問題, 但是系統類存取應用類就會出現問題。

如在系統類中提供了一個介面, 該介面需要在應用類中得以實現, 該介面還繫結一個工廠方法, 用於建立該介面的範例, 而介面和工廠方法都在啟動類載入器中。 這時, 就會出現該工廠方法無法建立由應用類載入器載入的應用範例

三、執行緒上下文類載入器

執行緒上下文類載入器就是用來解決類的雙親委託模型的缺陷

在Java中,官方為我們提供了很多SPI介面,例如JDBC、JBI、JNDI等。這類SPI介面,官方往往只會定義規範,具體的實現則是由第三方來完成的,比如JDBC,不同的資料庫廠商都需自己根據JDBC介面的定義進行實現。

而這些SPI介面直接由Java核心庫來提供,一般位於rt.jar包中,而第三方實現的具體程式碼庫則一般被放在classpath的路徑下。而此時問題來了:

位於rt.jar包中的SPI介面,是由Bootstrap類載入器完成載入的,而classpath路徑下的SPI實現類,則是App類載入器進行載入的。

但往往在SPI介面中,會經常呼叫實現者的程式碼,所以一般會需要先去載入自己的實現類,但實現類並不在Bootstrap類載入器的載入範圍內,經過前面的雙親委派機制的分析,我們已經得知:子類載入器可以將類載入請求委託給父類別載入器進行載入,但這個過程是不可逆的。也就是父類別載入器是不能將類載入請求委派給自己的子類載入器進行載入的,

此時就出現了這個問題:如何載入SPI介面的實現類?那就是打破雙親委派模型

SPI(Service Provider Interface):Java的SPI機制,其實就是可拔插機制。在一個系統中,往往會被分為不同的模組,比如紀錄檔模組、JDBC模組等,而每個模組一般都存在多種實現方案,如果在Java的核心庫中,直接以寫死的方式寫死實現邏輯,那麼如果要更換另一種實現方案,就需要修改核心庫程式碼,這就違反了可拔插機制的原則。為了避免這樣的問題出現,就需要一種動態的服務發現機制,可以在程式啟動過程中,動態的檢測實現者。而SPI中就提供了這麼一種機制,專門為某個介面尋找服務實現的機制。如下:

當第三方實現者提供了服務介面的一種實現之後,在jar包的 META-INF/services/ 目錄裡同時建立一個以服務介面命名的檔案,該檔案就是實現該服務介面的實現類。而當外部程式裝配這個模組的時候,就能通過該jar包 META-INF/services/ 裡的組態檔找到具體的實現類名,並裝載範例化,完成模組的注入。

基於這樣一個約定就能很好的找到服務介面的實現類,而不需要在程式碼裡制定。

同時,JDK官方也提供了一個查詢服務實現者的工具類:java.util.ServiceLoader

執行緒上下文類載入器就是雙親委派模型的破壞者,可以在執行執行緒中打破雙親委派機制的載入鏈關係,從而使得程式可以逆向使用類載入器

1.執行緒上下文類載入器(Context Classloader)

執行緒上下文類載入器(Context Classloader)是從JDK1.2開始引入的,類Thread中的getContextClassLoader()和setContextClassLoader(ClassLoader cl)分別用來獲取和設定上線文類載入器

如果沒有通過setContextClassLoader(ClassLoader cl)進行設定的話,執行緒將繼承其父執行緒的上下文類載入器。

Java應用執行時的初始執行緒的上下文類載入器是系統類載入器。線上程中執行的程式碼可以通過該類載入器來載入類與資源

它可以打破雙親委託機制,父ClassLoader可以使用當前執行緒的Thread.currentThread().getContextClassLoader()所指定的classLoader來載入類,這就可以改變父ClassLoader不能使用子ClassLoader或是其他沒有直接父子關係的ClassLoader載入的類的情況,即改變了雙親委託模型

對於SPI來說,有些介面是Java核心庫所提供的,而Java核心庫是由啟動類載入器載入的,而這些介面的實現卻是來自於不同jar包(廠商提供),Java的啟動類載入是不會載入其他來源的jar包,這樣傳統的雙親委託模型就無法滿足SPI的要求。而通過給當前執行緒設定上下文類載入器,就可以由設定的上線文類載入器來實現與介面實現類的載入

Java提供了很多核心介面的定義,這些介面被稱為SPI介面,同時為了方便載入第三方的實現類,SPI提供了一種動態的服務發現機制(約定),只要第三方在編寫實現類時,在工程內新建一個META-INF/services/目錄並在該目錄下建立一個與服務介面名稱同名的檔案,那麼在程式啟動的時候,就會根據約定去找到所有符合規範的實現類,然後交給執行緒上下文類載入器進行載入處理

MySQL的Driver驅動類

在使用 JDBC 時,都需要載入 Driver 驅動,不寫

Class.forName("com.mysql.jdbc.Driver")

Class.forName("com.mysql.cj.jdbc.Driver")

也可以讓 com.mysql.jdbc.Driver 正確載入

在MySQL6.0之後的jar包中,遺棄了之前的com.mysql.jdbc.Driver驅動,而是使用com.mysql.cj.jdbc.Driver取而代之,因為後者不需要再自己通過Class.forName("com.mysql.jdbc.Driver")這種方式手動註冊驅動,全部都可以交由給SPI機制處理

在使用 JDBC,MySQL的com.mysql.cj.jdbc.Driver的驅動類,主要就是用了Java中SPI定義的一個核心類:DriverManager,該類位於rt.jar包中,是Java中用於管理不同資料庫廠商實現的驅動,同時這些各廠商實現的Driver驅動類,都繼承自Java的核心類java.sql.Driver

DriverManager 的類載入器

System.out.println(DriverManager.class.getClassLoader());

列印 null,表示它的類載入器是 Bootstrap ClassLoader,會到 JAVA_HOME/jre/lib 下搜尋類,但 JAVA_HOME/jre/lib 下顯然沒有 mysql-connector-java-xx.xx.xx.jar 包

public class DriverManager {
    // 註冊驅動的集合
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers
        = new CopyOnWriteArrayList<>();
    // 初始化驅動
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
}

loadInitialDrivers() 方法

private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()
        // 1、使用 ServiceLoader 機制載入驅動,即 SPI
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                /* Load these drivers, so that they can be instantiated.
                 * It may be the case that the driver class may not be there
                 * i.e. there may be a packaged driver with the service class
                 * as implementation of java.sql.Driver but the actual class
                 * may be missing. In that case a java.util.ServiceConfigurationError
                 * will be thrown at runtime by the VM trying to locate
                 * and load the service.
                 *
                 * Adding a try catch block to catch those runtime errors
                 * if driver not available in classpath but it's
                 * packaged as service and that service is there in classpath.
                 */
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
        println("DriverManager.initialize: jdbc.drivers = " + drivers);
        // 2、使用 jdbc.drivers 定義的驅動名載入驅動
        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                // 這裡的 ClassLoader.getSystemClassLoader() 就是應用程式類載入器
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

從DriverManager中的loadInitialDrivers我們可以得知,我們即使不使用Class.forName(“com.mysql.cj.jdbc.Driver”),mysql的驅動也能被載入,這是因為後期jdk使用了ServiceLoader

2 是使用 Class.forName 完成類的載入和初始化,關聯的是應用程式類載入器,因此 可以順利完成類載入

1 就是 Service Provider Interface (SPI) 約定如下,在 jar 包的 META-INF/services 包下,以介面全限定名名為檔案,檔案內容是實現類名稱

2.ServiceLoader

ServiceLoader 是一個簡單的載入服務提供者的機制。通常服務提供者會實現服務當中所定義的介面。服務提供者可以以一種擴充套件的jar包的形式安裝到java平臺上擴充套件目錄中,也可以新增到應用的classpath中。

1、服務提供者需要提供一個無引數的構造方法

2、服務提供者是通過在META-INF/services目錄下相應的提供者組態檔,該組態檔的檔名由服務介面的包名組成。

3、提供者組態檔裡面就是實現這個服務介面的類路徑,每個服務提供者佔一行。

4、ServiceLoader是按需載入和範例化提供者的,就是懶載入,ServiceLoader其中還包含一個服務提供者快取,裡面存放著已經載入的服務提供者。

5、ServiceLoader會返回一個iterator迭代器,會返回所有已經載入了的服務提供者

6、ServiceLoader是執行緒不安全的

使用ServiceLoader

ServiceLoader<介面型別> allImpls = ServiceLoader.load(介面型別.class);
Iterator<介面型別> iter = allImpls.iterator();
while(iter.hasNext()) {
    iter.next();
}

例:

ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
Iterator<Driver> iterator = loader.iterator();
while (iterator.hasNext()){
   Driver dirver = iterator.next();
   System.out.println(dirver.getClass()+", 類載入器:"+dirver.getClass().getClassLoader());
}
System.out.println("當前執行緒上線文類載入器:"+Thread.currentThread().getContextClassLoader());
System.out.println("ServiceLoader類載入器:"+loader.getClass().getClassLoader());

ServiceLoader.load 方法

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 獲取執行緒上下文類載入器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

執行緒上下文類載入器是當前執行緒使用的類載入器,預設就是應用程式類載入器,它內部又是由 Class.forName 呼叫了執行緒上下文類載入器完成類載入,具體程式碼在 ServiceLoader 的內部類 LazyIterator 中

        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                        "Provider " + cn + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                        "Provider " + cn + " could not be instantiated",
                        x);
            }
            throw new Error(); // This cannot happen
        }

可參考

剖析Java類載入器

Java中執行緒上下文類載入器的講解

四、自定義類載入器

1、自定義類載入器場景

1、載入非 classpath 隨意路徑中的類檔案

2、通過介面來使用實現,希望解耦時(常用在框架設計)

3、不同應用的同名類都可以載入,不衝突,常見於 tomcat 容器

2、自定義類載入器步驟

1、繼承 ClassLoader 父類別

2、遵從雙親委派機制,重寫 findClass 方法

注:不是重寫 loadClass 方法,否則不會走雙親委派機制

3、讀取類檔案的位元組碼

4、呼叫父類別的 defineClass 方法來載入類

5、使用者呼叫該類載入器的 loadClass 方法

public class Load7 {
    public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader();
        Class<?> c1 = classLoader.loadClass("TestServiceImpl");
        Class<?> c2 = classLoader.loadClass("TestServiceImpl");
        System.out.println(c1 == c2);//true
        MyClassLoader classLoader2 = new MyClassLoader();
        Class<?> c3 = classLoader2.loadClass("TestServiceImpl");
        //雖然相同類名,但不是同一個類載入器載入的
        System.out.println(c1 == c3);//false
        c1.newInstance();
    }
}
class MyClassLoader extends ClassLoader {
    @Override // name 就是類名稱
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String path = "D:\myclasspath\" + name + ".class";
        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            Files.copy(Paths.get(path), os);
            // 得到位元組陣列
            byte[] bytes = os.toByteArray();
            // byte[] -> *.class
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            e.printStackTrace();
            throw new ClassNotFoundException("類檔案未找到", e);
        }
    }
}

到此這篇關於Java類載入器與雙親委派機制和執行緒上下文類載入器專項解讀分析的文章就介紹到這了,更多相關Java類載入器內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com