從源碼角度學習Java動態代理


公眾號文章鏈接:https://mp.weixin.qq.com/s/jaLvb21yVHg2R_gJ-JSeVA

前言

最近,看了一下關於RMI(Remote Method Invocation)相關的知識,遇到了一個動態代理的問題,然后就決定探究一下動態代理。

這里先科普一下RMI。

RMI

像我們平時寫的程序,對象之間互相調用方法都是在同一個JVM中進行,而RMI可以實現一個JVM上的對象調用另一個JVM上對象的方法,即遠程調用。

接口定義

定義一個遠程對象接口,實現Remote接口來進行標記。

public interface UserInterface extends Remote {
    void sayHello() throws RemoteException;
}

遠程對象定義

定義一個遠程對象類,繼承UnicastRemoteObject來實現Serializable和Remote接口,並實現接口方法。

public class User extends UnicastRemoteObject implements UserInterface {
    public User() throws RemoteException {}
    @Override
    public void sayHello() {
        System.out.println("Hello World");
    }
}

服務端

啟動服務端,將user對象在注冊表上進行注冊。

public class RmiServer {
    public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
        User user = new User();
        LocateRegistry.createRegistry(8888);
        Naming.bind("rmi://127.0.0.1:8888/user", user);
        System.out.println("rmi server is starting...");
    }
}

啟動服務端:
在這里插入圖片描述

客戶端

從服務端注冊表獲取遠程對象,在服務端調用sayHello()方法。

public class RmiClient {
    public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
        UserInterface user = (UserInterface) Naming.lookup("rmi://127.0.0.1:8888/user");
        user.sayHello();
    }
}

服務端運行結果:
在這里插入圖片描述
至此,一個簡單的RMI demo完成。

動態代理

提出問題

看了看RMI代碼,覺得UserInterface這個接口有點多余,如果客戶端使用Naming.lookup()獲取的對象不強轉成UserInterface,直接強轉成User是不是也可以,於是試了一下,就報了以下錯誤:
在這里插入圖片描述
似曾相識又有點陌生的$Proxy0,翻了翻塵封的筆記找到了是動態代理的知識點,寥寥幾筆帶過,所以決定梳理一下動態代理,重新整理一份筆記。

動態代理Demo

接口定義

public interface UserInterface {
    void sayHello();
}

真實角色定義

public class User implements UserInterface {
    @Override
    public void sayHello() {
        System.out.println("Hello World");
    }
}

調用處理類定義

代理類調用真實角色的方法時,其實是調用與真實角色綁定的處理類對象的invoke()方法,而invoke()調用的是真實角色的方法。

這里需要實現 InvocationHandler 接口以及invoke()方法。

public class UserHandler implements InvocationHandler {
    private User user;
    public UserProxy(User user) {
        this.user = user;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("invoking start....");
        method.invoke(user);
        System.out.println("invoking stop....");
        return user;
    }
}

執行類

public class Main {
    public static void main(String[] args) {
        User user = new User();
        // 處理類和真實角色綁定
        UserHandler userHandler = new UserHandler(user);
        // 開啟將代理類class文件保存到本地模式,平時可以省略
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        // 動態代理生成代理對象$Proxy0
        Object o = Proxy.newProxyInstance(Main.class.getClassLoader(), new Class[]{UserInterface.class}, userHandler);
        // 調用的其實是invoke()
        ((UserInterface)o).sayHello();
    }

運行結果:
動態代理運行結果
這樣動態代理的基本用法就學完了,可是還有好多問題不明白。

