Java內存管理-掌握自定義類加載器的實現(七)

作一個積極的人

編碼、改bug、提高本身java

我有一個樂園,面向編程,春暖花開!編程

推薦閱讀segmentfault

第一季

0、Java的線程安全、單例模式、JVM內存結構等知識梳理
一、Java內存管理-程序運行過程(一)
二、Java內存管理-初始JVM和JVM啓動流程(二)
三、Java內存管理-JVM內存模型以及JDK7和JDK8內存模型對比總結(三)
四、Java內存管理-掌握虛擬機類加載機制(四)
五、Java內存管理-掌握虛擬機類加載器(五)
六、Java內存管理-類加載器的核心源碼和設計模式(六)
七、Java內存管理-掌握自定義類加載器的實現(七)
第一季總結:由淺入深JAVA內存管理 Core Storywindows

第二季

八、Java內存管理-愚人節new一個對象送給你(八)
【福利】JVM系列學習資源無套路贈送
九、Java內存管理-」一文掌握虛擬機建立對象的祕密」(九)
十、Java內存管理-你真的理解Java中的數據類型嗎(十)
十一、Java內存管理-Stackoverflow問答-Java是傳值仍是傳引用?(十一)
十二、Java內存管理-探索Java中字符串String(十二)設計模式

實戰

一文學會Java死鎖和CPU 100% 問題的排查技巧數組

分享一位老師的人工智能教程。零基礎!通俗易懂!風趣幽默!
你們能夠看看是否對本身有幫助,點擊這裏查看【人工智能教程】。接下來進入正文。安全

<font color='blue'>勿在流沙築高臺,出來混早晚要還的。</font>服務器

上一篇分析了ClassLoader的類加載相關的核心源碼,也簡單介紹了ClassLoader的設計思想,讀源碼相對來講是比較枯燥的,仍是這個是必需要走的過程,學習源碼中的一些思想,一些精髓,看一下大神級人物是怎麼寫出那麼牛逼的代碼。咱們可以從中學到一點點東西,那也是一種進步和成長了。本文基於上一篇文章內容,手把手寫一個自定義類加載器,而且經過一些簡單的案例(場景)讓咱們更加細緻和靜距離的體驗類加載器的神奇之處。網絡

<font color='red'>本文地圖:</font>app

1、上文回顧擴展

上一篇介紹了ClassLoader中loadClass()內的一些源碼,也介紹了一些核心的API,其中有一個getParent() 是沒有作說明的,這裏簡單說明一下,方便快速理解後續的內容。

// 返回委託的父類加載器。 
ClassLoader getParent()

這個方法是獲取父類加載器,那麼父類加載器是怎麼初始化的。上一文也提到了,雖然類加載器的加載模式爲雙親委派模型,可是真正在實現上並非使用繼承方式。

看下面源碼,sun.misc.Launcher類是java的入口,在啓動java應用的時候會首先建立Launcher類,建立Launcher類的時候回準備應用程序運行中須要的類加載器。

/**
*  刪除了一些其餘代碼,方便閱讀
**/
public class Launcher {
    // 可自行打印一下  bootClassPath ,看輸入內容是什麼?
    private static String bootClassPath = System.getProperty("sun.boot.class.path");// ①
    private static Launcher launcher = new Launcher(); // ②
    private ClassLoader loader;
    public Launcher() {
        Launcher.ExtClassLoader var1; 
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }
        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        Thread.currentThread().setContextClassLoader(this.loader);// ③
           // 省略其餘代碼....
    }
}

第一:Launcher做爲JAVA應用的入口,根據咱們以前所學的雙親委派模型,Laucher是由JVM建立的,它類加載器應該是BootStrapClassLoader, 這是一個C++編寫的類加載器,是Java應用體系中最頂層的類加載器,負責加載JVM須要的一些類庫,classpath配置 (%JAVA_HOME%/jre/lib)。下面經過簡單例子進行說明:

public class TestClassLoader {
    public static void main(String[] args) {
        // 能夠獲取是哪一個類加載器加載  Launcher 這個類
        ClassLoader classLoader = Launcher.class.getClassLoader();
        System.out.println("classLoader : " + classLoader);
        System.out.println("-------------------");
        String bootClassPath = System.getProperty("sun.boot.class.path");
        String[] split = bootClassPath.split(";");

        for (int i = 0; i < split.length; i++) {
            System.out.println(split[i]);
        }
}

輸出的結果,我本地機器上路徑:

classLoader : null 
-------------------
C:\Program Files\Java\jdk1.8.0_121\jre\lib\resources.jar
C:\Program Files\Java\jdk1.8.0_121\jre\lib\rt.jar
C:\Program Files\Java\jdk1.8.0_121\jre\lib\sunrsasign.jar
C:\Program Files\Java\jdk1.8.0_121\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.8.0_121\jre\lib\jce.jar
C:\Program Files\Java\jdk1.8.0_121\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_121\jre\lib\jfr.jar
C:\Program Files\Java\jdk1.8.0_121\jre\classes

第二:當初始化Launcher類的時候,遇到關鍵字new 進行初始化,調用構造方法。先獲取到ExtClassLoader類加載器,而後在獲取AppClassLoader類加載器器,而後設置ExtClassLoader作爲它父類加載器。具體設置代碼以下:

public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
    //刪掉其餘代碼...
    return new Launcher.AppClassLoader(var1x, var0);
}
AppClassLoader(URL[] var1, ClassLoader var2) {
    // 看 super的實現 ,var2 就是 ExtClassLoader
    super(var1, var2, Launcher.factory);
    this.ucp.initLookupCache(this);
}
/**
* 在AppClassLoader的父類中,java.net.URLClassLoader#URLClassLoader
*  super(parent); 設置父類加載器,最後能夠經過 getParent() 獲取父類加載器
*/
public URLClassLoader(URL[] urls, ClassLoader parent,
                      URLStreamHandlerFactory factory) {
    super(parent); // 這裏就不在繼續往上跟蹤了,在往上代碼相對簡單,可自行查閱
    SecurityManager security = System.getSecurityManager();
    //刪掉其餘代碼...
}

第三:設置上下文類加載器的思考?

在Java中爲何須要上下文類加載器呢,這個就是一個很是有意思的問題。 Java中有兩種方式獲取類加載器:

第一種:一個類被加載的時候使用哪一個類加載器來加載,也就是類.class.getClassLoader()或者對象.getClass().getClassLoader()

String str = new String();
ClassLoader classLoader1 = String.class.getClassLoader();
ClassLoader classLoader2 = str.getClass().getClassLoader();
System.out.println("String loader1 : " + classLoader1);
System.out.println("String loader2 : " + classLoader2);
-----輸出爲null,啓動類加載器進行加載--------
String loader1 : null
String loader2 : null

第二種:經過Thread的上限文獲取類加載器。爲何要經過上下文加載呢?

雖然咱們都知道Java類加載的雙親委派模型,在加載一個類的時候,會優先委派給父類加載器,這樣保證不會出現類被重複加載,也保證了Java一些基礎類(如String類)能夠穩定的存在,不會被用戶自定義類頂替掉。

可是雙親委派模型並非完美的,在一些場景下會出現一些比較難解決的問題,舉個例子,在使用SPI的時候,java.util.ServiceLoader是經過BootStrapClassLoader類加載器加載的,在執行到加載用戶編寫的擴展類的時候,若是使用當前類的類加載器,是確定沒法加載到用戶編寫的類的,這個時候就沒法繼續執行了,因此這個時候就須要使用Thread的上下文類加載器,查看源碼的時候咱們就發現,在用戶不主動傳遞ClassLoader的時候,會獲取當前上下文類加載器,這樣應用程序才能正常的執行。

public static <S> ServiceLoader<S> load(Class<S> var0) {
    ClassLoader var1 = Thread.currentThread().getContextClassLoader();
    return load(var0, var1);
}

<font color='blue'>小總結:</font>

上面的內容是在上一篇的基礎上繼續的擴展,若是對尚未看過上一篇內容,請先閱讀上一篇內容後,在來看這段內容。整個ClassLoader源碼的的部分就分析這麼多了,後面在自定義類加載器中有遇到須要分析源碼的地方,仍是會繼續進行說明和講解。