  1. 動態代理是怎么調用的invoke()方法?
  2. 處理類UserHandler有什么作用?
  3. 為什么要將類加載器和接口類數組當作參數傳入newProxyInstance?

假如讓你去實現動態代理,你有什么設計思路?

猜想

動態代理,是不是和靜態代理,即設計模式的代理模式有相同之處呢?

簡單捋一捋代理模式實現原理:真實角色和代理角色共同實現一個接口並實現抽象方法A,代理類持有真實角色對象,代理類在A方法中調用真實角色對象的A方法。在Main中實例化代理對象,調用其A方法,間接調用了真實角色的A方法。

實現代碼

// 接口和真實角色對象就用上面代碼
// 代理類,實現UserInterface接口
public class UserProxy implements UserInterface {
	  // 持有真實角色對象
    private User user = new User();
    @Override
    public void sayHello() {
        System.out.println("invoking start....");
        // 在代理對象的sayHello()里調用真實角色的sayHello()
        user.sayHello();
        System.out.println("invoking stop....");
    }
}
// 運行類
public class Main {
    public static void main(String[] args) {
    	  // 實例化代理角色對象
        UserInterface userProxy = new UserProxy();
        // 調用了代理對象的sayHello(),其實是調用了真實角色的sayHello()
        userProxy.sayHello();
    }

拿開始的動態代理代碼和靜態代理比較,接口、真實角色都有了,區別就是多了一個UserHandler處理類,少了一個UserProxy代理類。

接着對比一下兩者的處理類和代理類,發現UserHandler的invoke()和UserProxy的sayHello()這兩個方法的代碼都是一樣的。那么,是不是新建一個UserProxy類,然后實現UserInterface接口並持有UserHandler的對象,在sayHello()方法中調用UserHandler的invoke()方法,就可以動態代理了。

代碼大概就是這樣的

// 猜想的代理類結構,動態代理生成的代理是com.sun.proxy.$Proxy0
public class UserProxy implements UserInterface{
    // 持有處理類的對象
    private InvocationHandler handler;
    public UserProxy(InvocationHandler handler) {
        this.handler = handler;
    }
    // 實現sayHello()方法,並調用invoke()
    @Override
    public void sayHello() {
        try {
            handler.invoke(this, UserInterface.class.getMethod("sayHello"), null);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }
}
// 執行類
public static void main(String[] args) {
        User user = new User();
        UserHandler userHandler = new UserHandler(user);
        UserProxy proxy = new UserProxy(userHandler);
        proxy.sayHello();
    }

輸出結果:
在這里插入圖片描述

上面的代理類代碼是寫死的,而動態代理是當你調用Proxy.newProxyInstance()時,會根據你傳入的參數來動態生成這個代理類代碼,如果讓我實現,會是以下這個流程。

  1. 根據你傳入的Class[]接口數組,代理類會來實現這些接口及其方法(這里就是sayHello()),並且持有你傳入的userHandler對象,使用文件流將預先設定的包名、類名、方法名等一行行代碼寫到本地磁盤,生成$Proxy0.java文件
  2. 使用編譯器將$Proxy0.java編譯成$Proxy0.class
  3. 根據你傳入的ClassLoader將$Proxy0.class加載到JMV中
  4. 調用Proxy.newProxyInstance()就會返回一個$Proxy0的對象,然后調用sayHello(),就執行了里面userHandler的invoke()

以上就是對動態代理的一個猜想過程,下面就通過debug看看源碼是怎么實現的。


在困惑的日子里學會擁抱源碼

擁抱源碼

調用流程圖

這里先用PPT畫一個流程圖,可以跟着流程圖來看后面的源碼。

流程圖

從newProxyInstance()設置斷點
main

newProxyInstance()

newProxyInstance()代碼分為上下兩部分,上部分是獲取類$Proxy0.class,下部分是通過反射構建$Proxy0對象。

上部分代碼

newProxyInstance()

從名字看就知道getProxyClass0()是核心方法,step into

getProxyClass0()

getProxyClass()

里面調用了WeakCache對象的get()方法,這里暫停一下debug,先講講WeakCache類。

WeakCache

顧名思義,它是一個弱引用緩存。那什么是是弱引用呢,是不是還有強引用呢?

弱引用

WeakReference就是弱引用類,作為包裝類來包裝其他對象,在進行GC時,其中的包裝對象會被回收,而WeakReference對象會被放到引用隊列中。

舉個栗子:

 // 這就是強引用,只要不寫str1 = null,str1指向的這個字符串不就會被垃圾回收
 String str1 = new String("hello");
 ReferenceQueue referenceQueue = new ReferenceQueue();
 // 只要垃圾回收,這個str2里面包裝的對象就會被回收,但是這個弱引用對象不會被回收,即word會被回收,但是str2指向的弱引用對象不會
 // 每個弱引用關聯一個ReferenceQueue,當包裝的對象被回收,這個弱引用對象會被放入引用隊列中
 WeakReference<String> str2 = new WeakReference<>(new String("world"), referenceQueue);
 // 執行gc
 System.gc();
 Thread.sleep(3);
 // 輸出被回收包裝對象的弱引用對象:java.lang.ref.WeakReference@2077d4de
 // 可以debug看一下,弱引用對象的referent變量指向的包裝對象已經為null
 System.out.println(referenceQueue.poll());

WeakCache的結構

其實整個WeakCache的都是圍繞着成員變量map來工作的,構建了一個一個<K,<K,V>>格式的二級緩存,在動態代理中對應的類型是<類加載器, <接口Class, 代理Class>>,它們都使用了弱引用進行包裝,這樣在垃圾回收的時候就可以直接回收,減少了堆內存占用。

// 存放已回收弱引用的隊列
private final ReferenceQueue<K> refQueue = new ReferenceQueue<>();
// 使用ConcurrentMap實現的二級緩存結構
private final ConcurrentMap<Object, ConcurrentMap<Object, Supplier<V>>> map = new ConcurrentHashMap<>();
// 可以不關注這個,這個是用來標識二級緩存中的value是否存在的,即Supplier是否被回收
private final ConcurrentMap<Supplier<V>, Boolean> reverseMap = new ConcurrentHashMap<>();
// 包裝傳入的接口class,生成二級緩存的Key
private final BiFunction<K, P, ?> subKeyFactory = new KeyFactory();
// 包裝$Proxy0,生成二級緩存的Value
private final BiFunction<K, P, V> valueFactory = new ProxyClassFactory();

WeakCache的get()

回到debug,接着進入get()方法,看看map二級緩存是怎么生成KV的。

 public V get(K key, P parameter) {
        Objects.requireNonNull(parameter);
        // 遍歷refQueue,然后將緩存map中對應的失效value刪除
        expungeStaleEntries();
        // 以ClassLoader為key,構建map的一級緩存的Key,是CacheKey對象
        Object cacheKey = CacheK.valueOf(key, refQueue);
        // 通過Key從map中獲取一級緩存的value,即ConcurrentMap
        ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);
        if (valuesMap == null) {
            // 如果Key不存在,就新建一個ConCurrentMap放入map,這里使用的是putIfAbsent
            // 如果key已經存在了,就不覆蓋並返回里面的value,不存在就返回null並放入Key
            // 現在緩存map的結構就是ConCurrentMap<CacheKey, ConCurrentMap<Object, Supplier>>
            ConcurrentMap<Object, Supplier<V>> oldValuesMap = map.putIfAbsent(cacheKey, valuesMap = new ConcurrentHashMap<>());
            // 如果其他線程已經創建了這個Key並放入就可以復用了
            if (oldValuesMap != null) {
                valuesMap = oldValuesMap;
            }
        }
        // 生成二級緩存的subKey,現在緩存map的結構就是ConCurrentMap<CacheKey, ConCurrentMap<Key1, Supplier>>
        // 看后面的<生成二級緩存Key>!!!
        Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
        // 根據二級緩存的subKey獲取value
        Supplier<V> supplier = valuesMap.get(subKey);
        Factory factory = null;
        
// !!!直到完成二級緩存Value的構建才結束,Value是弱引用的$Proxy0.class!!!
        while (true) {
            // 第一次循環:suppiler肯定是null,因為還沒有將放入二級緩存的KV值
        	  // 第二次循環:這里suppiler不為null了!!!進入if
            if (supplier != null) {
                // 第二次循環:真正生成代理對象,
                // 往后翻,看<生成二級緩存Value>,核心!!!!!
                // 看完后面回到這里:value就是弱引用后的$Proxy0.class
                V value = supplier.get();
                if (value != null) {
					        // 本方法及上部分的最后一行代碼,跳轉最后的<構建$Proxy對象>
                    return value;
                }
            }
            // 第一次循環:factory肯定為null,生成二級緩存的Value
            if (factory == null) {
                factory = new Factory(key, parameter, subKey, valuesMap);
            }
            // 第一次循環:將subKey和factory作為KV放入二級緩存
            if (supplier == null) {
                supplier = valuesMap.putIfAbsent(subKey, factory);
                if (supplier == null) {
                    // 第一次循環:賦值之后suppiler就不為空了,記住!!!!!
                    supplier = factory;
                }
            } 
           }
        }
    }

生成二級緩存Key

在get()中調用subKeyFactory.apply(key, parameter),根據你newProxyInstance()傳入的接口Class[]的個數來生成二級緩存的Key,這里我們就傳入了一個UserInterface.class,所以就返回了Key1對象。

KeyFactory.apply()

不論是Key1、Key2還是KeyX,他們都繼承了WeakReference,都是包裝對象是Class的弱引用類。這里看看Key1的代碼。

Key1

生成二級緩存Value

在上面的while循環中,第一次循環只是生成了一個空的Factory對象放入了二級緩存的ConcurrentMap中。

在第二次循環中,才開始通過get()方法來真正的構建value。

別回頭,接着往下看。

Factory.get()生成弱引用value

CacheValue類是一個弱引用,是二級緩存的Value值,包裝的是class,在這里就是$Proxy0.class,至於這個類如何生成的,根據下面代碼注釋一直看完Class文件的生成

public synchronized V get() {
            // 檢查是否被回收,如果被回收,會繼續執行上面的while循環,重新生成Factory
            Supplier<V> supplier = valuesMap.get(subKey);
            if (supplier != this) {
                return null;
            }
            // 這里的V的類型是Class
            V value = null;
            // 這行是核心代碼,看后面<class文件的生成>,記住這里返回的是Class
            value = Objects.requireNonNull(valueFactory.apply(key, parameter));
            // 將Class對象包裝成弱引用
            CacheValue<V> cacheValue = new CacheValue<>(value);
            // 回到上面<WeakCache的get()方法>V value = supplier.get();
            return value;
        }
    }