2、實現自定義類加載器

前面的兩篇文章一直在爲自定義加載器作鋪墊,本文終於來引來這個神祕的嘉賓了,下面就咱們用"熱戀"的掌聲歡迎它的出場(活躍一下氣氛,由於剛纔看源碼太安靜了)!

首先看一下JDK API中如何教咱們實現從現從網絡加載類的類加載器的簡單示例。看下面代碼:

例如,應用程序能夠建立一個網絡類加載器,從服務器中下載類文件。示例代碼以下所示: 

ClassLoader loader = new NetworkClassLoader(host, port);
Object main = loader.loadClass("Main", true).newInstance();
. . .
網絡類加載器子類必須定義方法 findClass 和 loadClassData,以實現從網絡加載類。下載組成該類的字節後,它應該使用方法 defineClass 來建立類實例。示例實現以下: 

class NetworkClassLoader extends ClassLoader {
    String host;
    int port;

    public Class findClass(String name) {
    byte[] b = loadClassData(name);
    return defineClass(name, b, 0, b.length);
    }

    private byte[] loadClassData(String name) {
    // load the class data from the connection
    . . .
    }
}

下面就「手把手」一步步教你如何實現一個自定義類加載器!

第一步: 新建一個MyClassLoader 繼承 ClassLoader

public class MyClassLoader extends ClassLoader {}

第二步:添加自定義的類加載器屬性和 構造函數

public class MyClassLoader extends ClassLoader {
    /**
     * 類加載器名稱
     */
    private String name;

    /**
     * 自定義加載路徑
     */
    private String path;
    /**
     * 自定義加載文件後綴類型
     */
    private final String fileType = ".class";
    
    public MyClassLoader(String name,String path){
        //讓系統類加載器(AppClassLoader)成爲該類加載器的父類加載器
        super();
        this.name = name;
        this.path = path;
    }
    public MyClassLoader(ClassLoader parent,String name,String path){
        //顯示指定該類的父類加載器
        super(parent);
        this.name = name;
        this.path = path;
    }
    
    @Override
    public String toString() {
        return this.name;
    }
}

第三步:重寫findClass方法,自定義咱們本身的查詢類的方式,而後經過defineClass方法將一個 byte 數組轉換爲 Class 類的實例。

/**
* 加載咱們本身定義的類,經過咱們本身定義的類加載器
* @param name 二進制的文件名稱 
* @return Class實例對象
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    //獲取class文件的字節數組
    byte[] resultData = this.loadByteClassData(name);

    return super.defineClass(name, resultData, 0, resultData.length);
}
/**
* 加載指定路徑下面的class文件的字節數組
* @param name 二進制文件名稱 ,例如:com.learn.classloader.Demo
* @return 二進制字節數組
*/
private byte[] loadByteClassData(String name) {
    byte[] classData = null;
    InputStream in = null;
    ByteArrayOutputStream os = null;
    try {
        // 好比 有包名 二進制文件名:com.learn.classloader.Demo
        // 轉換爲本地路徑 com/learnclassloader/Demo.class
        name = this.path + name.replaceAll("\\.", "/") + fileType;
        File file = new File(name);
        os = new ByteArrayOutputStream();
        in = new FileInputStream(file);

        int tmp = 0;
        while ((tmp = in.read()) != -1){
            os.write(tmp);
        }
        // 文件流轉爲二進制字節流
        classData = os.toByteArray();
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        try {
            // 關閉流
            if(in != null){
                in.close();
            }
            if(os != null){
                os.close();
            }
        }catch (Exception e){
            e.printStackTrace();
        }

    }
    return classData;
}

上面三步整合在一塊兒,就實現了一個簡單的類加載! 查看自定義類加載器的所有代碼。

/**
 * 自定義類加載器
 *
 * @author:dufyun
 * @version:1.0.0
 * @date 2019/3/28
 */
public class MyClassLoader extends ClassLoader {
    /**
     * 類加載器名稱
     */
    private String name;

    /**
     * 自定義加載路徑
     */
    private String path;
    /**
     * 自定義加載文件後綴類型
     */
    private final String fileType = ".class";

    public MyClassLoader(String name,String path){
        //讓系統類加載器(AppClassLoader)成爲該類加載器的父類加載器
        super();
        this.name = name;
        this.path = path;
    }
    public MyClassLoader(ClassLoader parent,String name,String path){
        //顯示指定該類的父類加載器
        super(parent);
        this.name = name;
        this.path = path;
    }

    /**
     * 加載咱們本身定義的類,經過咱們本身定義的類加載器
     * @param name 二進制的文件名稱
     * @return Class實例對象
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //獲取class文件的字節數組
        byte[] resultData = this.loadByteClassData(name);

        return super.defineClass(name, resultData, 0, resultData.length);
    }
    /**
     * 加載指定路徑下面的class文件的字節數組
     * @param name 二進制文件名稱 ,例如:com.learn.classloader.Demo
     * @return 二進制字節數組
     */
    private byte[] loadByteClassData(String name) {
        byte[] classData = null;
        InputStream in = null;
        ByteArrayOutputStream os = null;
        try {
            // 好比 有包名 二進制文件名:com.learn.classloader.Demo
            // 轉換爲本地路徑 com/learn/classloader/Demo.class
            name = this.path + name.replaceAll("\\.", "/") + fileType;
            File file = new File(name);
            os = new ByteArrayOutputStream();
            in = new FileInputStream(file);

            int tmp = 0;
            while ((tmp = in.read()) != -1){
                os.write(tmp);
            }
            // 文件流轉爲二進制字節流
            classData = os.toByteArray();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                // 關閉流
                if(in != null){
                    in.close();
                }
                if(os != null){
                    os.close();
                }
            }catch (Exception e){
                e.printStackTrace();
            }

        }
        return classData;
    }

    @Override
    public String toString() {
        return this.name;
    }
}

第五: 簡單的測試自定義類加載器!

MyClassLoader myClassLoaderA = new MyClassLoader("aflyun", "F:\\tmp\\");
System.out.println("myClassLoaderA :" + myClassLoaderA.getParent().getClass().getName());

// 設置父類加載器 爲null ,啓動類加載器
MyClassLoader myClassLoaderB = new MyClassLoader(null, "aflyun", "F:\\tmp\\");
System.out.println("myClassLoaderB :" + myClassLoaderB.getParent());

------------
myClassLoaderA :sun.misc.Launcher$AppClassLoader
myClassLoaderB :null

看到這裏,你若是對本篇第一小節理解的話,這裏確定會好奇,MyClassLoader是怎麼設置AppClassLoader爲父類加載器的,在代碼中只寫了一個 super(); 系統類加載器(AppClassLoader)成爲該類加載器的父類加載器。仍是看源碼 ,使用super();在初始化的時候調用CLassLoader的構造函數,在此構造函數中有一個getSystemClassLoader()獲取的就是 AppClassLoader

protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}

咱們在看一下 getSystemClassLoader()的實現,其中有一個initSystemClassLoader()方法獲取到Launcher初始化加載的ClassLoader,而後將此ClassLoader賦值給 ClassLoader類 中的 scl!具體源碼以下:

// The class loader for the system
private static ClassLoader scl;
public static ClassLoader getSystemClassLoader() {
    initSystemClassLoader(); // 獲取系統初始化的ClassLoader
    if (scl == null) {
        return null;
    }
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkClassLoaderPermission(scl, Reflection.getCallerClass());
    }
    return scl;
}
private static synchronized void initSystemClassLoader() {
    if (!sclSet) {
        // 省略其餘代碼.....
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
        if (l != null) {
            Throwable oops = null;
            //從Launcher 中獲取ClassLoader也就是 AppClassLoader
            scl = l.getClassLoader();
          // 省略其餘代碼.....
        }
        sclSet = true;
    }
}

最後 this(checkCreateClassLoader(), getSystemClassLoader());調用了ClassLoader的另外一個構造函數,具體看下面源碼,比較簡單,就不作太多說明了。