CacheValue

Class文件的生成

包名類名的定義與驗證

進入valueFactory.apply(key, parameter)方法,看看class文件是怎么生成的。

 private static final String proxyClassNamePrefix = "$Proxy";

 public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
            Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
            // 遍歷你傳入的Class[],我們只傳入了UserInterface.class
            for (Class<?> intf : interfaces) {
                Class<?> interfaceClass = null;
                 // 獲取接口類
                interfaceClass = Class.forName(intf.getName(), false, loader);
                 // 這里就很明確為什么只能傳入接口類,不是接口類會報錯
                if (!interfaceClass.isInterface()) {
                    throw new IllegalArgumentException(
                        interfaceClass.getName() + " is not an interface");
                }
            String proxyPkg = null; 
            int accessFlags = Modifier.PUBLIC | Modifier.FINAL;
            for (Class<?> intf : interfaces) {
                int flags = intf.getModifiers();
                // 驗證接口是否是public,不是public代理類會用接口的package,因為只有在同一包內才能繼承
                // 我們的UserInterface是public,所以跳過
                if (!Modifier.isPublic(flags)) {
                    accessFlags = Modifier.FINAL;
                    String name = intf.getName();
                    int n = name.lastIndexOf('.');
                    String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
                    if (proxyPkg == null) {
                        proxyPkg = pkg;
                    } else if (!pkg.equals(proxyPkg)) {
                        throw new IllegalArgumentException(
                            "non-public interfaces from different packages");
                    }
                }
            }
            // 如果接口類是public,則用默認的包
            if (proxyPkg == null) {
                // PROXY_PACKAGE = "com.sun.proxy";
                proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
            }
            // 原子Int,此時num = 0
            long num = nextUniqueNumber.getAndIncrement();
            // com.sun.proxy.$Proxy0,這里包名和類名就出現了!!!
            String proxyName = proxyPkg + proxyClassNamePrefix + num;
            // !!!!生成class文件,查看后面<class文件寫入本地> 核心!!!!
            byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);
            // !!!看完下面再回來看這行!!!!
            // 獲取了字節數組之后,獲取了class的二進制流將類加載到了JVM中
            // 並且返回了$Proxy0.class,返回給Factory.get()來包裝
            return defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);
           
            }
        }
    }

defineClass0()是Proxy類自定義的類加載的native方法,會獲取class文件的二進制流加載到JVM中,以獲取對應的Class對象,這一塊可以參考JVM類加載器。

class文件寫入本地

generateProxyClass()方法會將class二進制文件寫入本地目錄,並返回class文件的二進制流,使用你傳入的類加載器加載,這里你知道類加載器的作用了么

 public static byte[] generateProxyClass(final String name,
                                            Class[] interfaces)
    {
        ProxyGenerator gen = new ProxyGenerator(name, interfaces);
        // 生成class文件的二進制,查看后面<生成class文件二進制>
        final byte[] classFile = gen.generateClassFile();
        // 將class文件寫入本地		
        if (saveGeneratedFiles) {
            java.security.AccessController.doPrivileged(
            new java.security.PrivilegedAction<Void>() {
                public Void run() {
                    try {
                        FileOutputStream file =
                            new FileOutputStream(dotToSlash(name) + ".class");
                        file.write(classFile);
                        file.close();
                        return null;
                    } catch (IOException e) {
                        throw new InternalError(
                            "I/O exception saving generated file: " + e);
                    }
                }
            });
        }
        // 返回$Proxy0.class字節數組,回到上面<class文件生成>
        return classFile;
    }

生成class文件二進制流