// 這段代碼上一篇文章就介紹過
private ClassLoader(Void unused, ClassLoader parent) {
    this.parent = parent; // 將獲取的系統類加載器做爲父類加載器,經過getParent()獲取!
    if (ParallelLoaders.isRegistered(this.getClass())) {
        parallelLockMap = new ConcurrentHashMap<>();
        // 省略其餘代碼.....
    } else {
        // no finer-grained lock; lock on the classloader instance
        parallelLockMap = null;
       // 省略其餘代碼.....
    }
}
/**
* 獲取父類加載器
*/
public final ClassLoader getParent() {
    if (parent == null)
        return null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkClassLoaderPermission(parent, Reflection.getCallerClass());
    }
    return parent;
}

<font color='blue'>一張圖在此說明:當前的類加載器就是自定義類加載器MyClassLoader!</font>

MyClassLoader myClassLoaderA = new MyClassLoader("aflyun", "F:\\tmp\\");

類加載器雙親委派模型


<font color='blue'>tips1 :自定義類加載說明</font>

在實現自定義類加載器過程當中能夠重寫findClass 也能夠重寫loadClass ,但通常建議重寫findClass

<font color='blue'>tips2 :自定義類加載器設置了加載路徑path,其實以前介紹過的類加載器也有對應的加載路徑。</font>

// BootStrapClassLoader
String bootClassPath = System.getProperty("sun.boot.class.path");
// ExtClassLoader
String var0 = System.getProperty("java.ext.dirs");
// AppClassLoader
String var1 = System.getProperty("java.class.path");

3、多案例分析

一、相同兩個類文件,名稱同樣,一個在磁盤F:\tmp\下(無包名)一個工程中(有包名)

F:盤中代碼

public class HelloWorld {
    public HelloWorld() {
        System.out.println("--F: --HelloWorld--" + this.getClass().getClassLoader());
    }
}

工程中代碼:

package com.learn.classloader;

public class HelloWorld {
    public HelloWorld() {
        System.out.println("--IDEA --HelloWorld--" + this.getClass().getClassLoader());
    }
}

執行類加載代碼

MyClassLoader myClassLoader = new MyClassLoader("myClassLoader","F:/tmp/");
Class<?> cls = null;
try {
    //加載類文件
    cls = myClassLoader.loadClass("HelloWorld");
    cls.newInstance();//實例化類。調用構造方法
} catch (Exception e) {
    e.printStackTrace();
}

思考一下打印的結果是什麼呢?

答案:--F: --HelloWorld--myClassLoader

緣由分析:HelloWorld類的class文件只有在F:/tmp/下存在,因此就加載的是F盤下的HelloWorld類文件!

二、相同兩個類文件,名稱和包名同樣,一個在磁盤F:\tmp\,一個在工程中

F:盤中代碼

package com.learn.classloader;
public class HelloWorld {
    public HelloWorld() {
        System.out.println("--F:com.learn.classloader --HelloWorld--" + this.getClass().getClassLoader());
    }
}

工程中的代碼:

package com.learn.classloader;

public class HelloWorld {
    public HelloWorld() {
        System.out.println("--IDEA --HelloWorld--" + this.getClass().getClassLoader());
    }
}

執行類加載代碼

MyClassLoader myClassLoader = new MyClassLoader("myClassLoader","F:/tmp/");
Class<?> cls = null;
try {
    cls = myClassLoader.loadClass("com.learn.classloader.HelloWorld");
    cls.newInstance();
} catch (Exception e) {
    e.printStackTrace();
}

思考一下打印的結果是什麼呢?

答案:--IDEA --HelloWorld--sun.misc.Launcher$AppClassLoader@18b4aac2

緣由分析:F盤中HelloWorld和工程中的HelloWorld具備同樣的包名,而且MyClassLoader沒有設置父類加載器,那麼默認的父類加載類就是AppClassLoader,根據以前所學的雙親委派模型,HelloWorld類文件會首先被父類加載器加載,也就是被AppClassLoader加載,只要父類加載器加載成功,子類加載器就不會在進行加載!

三、新增一個myClassLoaderB的實例設置父類加載器myClassLoader,加載MyHelloWorld

F: 盤 MyHelloWorld

package com.learn.classloader;
public class MyHelloWorld {
    public MyHelloWorld() {
        System.out.println("--F:com.learn.classloader --MyHelloWorld--" + this.getClass().getClassLoader());
    }
}

D:盤 MyHelloWorld

package com.learn.classloader;
public class MyHelloWorld {
    public MyHelloWorld() {
        System.out.println("--D:com.learn.classloader --MyHelloWorld--" + this.getClass().getClassLoader());
    }
}

此時新增一個myClassLoaderB,加載MyHelloWorld!

加載的代碼以下:

MyClassLoader myClassLoader = new MyClassLoader("myClassLoader","F:/tmp/");
MyClassLoader myClassLoaderB = new MyClassLoader(myClassLoader,"myClassLoader","D:/tmp/");
Class<?> cls = null;
try {
    cls = myClassLoaderB.loadClass("com.learn.classloader.MyHelloWorld");
    cls.newInstance();
} catch (Exception e) {
    e.printStackTrace();
}

思考一下打印的結果是什麼呢?

答案:--F:com.learn.classloader --MyHelloWorld--myClassLoader

緣由分析:myClassLoaderB父類加載器是myClassLoader,此時在加載 MyHelloWorld,首先會被父類加載器myClassLoader 加載!

四、新增myClassLoaderC設置父類加載器爲null(啓動類加載器),加載MyHelloWorld!

代碼示例和示例3同樣!只是修改類加載器!代碼以下:

MyClassLoader myClassLoaderC = new MyClassLoader(null,"myClassLoaderC","D:/tmp/");
Class<?> cls = null;
try {
    cls = myClassLoaderC.loadClass("com.learn.classloader.MyHelloWorld");
    cls.newInstance();
} catch (Exception e) {
    e.printStackTrace();
}

思考一下打印的結果是什麼呢?

答案:--D:com.learn.classloader --MyHelloWorld--myClassLoaderC

緣由分析:myClassLoaderC父類加載器是啓動類加載器,在啓動類加載器中找不到MyHelloWorld,轉一圈回來仍是須要myClassLoaderC本身去加載!

4、本文總結

本文實現了一個自定義的類加載器,而且經過簡單的案例進行講解和說明,讓咱們更加深刻的瞭解類加載器的雙親委派模式和實現原理。

對類加載器有了比較深刻的學習和思考以後,會對咱們之後寫Java代碼會有必定幫助,而且在遇到一些Java的異常如ClassNotFoundException可以快速知道緣由。 其實類加載的知識還有不少,在這裏先拋出兩個問題:

<font color='blue'>問題一、Java熱部署如何實現 ? </font>修改一個Java文件後,不須要啓動服務,就能夠動態生效! (目前的主流開發工具都支持,如IDEA 在windows下 Ctrl+Shirt+F9,動態編譯,動態加載!或者 SpringBoot經過配置devtools實現熱部署的原理是什麼?)

本質上是更新clas文件內容! 不須要從新啓動服務!

<font color='blue'>問題二、以前一直提的類加載模式: 雙親模式模式!可是在Tomcat中你知道 WebappClassLoader 的加載機制嗎?</font>

說明:WebappClassLoader 會先加載本身的Class ,找不到在委託給parent,破壞雙親委派模式!

後續有時間會去整理,若是你對這兩個問題感興趣,也歡迎在文末留言,一塊兒探討!

5、參考資料

《JDK API 文檔》

《深刻理解Java虛擬機》

<font color='red'>備註: 因爲本人能力有限,文中如有錯誤之處,歡迎指正。</font>


謝謝你的閱讀,若是您以爲這篇博文對你有幫助,請點贊或者喜歡,讓更多的人看到!祝你天天開心愉快!


<center><font color='red'>Java編程技術樂園</font>:一個分享編程知識的公衆號。跟着老司機一塊兒學習乾貨技術知識,天天進步一點點,讓小的積累,帶來大的改變!</center>
<p/>
<center><font color='blue'>掃描關注,後臺回覆【資源】,獲取珍藏乾貨! 99.9%的夥伴都很喜歡</font></center>
<p/>

image.png | center| 747x519

<center>© 天天都在變得更好的阿飛雲</center>

相關文章
相關標籤/搜索