generateClassFile()生成class文件,並存放到字節數組,可以順便學一下class結構,這里也體現了你傳入的class[]的作用

    private byte[] generateClassFile() {
        // 將hashcode、equals、toString是三個方法放入代理類中
        addProxyMethod(hashCodeMethod, Object.class);
        addProxyMethod(equalsMethod, Object.class);
        addProxyMethod(toStringMethod, Object.class);
        for (int i = 0; i < interfaces.length; i++) {
            Method[] methods = interfaces[i].getMethods();
            for (int j = 0; j < methods.length; j++) {
            	// 將接口類的方法放入新建的代理類中,這里就是sayHello()
                addProxyMethod(methods[j], interfaces[i]);
            }
        }
        for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
            checkReturnTypes(sigmethods);
        }
        // 給代理類增加構造方法
        methods.add(generateConstructor());
        for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
            for (ProxyMethod pm : sigmethods) {
                   // 將上面的四個方法都封裝成Method類型成員變量
                    fields.add(new FieldInfo(pm.methodFieldName,
                        "Ljava/lang/reflect/Method;",
                         ACC_PRIVATE | ACC_STATIC));
                    // generate code for proxy method and add it
                    methods.add(pm.generateMethod());
                }
            }
        // static靜態塊構造
        methods.add(generateStaticInitializer());
        cp.getClass(dotToSlash(className));
        cp.getClass(superclassName);
        for (int i = 0; i < interfaces.length; i++) {
            cp.getClass(dotToSlash(interfaces[i].getName()));
        }
        cp.setReadOnly();
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        DataOutputStream dout = new DataOutputStream(bout);
        // !!!核心點來了!這里就開始構建class文件了,以下都是class的結構,只寫一部分
        try {   
            // u4 magic,class文件的魔數,確認是否為一個能被JVM接受的class
            dout.writeInt(0xCAFEBABE);
            // u2 minor_version,0
            dout.writeShort(CLASSFILE_MINOR_VERSION);
            // u2 major_version,主版本號,Java8對應的是52;
            dout.writeShort(CLASSFILE_MAJOR_VERSION);
            // 常量池
            cp.write(dout);
            // 其他結構,可參考class文件結構
            dout.writeShort(ACC_PUBLIC | ACC_FINAL | ACC_SUPER);
            dout.writeShort(cp.getClass(dotToSlash(className)));
            dout.writeShort(cp.getClass(superclassName));
            dout.writeShort(interfaces.length);
            for (int i = 0; i < interfaces.length; i++) {
                dout.writeShort(cp.getClass(
                    dotToSlash(interfaces[i].getName())));
            }
            dout.writeShort(fields.size());
            for (FieldInfo f : fields) {
                f.write(dout);
            }
            dout.writeShort(methods.size());           
            for (MethodInfo m : methods) {
                m.write(dout);
            }
            dout.writeShort(0); 
        } catch (IOException e) {
            throw new InternalError("unexpected I/O Exception", e);
        }
        // 將class文件字節數組返回
        return bout.toByteArray();
    }

構建$Proxy對象

newProxyInstance()上半部分經過上面層層代碼調用,獲取了$Proxy0.class,接下來看下部分代碼:

newInstance

cl就是上面獲取的Proxy0.class,h就是上面傳入的userHandler,被當做構造參數來創建$Proxy0對象。然后獲取這個動態代理對象,調用sayHello()方法,相當於調用了UserHandler的invoke(),這里就是UserHandler的作用

$Proxy.class文件

我們開啟了將代理class寫到本地目錄的功能,在項目下的com/sum/proxy目錄下找到了$Proxy0的class文件。

看一下反編譯的class

package com.sun.proxy;

import com.test.proxy.UserInterface;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements UserInterface {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final void sayHello() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.test.proxy.UserInterface").getMethod("sayHello");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

結語

上面就是動態代理源碼的調試過程,與之前的猜想的代理類的生成過程比較,動態代理是直接生成class文件,省去了java文件和編譯這一塊。

剛開始看可能比較繞,跟着注釋及跳轉指引,耐心多看兩遍就明白了。動態代理涉及的知識點比較多,我自己看的時候,在WeakCache這一塊糾結了一陣,其實把它當成一個兩層的map對待即可,只不過里面所有的KV都被弱引用包裝。****



寫的都是日常工作中的親身實踐,處於自己的角度從0寫到1,保證能夠真正讓大家看懂。

文章會在公眾號 [入門到放棄之路] 首發,期待你的關注。

公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM